<?php
/**
 * calidad.php
 * ============================================================
 * BReport · MÓDULO CALIDAD (v3.3)
 * ------------------------------------------------------------
 * Secciones:
 * - Merma
 * - Scrap
 * - Sobrante Cubicado
 * - Monitoreo (Temperaturas + PPM)
 * - Salmuera (Tanques A-F)
 *
 * CAMBIOS:
 * - Corrige CSS anidado
 * - Rediseño visual a fondo blanco y texto legible
 * - Agrega sección Sobrante Cubicado
 * - Al bajar sobrante se descuenta vía inventory_moves OUT
 * - Agrega sección Salmuera con 5 inspecciones por turno y grado único por turno
 * - Crea tablas auxiliares automáticamente si no existen
 * - Crea tanques A-F automáticamente
 * - Disposición de merma con cálculo automático Merma = Disponible - Scrap
 * - Confirmación previa y resumen posterior del movimiento
 * - Sobrante Cubicado: baja total/parcial a SCRAP, captura decimal a 2 decimales
 * ============================================================
 */

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

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

$scope = scope();
$companyId  = (int)$scope['company_id'];
$locationId = (int)$scope['location_id'];
$uid = (int)($u['id'] ?? 0);

$canWrite = has_role('admin') || has_role('quality');
$action = (string)($_GET['action'] ?? '');

/* ===================== Helpers generales ===================== */
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 {
  $m = strtoupper((string)($_SERVER['REQUEST_METHOD'] ?? 'GET'));
  if ($m === 'GET') return;
  $hdr = $_SERVER['HTTP_X_CSRF'] ?? '';
  if ($hdr === '') jexit(['ok'=>false,'error'=>'Falta X-CSRF'], 419);
  csrf_verify($hdr);
}
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 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 norm_shift(int $shift): int {
  return in_array($shift, [1,2,3], true) ? $shift : 1;
}
function shift_label(int $shift): string {
  return match($shift) {
    1 => 'Turno 1',
    2 => 'Turno 2',
    3 => 'Turno 3',
    default => 'Turno 1'
  };
}
function shift_time_range(int $shift): array {
  return match($shift) {
    1 => ['start' => '07:00:00', 'end' => '15:00:00'],
    2 => ['start' => '15:00:00', 'end' => '22:30:00'],
    3 => ['start' => '22:30:00', 'end' => '07:00:00'],
    default => ['start' => '07:00:00', 'end' => '15:00:00'],
  };
}
function shift_handover_slot(int $shift): string {
  return match($shift) {
    1 => '07:00',
    2 => '15:00',
    3 => '22:30',
    default => '07:00',
  };
}
function shift_brine_slots(int $shift): array {
  return match($shift) {
    1 => ['07:00','09:00','11:00','13:00','14:45'],
    2 => ['15:00','17:00','19:00','21:00','22:15'],
    3 => ['22:30','00:30','02:30','04:30','06:30'],
    default => ['07:00','09:00','11:00','13:00','14:45'],
  };
}
function shift_periodic_slots(int $shift): array {
  return match($shift) {
    1 => ['08:00','10:00','12:00','14:00'],
    2 => ['16:00','18:00','20:00','22:00'],
    3 => ['00:00','02:00','04:00','06:00'],
    default => ['08:00','10:00','12:00','14:00'],
  };
}
function calc_temp_status(float $value): string {
  if ($value <= -5.0) return 'OK';
  if ($value <= -1.0) return 'WARN';
  return 'ALERT';
}
function calc_ppm_status(float $value): string {
  if ($value >= 15.0 && $value <= 25.0) return 'OK';
  if (($value >= 12.0 && $value < 15.0) || ($value > 25.0 && $value <= 28.0)) return 'WARN';
  return 'ALERT';
}
function calc_monitor_status(string $pointType, float $value): string {
  return strtoupper($pointType) === 'TEMP'
    ? calc_temp_status($value)
    : calc_ppm_status($value);
}
function combine_date_time(string $date, string $hhmm, int $shift): string {
  return $date . ' ' . $hhmm . ':00';
}
function normalize_hhmm(string $hhmm): ?string {
  $hhmm = trim($hhmm);
  if ($hhmm === '') return null;
  if (!preg_match('/^([01]?\d|2[0-3]):([0-5]\d)$/', $hhmm, $m)) return null;
  return str_pad($m[1], 2, '0', STR_PAD_LEFT) . ':' . $m[2];
}
function combine_manual_datetime(string $date, string $hhmm, int $shift): string {
  $hhmm = normalize_hhmm($hhmm);
  if ($hhmm === null) {
    throw new InvalidArgumentException('Hora inválida. Usa formato HH:MM');
  }

  $baseDate = $date;
  if ($shift === 3 && strcmp($hhmm, '07:00') < 0) {
    $baseDate = (new DateTimeImmutable($date))->modify('+1 day')->format('Y-m-d');
  }

  return $baseDate . ' ' . $hhmm . ':00';
}
function get_previous_shift_context(string $date, int $shift): array {
  $d = new DateTimeImmutable($date);
  return match($shift) {
    1 => ['work_date' => $d->modify('-1 day')->format('Y-m-d'), 'shift' => 3],
    2 => ['work_date' => $date, 'shift' => 1],
    3 => ['work_date' => $date, 'shift' => 2],
    default => ['work_date' => $date, 'shift' => 1],
  };
}
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);
}

/**
 * Producto destino para la merma aprovechable.
 * REGLA OFICIAL:
 * Toda merma que Calidad dispone como aprovechable debe entrar como el producto
 * APROVECHAMIENTO DE MERMA en el almacén APROV-MERMA.
 *
 * No se permite fallback a BOLSA TRIT / TRITURADO porque eso contamina el
 * inventario de producto terminado y hace que la tarjeta de aprovechamiento no
 * coincida con el kardex real.
 */
function get_aprovechamiento_merma_product_id(PDO $pdo, int $companyId): int {
  $st = $pdo->prepare("
    SELECT id
    FROM products
    WHERE company_id=?
      AND active=1
      AND (
        UPPER(TRIM(sku)) = 'APROVECHAMIENTO DE MERMA'
        OR UPPER(TRIM(name)) = 'APROVECHAMIENTO DE MERMA'
        OR UPPER(CONCAT(COALESCE(sku,''),' ',COALESCE(name,''))) LIKE '%APROVECHAMIENTO%MERMA%'
      )
    ORDER BY
      CASE
        WHEN UPPER(TRIM(sku)) = 'APROVECHAMIENTO DE MERMA' THEN 0
        WHEN UPPER(TRIM(name)) = 'APROVECHAMIENTO DE MERMA' THEN 1
        ELSE 2
      END,
      id
    LIMIT 1
  ");
  $st->execute([$companyId]);
  return (int)($st->fetchColumn() ?: 0);
}

function ensure_merma_source_warehouse(PDO $pdo, int $companyId, int $locationId): int {
  /* Almacén donde Producción deja la merma pendiente de disposición. */
  $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 ensure_aprovechamiento_merma_warehouse(PDO $pdo, int $companyId, int $locationId): int {
  $st = $pdo->prepare("
    SELECT id
    FROM warehouses
    WHERE company_id=? AND location_id=? AND active=1
      AND (
        UPPER(code) IN ('APROV-MERMA','APROVECHAMIENTO')
        OR UPPER(name) LIKE '%APROVECHAMIENTO%MERMA%'
      )
    ORDER BY
      CASE
        WHEN UPPER(code)='APROV-MERMA' THEN 0
        WHEN UPPER(name) LIKE '%APROVECHAMIENTO%MERMA%' THEN 1
        ELSE 2
      END,
      id
    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(?, ?, 'APROV-MERMA', 'Aprovechamiento de Merma', 1, NOW())
  ");
  $st->execute([$companyId, $locationId]);
  return (int)$pdo->lastInsertId();
}

function get_bar_150_product_id(PDO $pdo, int $companyId): int {
  $st = $pdo->prepare("\n    SELECT id\n    FROM products\n    WHERE company_id=? AND active=1\n      AND (\n        UPPER(TRIM(sku)) = 'BARRA 150 KILOS'\n        OR UPPER(TRIM(name)) = 'HIELO BARRA 150 KG'\n        OR (UPPER(sku) LIKE '%BARRA%' AND UPPER(sku) LIKE '%150%')\n        OR (UPPER(name) LIKE '%BARRA%' AND UPPER(name) LIKE '%150%')\n      )\n    ORDER BY\n      CASE\n        WHEN UPPER(TRIM(sku)) = 'BARRA 150 KILOS' THEN 0\n        WHEN UPPER(TRIM(name)) = 'HIELO BARRA 150 KG' THEN 1\n        ELSE 9\n      END,\n      id\n    LIMIT 1\n  ");
  $st->execute([$companyId]);
  return (int)($st->fetchColumn() ?: 0);
}

function ensure_sob_cub_warehouse(PDO $pdo, int $companyId, int $locationId): int {
  $id = wh_id_by_code($pdo, $companyId, $locationId, 'SOB-CUB');
  if ($id > 0) return $id;

  $st = $pdo->prepare("\n    INSERT INTO warehouses(company_id, location_id, code, name, active, created_at)\n    VALUES(?, ?, 'SOB-CUB', 'Sobrante Cubicado', 1, NOW())\n  ");
  $st->execute([$companyId, $locationId]);
  return (int)$pdo->lastInsertId();
}

function get_product_label(PDO $pdo, int $companyId, int $productId): string {
  $st = $pdo->prepare("SELECT CONCAT(COALESCE(sku,''), ' · ', COALESCE(name,'')) FROM products WHERE company_id=? AND id=? LIMIT 1");
  $st->execute([$companyId, $productId]);
  return trim((string)($st->fetchColumn() ?: ('Producto #' . $productId)));
}

/**
 * Convierte la merma dispuesta a equivalente de BARRA 150 KILOS para SOB-CUB.
 * Regla oficial:
 * - Si la merma origen ya es BARRA 150 KILOS, entra la misma cantidad de barras a SOB-CUB.
 * - Si la merma origen es artículo de cubicado, se usa product_conversions con origen BARRA 150 KILOS.
 *   factor = piezas por barra, por lo tanto barras_equivalentes = piezas_merma / factor.
 * - No se asume factor si no existe: se bloquea la disposición para evitar contaminar inventario.
 */
function merma_to_sob_cub_equivalent(PDO $pdo, int $companyId, int $sourceProductId, float $qtyMerma): array {
  $qtyMerma = round($qtyMerma, 6);
  if ($qtyMerma <= 0) {
    throw new RuntimeException('Cantidad de merma inválida para convertir a SOB-CUB.');
  }

  $barProductId = get_bar_150_product_id($pdo, $companyId);
  if ($barProductId <= 0) {
    throw new RuntimeException('No se encontró el producto BARRA 150 KILOS para registrar SOB-CUB.');
  }

  if ($sourceProductId === $barProductId) {
    return [
      'bar_product_id' => $barProductId,
      'qty_bars' => $qtyMerma,
      'factor' => 1.0,
      'mode' => 'SOURCE_IS_BAR'
    ];
  }

  if (!table_exists($pdo, 'product_conversions')) {
    throw new RuntimeException('No existe tabla product_conversions para convertir la merma a equivalente de barra.');
  }

  $st = $pdo->prepare("\n    SELECT factor\n    FROM product_conversions\n    WHERE company_id=?\n      AND from_product_id=?\n      AND to_product_id=?\n      AND active=1\n    ORDER BY id DESC\n    LIMIT 1\n  ");
  $st->execute([$companyId, $barProductId, $sourceProductId]);
  $factor = (float)($st->fetchColumn() ?: 0);
  if ($factor > 0) {
    return [
      'bar_product_id' => $barProductId,
      'qty_bars' => round($qtyMerma / $factor, 6),
      'factor' => $factor,
      'mode' => 'BAR_TO_PRODUCT'
    ];
  }

  // Respaldo por si alguna instalación capturó la conversión inversa.
  $st = $pdo->prepare("\n    SELECT factor\n    FROM product_conversions\n    WHERE company_id=?\n      AND from_product_id=?\n      AND to_product_id=?\n      AND active=1\n    ORDER BY id DESC\n    LIMIT 1\n  ");
  $st->execute([$companyId, $sourceProductId, $barProductId]);
  $reverseFactor = (float)($st->fetchColumn() ?: 0);
  if ($reverseFactor > 0) {
    return [
      'bar_product_id' => $barProductId,
      'qty_bars' => round($qtyMerma * $reverseFactor, 6),
      'factor' => $reverseFactor,
      'mode' => 'PRODUCT_TO_BAR'
    ];
  }

  throw new RuntimeException('No existe conversión activa desde BARRA 150 KILOS hacia ' . get_product_label($pdo, $companyId, $sourceProductId) . '. Configura product_conversions antes de disponer esta merma a SOB-CUB.');
}


function create_inventory_move_type(PDO $pdo, int $companyId, int $locationId, ?int $warehouseId, string $workDate, int $productId, float $qty, string $moveType, string $refType, int $refId, ?string $notes, int $uid): int {
  $moveType = strtoupper($moveType);
  if (!in_array($moveType, ['IN','OUT','ADJ'], true)) {
    $moveType = 'OUT';
  }

  $st = $pdo->prepare("
    INSERT INTO inventory_moves
      (company_id, location_id, warehouse_id, work_date, product_id, qty, move_type, channel_id, ref_type, ref_id, notes, created_by, created_at)
    VALUES
      (?, ?, ?, ?, ?, ?, ?, NULL, ?, ?, ?, ?, NOW())
  ");
  $st->execute([$companyId, $locationId, $warehouseId, $workDate, $productId, $qty, $moveType, $refType, $refId, $notes, $uid]);
  return (int)$pdo->lastInsertId();
}

function inventory_balance_by_warehouse(PDO $pdo, int $companyId, int $locationId, int $productId, int $warehouseId): float {
  if ($productId <= 0 || $warehouseId <= 0) return 0.0;

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


function get_leftover_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%' OR UPPER(name) LIKE '%BARRA%')
      AND (UPPER(sku) LIKE '%150%' OR UPPER(name) LIKE '%150%')
    ORDER BY id
    LIMIT 1
  ");
  $st->execute([$companyId]);
  $r = $st->fetch(PDO::FETCH_ASSOC);
  return $r ?: null;
}
function calc_leftover_available(PDO $pdo, int $companyId, int $locationId, int $productId): array {
  $whId = wh_id_by_code($pdo, $companyId, $locationId, 'SOB-CUB');
  if ($whId <= 0) {
    return ['warehouse_id'=>0, 'bars'=>0.0, 'kg'=>0.0];
  }

  /*
    FUENTE ÚNICA REAL PARA SOBRANTE:
    La disponibilidad ya no se calcula desde cubing_entries menos disposiciones.
    Se toma del kardex oficial inventory_moves en el almacén SOB-CUB.
  */
  $availableBars = inventory_balance_by_warehouse($pdo, $companyId, $locationId, $productId, $whId);
  if ($availableBars < 0 && abs($availableBars) < 0.0001) $availableBars = 0.0;

  return [
    'warehouse_id' => $whId,
    'bars' => $availableBars,
    'kg'   => $availableBars * 150.0
  ];
}
function calc_brine_status_deg(float $v): string {
  if ($v >= 22.0 && $v <= 26.0) return 'OK';
  if (($v >= 20.0 && $v < 22.0) || ($v > 26.0 && $v <= 28.0)) return 'WARN';
  return 'ALERT';
}
function calc_brine_status_temp(float $v): string {
  if ($v <= -8.0) return 'OK';
  if ($v <= -4.0) return 'WARN';
  return 'ALERT';
}
function brine_primary_slot(int $shift): string {
  $slots = shift_brine_slots($shift);
  return (string)($slots[0] ?? '07:00');
}
function get_shift_brine_degree(PDO $pdo, int $companyId, int $locationId, string $workDate, int $shift): ?array {
  $st = $pdo->prepare("
    SELECT id, degree_value, status_code, captured_at, notes
    FROM quality_brine_shift_degree
    WHERE company_id=? AND location_id=? AND work_date=? AND shift=?
    LIMIT 1
  ");
  $st->execute([$companyId, $locationId, $workDate, $shift]);
  $r = $st->fetch(PDO::FETCH_ASSOC);
  return $r ?: null;
}
function get_tank_brine_degrees(PDO $pdo, int $companyId, int $locationId, string $workDate, int $shift): array {
  if (!table_exists($pdo, 'quality_brine_tank_degree')) {
    return [];
  }

  $st = $pdo->prepare("
    SELECT id, tank_id, degree_value, status_code, captured_at, notes
    FROM quality_brine_tank_degree
    WHERE company_id=? AND location_id=? AND work_date=? AND shift=?
  ");
  $st->execute([$companyId, $locationId, $workDate, $shift]);

  $map = [];
  while ($r = $st->fetch(PDO::FETCH_ASSOC)) {
    $map[(int)$r['tank_id']] = $r;
  }
  return $map;
}
function get_tank_brine_degree(PDO $pdo, int $companyId, int $locationId, string $workDate, int $shift, int $tankId): ?array {
  $map = get_tank_brine_degrees($pdo, $companyId, $locationId, $workDate, $shift);
  return $map[$tankId] ?? null;
}

function ensure_quality_scrap_seed(PDO $pdo, int $companyId): void {
  $st = $pdo->prepare("
    INSERT INTO quality_concepts(company_id, concept_type, name, active, sort_order)
    SELECT ?, 'SCRAP', 'Scrap dispuesto', 1, 1
    WHERE NOT EXISTS (
      SELECT 1 FROM quality_concepts
      WHERE company_id=? AND concept_type='SCRAP' AND active=1
    )
  ");
  $st->execute([$companyId, $companyId]);
}
function get_default_scrap_concept_id(PDO $pdo, int $companyId): int {
  ensure_quality_scrap_seed($pdo, $companyId);
  $st = $pdo->prepare("
    SELECT id
    FROM quality_concepts
    WHERE company_id=? AND concept_type='SCRAP' AND active=1
    ORDER BY sort_order, id
    LIMIT 1
  ");
  $st->execute([$companyId]);
  return (int)($st->fetchColumn() ?: 0);
}
function quality_merma_aggregated_rows(PDO $pdo, int $companyId, int $locationId, string $date, int $shift, int $productId=0): array {
  $params = [$companyId, $locationId, $date, $shift, $productId, $productId, $companyId, $locationId, $date, $shift];
  $st = $pdo->prepare("
    SELECT base.product_id, base.sku, base.product_name, base.uom,
           base.concept_id, base.concept_name, base.captured_qty, base.capture_count,
           COALESCE(d.qty_merma,0) AS disposed_merma_qty,
           COALESCE(d.qty_scrap,0) AS disposed_scrap_qty,
           GREATEST(base.captured_qty - COALESCE(d.qty_total,0), 0) AS evidence_available_qty
    FROM (
      SELECT qe.product_id, p.sku, p.name AS product_name, p.uom,
             qe.concept_id, qc.name AS concept_name,
             SUM(qe.qty) AS captured_qty, COUNT(*) AS capture_count
      FROM quality_entries qe
      INNER JOIN products p ON p.id = qe.product_id
      INNER JOIN quality_concepts qc ON qc.id = qe.concept_id
      WHERE qe.company_id=? AND qe.location_id=?
        AND qe.work_date=? AND qe.shift=?
        AND (?=0 OR qe.product_id=?)
        AND qe.concept_type='MERMA'
      GROUP BY qe.product_id, p.sku, p.name, p.uom, qe.concept_id, qc.name
    ) base
    LEFT JOIN (
      SELECT product_id, source_concept_id,
             SUM(COALESCE(qty_merma,0)) AS qty_merma,
             SUM(COALESCE(qty_scrap,0)) AS qty_scrap,
             SUM(COALESCE(qty_merma,0) + COALESCE(qty_scrap,0)) AS qty_total
      FROM quality_merma_disposals
      WHERE company_id=? AND location_id=? AND work_date=? AND shift=?
      GROUP BY product_id, source_concept_id
    ) d ON d.product_id = base.product_id AND d.source_concept_id = base.concept_id
    ORDER BY base.sku, base.concept_name
  ");
  $st->execute($params);
  $rows = $st->fetchAll(PDO::FETCH_ASSOC) ?: [];

  /*
    La disponibilidad que Calidad puede disponer debe existir físicamente en MERMA.
    quality_entries queda como evidencia, pero el movimiento real se valida contra
    inventory_moves del almacén MERMA.
  */
  $mermaWhId = ensure_merma_source_warehouse($pdo, $companyId, $locationId);
  $stockCache = [];
  foreach ($rows as &$r) {
    $pid = (int)$r['product_id'];
    if (!array_key_exists($pid, $stockCache)) {
      $stockCache[$pid] = $mermaWhId > 0 ? inventory_balance_by_warehouse($pdo, $companyId, $locationId, $pid, $mermaWhId) : 0.0;
    }
    $realStock = max(0.0, (float)$stockCache[$pid]);
    $evidenceAvailable = max(0.0, (float)($r['evidence_available_qty'] ?? 0));
    $r['merma_warehouse_qty'] = $realStock;
    $r['available_qty'] = min($evidenceAvailable, $realStock);
  }
  unset($r);

  return $rows;
}
function quality_merma_available(PDO $pdo, int $companyId, int $locationId, string $date, int $shift, int $productId, int $conceptId): float {
  foreach (quality_merma_aggregated_rows($pdo, $companyId, $locationId, $date, $shift, $productId) as $r) {
    if ((int)$r['product_id'] === $productId && (int)$r['concept_id'] === $conceptId) {
      return (float)$r['available_qty'];
    }
  }
  return 0.0;
}

function quality_merma_detail_rows(PDO $pdo, int $companyId, int $locationId, string $date, int $shift, int $productId, int $conceptId): array {
  $st = $pdo->prepare("
    SELECT qe.id, qe.work_date, qe.shift, qe.product_id, qe.concept_id, qe.qty, qe.notes, qe.created_at, qe.updated_at,
           p.sku, p.name AS product_name, p.uom,
           qc.name AS concept_name
    FROM quality_entries qe
    INNER JOIN products p ON p.id = qe.product_id
    INNER JOIN quality_concepts qc ON qc.id = qe.concept_id
    WHERE qe.company_id=? AND qe.location_id=?
      AND qe.work_date=? AND qe.shift=?
      AND qe.product_id=? AND qe.concept_id=?
      AND qe.concept_type='MERMA'
    ORDER BY COALESCE(qe.created_at, qe.updated_at) DESC, qe.id DESC
  ");
  $st->execute([$companyId, $locationId, $date, $shift, $productId, $conceptId]);
  return $st->fetchAll(PDO::FETCH_ASSOC) ?: [];
}

function quality_summary_from_rows(array $rows, string $type): array {
  $sum = [];
  foreach ($rows as $r) {
    $pid = (int)$r['product_id'];
    if (!isset($sum[$pid])) {
      $sum[$pid] = [
        'product_id' => $pid,
        'sku' => $r['sku'],
        'product_name' => $r['product_name'],
        'uom' => $r['uom'],
        'qty' => 0.0,
      ];
    }
    $sum[$pid]['qty'] += (float)($type === 'MERMA' ? ($r['available_qty'] ?? 0) : ($r['captured_qty'] ?? $r['qty'] ?? 0));
  }
  return array_values($sum);
}
function create_inventory_move(PDO $pdo, int $companyId, int $locationId, ?int $warehouseId, string $workDate, int $productId, float $qty, string $refType, int $refId, ?string $notes, int $uid): int {
  $st = $pdo->prepare("
    INSERT INTO inventory_moves
      (company_id, location_id, warehouse_id, work_date, product_id, qty, move_type, channel_id, ref_type, ref_id, notes, created_by, created_at)
    VALUES
      (?, ?, ?, ?, ?, ?, 'OUT', NULL, ?, ?, ?, ?, NOW())
  ");
  $st->execute([$companyId, $locationId, $warehouseId, $workDate, $productId, $qty, $refType, $refId, $notes, $uid]);
  return (int)$pdo->lastInsertId();
}
function create_inv_ledger_if_exists(PDO $pdo, int $companyId, int $locationId, int $productId, string $movementType, float $qty, string $refTable, int $refId, ?string $comment, int $uid): void {
  if (!table_exists($pdo, 'inv_ledger')) return;
  $st = $pdo->prepare("
    INSERT INTO inv_ledger
      (company_id, location_id, product_id, occurred_at, movement_type, qty_signed, ref_table, ref_id, created_by, comment)
    VALUES
      (?, ?, ?, NOW(), ?, ?, ?, ?, ?, ?)
  ");
  $st->execute([$companyId, $locationId, $productId, $movementType, 0 - abs($qty), $refTable, $refId, $uid, $comment]);
}

function ensure_quality_extras(PDO $pdo): void {
  if (!table_exists($pdo, 'quality_cubing_leftover_disposals')) {
    $pdo->exec("
      CREATE TABLE quality_cubing_leftover_disposals (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT(20) UNSIGNED NOT NULL,
        location_id BIGINT(20) UNSIGNED NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT(3) UNSIGNED NOT NULL,
        disposal_time DATETIME NOT NULL,
        product_id BIGINT(20) UNSIGNED NOT NULL,
        warehouse_id BIGINT(20) UNSIGNED NOT NULL,
        qty_bars DECIMAL(14,6) NOT NULL DEFAULT 0.000000,
        qty_kg DECIMAL(14,3) NOT NULL DEFAULT 0.000,
        reason VARCHAR(120) DEFAULT NULL,
        notes VARCHAR(255) DEFAULT NULL,
        inventory_move_id BIGINT(20) UNSIGNED DEFAULT NULL,
        quality_entry_scrap_id BIGINT(20) UNSIGNED DEFAULT NULL,
        created_by BIGINT(20) UNSIGNED NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_by BIGINT(20) UNSIGNED DEFAULT NULL,
        updated_at DATETIME DEFAULT NULL,
        PRIMARY KEY (id),
        KEY idx_qcld_scope_date (company_id, location_id, work_date, shift),
        KEY idx_qcld_wh (warehouse_id),
        KEY idx_qcld_product (product_id)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    ");
  }

  if (table_exists($pdo, 'quality_cubing_leftover_disposals')) {
    if (!col_exists($pdo, 'quality_cubing_leftover_disposals', 'quality_entry_scrap_id')) {
      $pdo->exec("ALTER TABLE quality_cubing_leftover_disposals ADD COLUMN quality_entry_scrap_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER inventory_move_id");
    }
  }


  if (!table_exists($pdo, 'quality_merma_disposals')) {
    $pdo->exec("
      CREATE TABLE quality_merma_disposals (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT(20) UNSIGNED NOT NULL,
        location_id BIGINT(20) UNSIGNED NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT(3) UNSIGNED NOT NULL,
        disposal_time DATETIME NOT NULL,
        product_id BIGINT(20) UNSIGNED NOT NULL,
        source_concept_id BIGINT(20) UNSIGNED NOT NULL,
        qty_merma DECIMAL(14,3) NOT NULL DEFAULT 0.000,
        qty_scrap DECIMAL(14,3) NOT NULL DEFAULT 0.000,
        notes VARCHAR(255) DEFAULT NULL,
        inventory_move_merma_id BIGINT(20) UNSIGNED DEFAULT NULL,
        inventory_move_scrap_id BIGINT(20) UNSIGNED DEFAULT NULL,
        created_by BIGINT(20) UNSIGNED NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_by BIGINT(20) UNSIGNED DEFAULT NULL,
        updated_at DATETIME DEFAULT NULL,
        PRIMARY KEY (id),
        KEY idx_qmd_scope (company_id, location_id, work_date, shift),
        KEY idx_qmd_product_concept (product_id, source_concept_id)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    ");
  }

  if (table_exists($pdo, 'quality_merma_disposals')) {
    if (!col_exists($pdo, 'quality_merma_disposals', 'disposal_time')) {
      $pdo->exec("ALTER TABLE quality_merma_disposals ADD COLUMN disposal_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP AFTER shift");
      $pdo->exec("UPDATE quality_merma_disposals SET disposal_time = COALESCE(created_at, NOW()) WHERE disposal_time IS NULL");
    }
    if (!col_exists($pdo, 'quality_merma_disposals', 'source_concept_id')) {
      $pdo->exec("ALTER TABLE quality_merma_disposals ADD COLUMN source_concept_id BIGINT(20) UNSIGNED NOT NULL DEFAULT 0 AFTER product_id");
    }
    if (!col_exists($pdo, 'quality_merma_disposals', 'qty_merma')) {
      $pdo->exec("ALTER TABLE quality_merma_disposals ADD COLUMN qty_merma DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER source_concept_id");
    }
    if (!col_exists($pdo, 'quality_merma_disposals', 'qty_scrap')) {
      $pdo->exec("ALTER TABLE quality_merma_disposals ADD COLUMN qty_scrap DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER qty_merma");
    }
    if (!col_exists($pdo, 'quality_merma_disposals', 'inventory_move_merma_id')) {
      $pdo->exec("ALTER TABLE quality_merma_disposals ADD COLUMN inventory_move_merma_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER notes");
    }
    if (!col_exists($pdo, 'quality_merma_disposals', 'inventory_move_scrap_id')) {
      $pdo->exec("ALTER TABLE quality_merma_disposals ADD COLUMN inventory_move_scrap_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER inventory_move_merma_id");
    }
  }


  if (!table_exists($pdo, 'quality_brine_tank_degree')) {
    $pdo->exec("
      CREATE TABLE quality_brine_tank_degree (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT(20) UNSIGNED NOT NULL,
        location_id BIGINT(20) UNSIGNED NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT(3) UNSIGNED NOT NULL,
        tank_id BIGINT(20) UNSIGNED NOT NULL,
        degree_value DECIMAL(8,2) NOT NULL DEFAULT 0.00,
        status_code VARCHAR(10) NOT NULL DEFAULT 'OK',
        captured_at DATETIME NOT NULL,
        notes VARCHAR(255) DEFAULT NULL,
        created_by BIGINT(20) UNSIGNED NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_by BIGINT(20) UNSIGNED DEFAULT NULL,
        updated_at DATETIME DEFAULT NULL,
        PRIMARY KEY (id),
        UNIQUE KEY uq_qbtd_scope (company_id, location_id, work_date, shift, tank_id),
        KEY idx_qbtd_scope (company_id, location_id, work_date, shift),
        KEY idx_qbtd_tank (tank_id)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    ");
  }

  if (!table_exists($pdo, 'quality_brine_tanks')) {
    $pdo->exec("
      CREATE TABLE quality_brine_tanks (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT(20) UNSIGNED NOT NULL,
        location_id BIGINT(20) UNSIGNED NOT NULL,
        tank_code VARCHAR(10) NOT NULL,
        tank_name VARCHAR(60) NOT NULL,
        active TINYINT(1) NOT NULL DEFAULT 1,
        sort_order INT NOT NULL DEFAULT 0,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        UNIQUE KEY uq_brine_tank (company_id, location_id, tank_code)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    ");
  }

  if (!table_exists($pdo, 'quality_brine_readings')) {
    $pdo->exec("
      CREATE TABLE quality_brine_readings (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT(20) UNSIGNED NOT NULL,
        location_id BIGINT(20) UNSIGNED NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT(3) UNSIGNED NOT NULL,
        tank_id BIGINT(20) UNSIGNED NOT NULL,
        slot_label VARCHAR(10) NOT NULL,
        reading_at DATETIME NOT NULL,
        brine_degree DECIMAL(8,2) NOT NULL DEFAULT 0.00,
        temp_c DECIMAL(8,2) NOT NULL DEFAULT 0.00,
        brine_status VARCHAR(10) NOT NULL DEFAULT 'OK',
        temp_status VARCHAR(10) NOT NULL DEFAULT 'OK',
        notes VARCHAR(255) DEFAULT NULL,
        created_by BIGINT(20) UNSIGNED NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_by BIGINT(20) UNSIGNED DEFAULT NULL,
        updated_at DATETIME DEFAULT NULL,
        PRIMARY KEY (id),
        UNIQUE KEY uq_brine_slot (company_id, location_id, work_date, shift, tank_id, slot_label),
        KEY idx_brine_scope (company_id, location_id, work_date, shift),
        KEY idx_brine_tank (tank_id)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    ");
  }

  if (!table_exists($pdo, 'quality_brine_shift_degree')) {
    $pdo->exec("
      CREATE TABLE quality_brine_shift_degree (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT(20) UNSIGNED NOT NULL,
        location_id BIGINT(20) UNSIGNED NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT(3) UNSIGNED NOT NULL,
        degree_value DECIMAL(8,2) NOT NULL DEFAULT 0.00,
        status_code VARCHAR(10) NOT NULL DEFAULT 'OK',
        captured_at DATETIME NOT NULL,
        notes VARCHAR(255) DEFAULT NULL,
        created_by BIGINT(20) UNSIGNED NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        updated_by BIGINT(20) UNSIGNED DEFAULT NULL,
        updated_at DATETIME DEFAULT NULL,
        PRIMARY KEY (id),
        UNIQUE KEY uq_qbsd_scope (company_id, location_id, work_date, shift),
        KEY idx_qbsd_scope (company_id, location_id, work_date, shift)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
    ");
  }

  if (!col_exists($pdo, 'quality_monitor_readings', 'captured_hhmm')) {
    $pdo->exec("ALTER TABLE quality_monitor_readings ADD COLUMN captured_hhmm VARCHAR(5) DEFAULT NULL AFTER slot_label");
    $pdo->exec("UPDATE quality_monitor_readings SET captured_hhmm = DATE_FORMAT(reading_at, '%H:%i') WHERE captured_hhmm IS NULL OR captured_hhmm = ''");
  }

  if (!col_exists($pdo, 'quality_brine_readings', 'captured_hhmm')) {
    $pdo->exec("ALTER TABLE quality_brine_readings ADD COLUMN captured_hhmm VARCHAR(5) DEFAULT NULL AFTER slot_label");
    $pdo->exec("UPDATE quality_brine_readings SET captured_hhmm = DATE_FORMAT(reading_at, '%H:%i') WHERE captured_hhmm IS NULL OR captured_hhmm = ''");
  }

  if (table_exists($pdo, 'quality_brine_shift_degree') && table_exists($pdo, 'quality_brine_readings')) {
    $pdo->exec("
      INSERT IGNORE INTO quality_brine_shift_degree
      (company_id, location_id, work_date, shift, degree_value, status_code, captured_at, notes, created_by, created_at)
      SELECT r.company_id, r.location_id, r.work_date, r.shift,
             r.brine_degree,
             COALESCE(NULLIF(r.brine_status,''), 'OK'),
             r.reading_at,
             r.notes,
             r.created_by,
             COALESCE(r.created_at, NOW())
      FROM quality_brine_readings r
      INNER JOIN (
        SELECT company_id, location_id, work_date, shift, MIN(id) AS min_id
        FROM quality_brine_readings
        GROUP BY company_id, location_id, work_date, shift
      ) x ON x.min_id = r.id
    ");
  }

  if (table_exists($pdo, 'quality_brine_tank_degree') && table_exists($pdo, 'quality_brine_readings')) {
    $pdo->exec("
      INSERT IGNORE INTO quality_brine_tank_degree
      (company_id, location_id, work_date, shift, tank_id, degree_value, status_code, captured_at, notes, created_by, created_at)
      SELECT r.company_id, r.location_id, r.work_date, r.shift, r.tank_id,
             r.brine_degree,
             COALESCE(NULLIF(r.brine_status,''), 'OK'),
             r.reading_at,
             r.notes,
             r.created_by,
             COALESCE(r.created_at, NOW())
      FROM quality_brine_readings r
      INNER JOIN (
        SELECT company_id, location_id, work_date, shift, tank_id, MIN(id) AS min_id
        FROM quality_brine_readings
        GROUP BY company_id, location_id, work_date, shift, tank_id
      ) x ON x.min_id = r.id
    ");
  }

  if (table_exists($pdo, 'quality_brine_tank_degree') && table_exists($pdo, 'quality_brine_shift_degree') && table_exists($pdo, 'quality_brine_tanks')) {
    $pdo->exec("
      INSERT IGNORE INTO quality_brine_tank_degree
      (company_id, location_id, work_date, shift, tank_id, degree_value, status_code, captured_at, notes, created_by, created_at)
      SELECT sd.company_id, sd.location_id, sd.work_date, sd.shift, t.id,
             sd.degree_value,
             sd.status_code,
             sd.captured_at,
             CONCAT('Migrado desde quality_brine_shift_degree', IF(sd.notes IS NULL OR sd.notes='', '', CONCAT(' | ', sd.notes))),
             sd.created_by,
             COALESCE(sd.created_at, NOW())
      FROM quality_brine_shift_degree sd
      INNER JOIN quality_brine_tanks t
        ON t.company_id = sd.company_id
       AND t.location_id = sd.location_id
       AND t.active = 1
    ");
  }
}
function ensure_brine_seed(PDO $pdo, int $companyId, int $locationId): void {
  $tanks = ['A','B','C','D','E','F'];
  $i = 1;
  foreach ($tanks as $code) {
    $st = $pdo->prepare("
      INSERT INTO quality_brine_tanks(company_id, location_id, tank_code, tank_name, sort_order)
      SELECT ?, ?, ?, ?, ?
      WHERE NOT EXISTS (
        SELECT 1 FROM quality_brine_tanks
        WHERE company_id=? AND location_id=? AND tank_code=?
      )
    ");
    $st->execute([$companyId,$locationId,$code,'Tanque '.$code,$i,$companyId,$locationId,$code]);
    $i++;
  }
}

ensure_quality_extras($pdo);
ensure_brine_seed($pdo, $companyId, $locationId);
ensure_quality_scrap_seed($pdo, $companyId);

/* ===================== API ===================== */
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);
    }
  });

  try {
    need_csrf();

    if ($action === 'products') {
      $st = $pdo->prepare("
        SELECT id, sku, name, uom
        FROM products
        WHERE company_id=? AND active=1
        ORDER BY sku
      ");
      $st->execute([$companyId]);
      jexit(['ok'=>true,'rows'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'concepts') {
      $type = strtoupper(trim((string)($_GET['concept_type'] ?? 'MERMA')));
      if (!in_array($type, ['MERMA','SCRAP'], true)) $type = 'MERMA';

      $st = $pdo->prepare("
        SELECT id, concept_type, name, sort_order
        FROM quality_concepts
        WHERE company_id=? AND concept_type=? AND active=1
        ORDER BY sort_order, name
      ");
      $st->execute([$companyId, $type]);
      jexit(['ok'=>true,'rows'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'list') {
      $date  = (string)($_GET['date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($_GET['shift'] ?? 1));
      $productId = (int)($_GET['product_id'] ?? 0);

      $type = strtoupper(trim((string)($_GET['concept_type'] ?? 'MERMA')));
      if (!in_array($type, ['MERMA','SCRAP'], true)) $type = 'MERMA';

      if ($type === 'MERMA') {
        $rows = quality_merma_aggregated_rows($pdo, $companyId, $locationId, $date, $shift, $productId);
        $sum = quality_summary_from_rows($rows, 'MERMA');
      } else {
        $st = $pdo->prepare("
          SELECT qe.product_id, p.sku, p.name AS product_name, p.uom,
                 qe.concept_id, qc.name AS concept_name,
                 SUM(qe.qty) AS captured_qty,
                 COUNT(*) AS capture_count
          FROM quality_entries qe
          JOIN products p ON p.id=qe.product_id
          JOIN quality_concepts qc ON qc.id=qe.concept_id
          WHERE qe.company_id=? AND qe.location_id=?
            AND qe.work_date=? AND qe.shift=?
            AND (?=0 OR qe.product_id=?)
            AND qe.concept_type=?
          GROUP BY qe.product_id, p.sku, p.name, p.uom, qe.concept_id, qc.name
          ORDER BY p.sku, qc.name
        ");
        $st->execute([$companyId, $locationId, $date, $shift, $productId, $productId, $type]);
        $rows = $st->fetchAll(PDO::FETCH_ASSOC);
        $sum = quality_summary_from_rows($rows, 'SCRAP');
      }

      $productionInfo = null;
      try {
        $stp = $pdo->prepare("
          SELECT pe.product_id, p.sku, p.name AS product_name, SUM(pe.qty) AS qty
          FROM production_entries pe
          JOIN products p ON p.id=pe.product_id
          WHERE pe.company_id=? AND pe.location_id=? AND pe.work_date=? AND pe.shift=?
            AND (?=0 OR pe.product_id=?)
          GROUP BY pe.product_id, p.sku, p.name
          ORDER BY p.sku
        ");
        $stp->execute([$companyId,$locationId,$date,$shift,$productId,$productId]);
        $productionInfo = $stp->fetchAll(PDO::FETCH_ASSOC);
      } catch (Throwable $e) {
        $productionInfo = null;
      }

      jexit([
        'ok'        => true,
        'rows'      => $rows,
        'summary'   => $sum,
        'production'=> $productionInfo,
        'can_write' => $canWrite
      ]);
    }

    if ($action === 'merma_detail') {
      $date  = (string)($_GET['date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($_GET['shift'] ?? 1));
      $productId = (int)($_GET['product_id'] ?? 0);
      $conceptId = (int)($_GET['concept_id'] ?? 0);

      if ($productId <= 0 || $conceptId <= 0) {
        jexit(['ok'=>false,'error'=>'product_id y concept_id son requeridos'], 400);
      }

      $rows = quality_merma_detail_rows($pdo, $companyId, $locationId, $date, $shift, $productId, $conceptId);

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

    if ($action === 'save') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);

      $d = read_json();
      $id = (int)($d['id'] ?? 0);
      $date  = (string)($d['work_date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($d['shift'] ?? 1));
      $productId = (int)($d['product_id'] ?? 0);

      $type = strtoupper(trim((string)($d['concept_type'] ?? 'MERMA')));
      if (!in_array($type, ['MERMA','SCRAP'], true)) $type = 'MERMA';

      $conceptId = (int)($d['concept_id'] ?? 0);
      $qty = (float)($d['qty'] ?? 0);
      $notes = trim((string)($d['notes'] ?? ''));
      if ($notes === '') $notes = null;

      if ($productId <= 0 || $conceptId <= 0) {
        jexit(['ok'=>false,'error'=>'product_id y concept_id son requeridos'], 400);
      }
      if ($qty < 0) {
        jexit(['ok'=>false,'error'=>'qty no puede ser negativo'], 400);
      }

      $stc = $pdo->prepare("
        SELECT id
        FROM quality_concepts
        WHERE id=? AND company_id=? AND concept_type=? AND active=1
        LIMIT 1
      ");
      $stc->execute([$conceptId, $companyId, $type]);
      if (!$stc->fetch(PDO::FETCH_ASSOC)) {
        jexit(['ok'=>false,'error'=>'Concepto inválido para esta empresa/tipo'], 400);
      }

      if ($id > 0) {
        $st = $pdo->prepare("
          UPDATE quality_entries
          SET work_date=?, shift=?, product_id=?, concept_type=?, concept_id=?, qty=?, notes=?,
              updated_by=?, updated_at=NOW()
          WHERE id=? AND company_id=? AND location_id=?
        ");
        $st->execute([$date,$shift,$productId,$type,$conceptId,$qty,$notes,$uid,$id,$companyId,$locationId]);
        jexit(['ok'=>true,'id'=>$id]);
      } else {
        $st = $pdo->prepare("
          INSERT INTO quality_entries
            (company_id,location_id,work_date,shift,product_id,concept_type,concept_id,qty,notes,created_by,created_at)
          VALUES
            (?,?,?,?,?,?,?,?,?,?,NOW())
        ");
        $st->execute([$companyId,$locationId,$date,$shift,$productId,$type,$conceptId,$qty,$notes,$uid]);
        jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
      }
    }

    if ($action === 'delete') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);
      $d = read_json();
      $id = (int)($d['id'] ?? 0);
      if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);

      $st = $pdo->prepare("DELETE FROM quality_entries WHERE id=? AND company_id=? AND location_id=?");
      $st->execute([$id,$companyId,$locationId]);
      jexit(['ok'=>true]);
    }


    if ($action === 'dispose_merma') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);

      $d = read_json();
      $date  = (string)($d['work_date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($d['shift'] ?? 1));
      $productId = (int)($d['product_id'] ?? 0);
      $conceptId = (int)($d['concept_id'] ?? 0);
      $qtyMerma = (float)($d['qty_merma'] ?? 0);
      $qtyScrap = (float)($d['qty_scrap'] ?? 0);
      $qtyTotal = $qtyMerma + $qtyScrap;
      $notes = trim((string)($d['notes'] ?? ''));
      if ($notes === '') $notes = null;

      if ($productId <= 0 || $conceptId <= 0) jexit(['ok'=>false,'error'=>'Producto y concepto origen son requeridos'], 400);
      if ($qtyTotal <= 0) jexit(['ok'=>false,'error'=>'Captura una cantidad a disponer'], 400);

      $mermaWarehouseId = ($qtyMerma > 0 || $qtyScrap > 0) ? ensure_merma_source_warehouse($pdo, $companyId, $locationId) : 0;
      if (($qtyMerma > 0 || $qtyScrap > 0) && $mermaWarehouseId <= 0) {
        jexit(['ok'=>false,'error'=>'No se encontró/creó el almacén MERMA.'], 500);
      }

      $available = quality_merma_available($pdo, $companyId, $locationId, $date, $shift, $productId, $conceptId);
      $realMermaStock = $mermaWarehouseId > 0 ? inventory_balance_by_warehouse($pdo, $companyId, $locationId, $productId, $mermaWarehouseId) : 0.0;
      if ($available <= 0 || $realMermaStock <= 0) {
        jexit([
          'ok'=>false,
          'error'=>'No hay merma física disponible en almacén MERMA para disponer en este filtro.',
          'available' => $available,
          'merma_warehouse_qty' => $realMermaStock
        ], 400);
      }
      if ($qtyTotal > $available + 0.0001 || $qtyTotal > $realMermaStock + 0.0001) {
        jexit([
          'ok'=>false,
          'error'=>'La cantidad a disponer excede la merma disponible en el kardex MERMA.',
          'available' => $available,
          'merma_warehouse_qty' => $realMermaStock
        ], 400);
      }

      $disposedAt = date('Y-m-d H:i:s');
      $scrapConceptId = $qtyScrap > 0 ? get_default_scrap_concept_id($pdo, $companyId) : 0;

      $pdo->beginTransaction();
      try {
        $st = $pdo->prepare("
          INSERT INTO quality_merma_disposals
            (company_id, location_id, work_date, shift, disposal_time, product_id, source_concept_id, qty_merma, qty_scrap, notes, created_by, created_at)
          VALUES
            (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
        ");
        $st->execute([$companyId, $locationId, $date, $shift, $disposedAt, $productId, $conceptId, $qtyMerma, $qtyScrap, $notes, $uid]);
        $disposalId = (int)$pdo->lastInsertId();

        if ($qtyMerma > 0) {
          /*
            Regla oficial actualizada:
            Toda merma aprovechable dispuesta por Calidad se mueve a SOB-CUB
            como equivalente real de BARRA 150 KILOS.

            No se registra entrada a APROV-MERMA ni al producto
            APROVECHAMIENTO DE MERMA en este flujo.

            Flujo real de inventory_moves:
            1) OUT del producto origen desde MERMA por la cantidad capturada.
            2) IN de BARRA 150 KILOS a SOB-CUB por barras equivalentes.
          */
          $sourceMermaWhId = $mermaWarehouseId;
          $sobCubWhId = ensure_sob_cub_warehouse($pdo, $companyId, $locationId);
          if ($sobCubWhId <= 0) {
            throw new RuntimeException('No se encontró/creó el almacén SOB-CUB.');
          }

          $equiv = merma_to_sob_cub_equivalent($pdo, $companyId, $productId, $qtyMerma);
          $barProductId = (int)$equiv['bar_product_id'];
          $qtyBarsToSob = round((float)$equiv['qty_bars'], 6);
          if ($qtyBarsToSob <= 0) {
            throw new RuntimeException('La conversión de merma a SOB-CUB resultó en cero. Valida factor y cantidad.');
          }

          $sourceOutNotes = 'Salida de merma origen por recuperación #' . $disposalId .
            ' | Almacén origen MERMA' .
            ' | Destino SOB-CUB como BARRA 150 KILOS equivalente' .
            ' | Factor: ' . number_format((float)$equiv['factor'], 6, '.', '') .
            ' | Modo: ' . (string)$equiv['mode'] .
            ($notes ? (' | ' . $notes) : '');

          create_inventory_move_type(
            $pdo,
            $companyId,
            $locationId,
            $sourceMermaWhId,
            $date,
            $productId,
            $qtyMerma,
            'OUT',
            'QUALITY_MERMA_SRC',
            $disposalId,
            $sourceOutNotes,
            $uid
          );

          $moveNotes = 'Ingreso a SOB-CUB desde disposición de merma #' . $disposalId .
            ' | Producto origen ID ' . $productId .
            ' | Cantidad origen: ' . number_format($qtyMerma, 3, '.', '') .
            ' | Factor: ' . number_format((float)$equiv['factor'], 6, '.', '') .
            ' | Barras equivalentes: ' . number_format($qtyBarsToSob, 6, '.', '') .
            ($notes ? (' | ' . $notes) : '');

          $inventoryMoveMermaId = create_inventory_move_type(
            $pdo,
            $companyId,
            $locationId,
            $sobCubWhId,
            $date,
            $barProductId,
            $qtyBarsToSob,
            'IN',
            'QUALITY_MERMA_SOB_CUB',
            $disposalId,
            $moveNotes,
            $uid
          );

          if (col_exists($pdo, 'quality_merma_disposals', 'inventory_move_merma_id')) {
            $upm = $pdo->prepare("UPDATE quality_merma_disposals SET inventory_move_merma_id=? WHERE id=? AND company_id=? AND location_id=?");
            $upm->execute([$inventoryMoveMermaId, $disposalId, $companyId, $locationId]);
          }
        }
        if ($qtyScrap > 0) {
          $scrapNotes = 'Generado desde disposición de merma #' . $disposalId . ($notes ? (' | ' . $notes) : '');
          $st = $pdo->prepare("
            INSERT INTO quality_entries
              (company_id,location_id,work_date,shift,product_id,concept_type,concept_id,qty,notes,created_by,created_at)
            VALUES
              (?,?,?,?,?,'SCRAP',?,?,?,?,NOW())
          ");
          $st->execute([$companyId, $locationId, $date, $shift, $productId, $scrapConceptId, $qtyScrap, $scrapNotes, $uid]);
          if ($mermaWarehouseId <= 0) {
            throw new RuntimeException('No se encontró almacén MERMA para registrar salida SCRAP.');
          }
          $inventoryMoveScrapId = create_inventory_move_type($pdo, $companyId, $locationId, $mermaWarehouseId, $date, $productId, $qtyScrap, 'OUT', 'QUALITY_SCRAP', $disposalId, $scrapNotes . ' | Salida desde MERMA', $uid);
          if (col_exists($pdo, 'quality_merma_disposals', 'inventory_move_scrap_id')) {
            $ups = $pdo->prepare("UPDATE quality_merma_disposals SET inventory_move_scrap_id=? WHERE id=? AND company_id=? AND location_id=?");
            $ups->execute([$inventoryMoveScrapId, $disposalId, $companyId, $locationId]);
          }
          create_inv_ledger_if_exists($pdo, $companyId, $locationId, $productId, 'SCRAP_OUT', $qtyScrap, 'quality_merma_disposals', $disposalId, $scrapNotes, $uid);
        }

        $pdo->commit();
        jexit([
          'ok' => true,
          'id' => $disposalId,
          'qty_merma' => $qtyMerma,
          'qty_scrap' => $qtyScrap,
          'qty_total' => $qtyTotal,
          'inventory_move_merma_id' => $inventoryMoveMermaId ?? null,
          'inventory_move_scrap_id' => $inventoryMoveScrapId ?? null,
          'message' => 'Disposición de merma registrada correctamente.'
        ]);
      } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $e;
      }
    }

    if ($action === 'leftover_dashboard') {
      $date  = (string)($_GET['date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($_GET['shift'] ?? 1));

      $prod = get_leftover_product($pdo, $companyId);
      if (!$prod) {
        jexit(['ok'=>false,'error'=>'No se encontró producto barra 150kg'], 404);
      }

      $stock = calc_leftover_available($pdo, $companyId, $locationId, (int)$prod['id']);

      $st = $pdo->prepare("
        SELECT q.id, q.work_date, q.shift, q.disposal_time, q.qty_bars, q.qty_kg, q.reason, q.notes,
               p.sku, p.name AS product_name
        FROM quality_cubing_leftover_disposals q
        JOIN products p ON p.id = q.product_id
        WHERE q.company_id=? AND q.location_id=? AND q.work_date=? AND q.shift=?
        ORDER BY q.disposal_time DESC, q.id DESC
      ");
      $st->execute([$companyId,$locationId,$date,$shift]);
      $rows = $st->fetchAll(PDO::FETCH_ASSOC);

      $summaryBars = 0.0;
      $summaryKg = 0.0;
      foreach ($rows as $r) {
        $summaryBars += (float)$r['qty_bars'];
        $summaryKg   += (float)$r['qty_kg'];
      }

      jexit([
        'ok' => true,
        'product' => $prod,
        'stock' => $stock,
        'rows' => $rows,
        'summary' => [
          'bars' => $summaryBars,
          'kg'   => $summaryKg
        ],
        'can_write' => $canWrite
      ]);
    }

    if ($action === 'leftover_save') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);

      $d = read_json();
      $date  = (string)($d['work_date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($d['shift'] ?? 1));
      $qtyBars = (float)($d['qty_bars'] ?? 0);
      $reason  = trim((string)($d['reason'] ?? ''));
      $notes   = trim((string)($d['notes'] ?? ''));
      $time    = trim((string)($d['disposal_time'] ?? ''));
      if ($notes === '') $notes = null;
      if ($reason === '') $reason = null;
      if ($time === '') $time = $date . ' ' . date('H:i:s');
      else {
        $time = str_replace('T', ' ', $time);
        if (strlen($time) === 16) $time .= ':00';
      }

      $prod = get_leftover_product($pdo, $companyId);
      if (!$prod) jexit(['ok'=>false,'error'=>'No se encontró producto barra 150kg'], 404);

      $stock = calc_leftover_available($pdo, $companyId, $locationId, (int)$prod['id']);
      if ($stock['warehouse_id'] <= 0) {
        jexit(['ok'=>false,'error'=>'No existe warehouse SOB-CUB'], 409);
      }

      if ($qtyBars <= 0) {
        jexit(['ok'=>false,'error'=>'Cantidad inválida'], 400);
      }

      if (($qtyBars - (float)$stock['bars']) > 0.000001) {
        jexit(['ok'=>false,'error'=>'No hay sobrante suficiente disponible en SOB-CUB'], 409);
      }

      // Calidad puede disponer todo o parcial del sobrante.
      // Regla: la baja se registra como SCRAP y se descuenta físicamente de SOB-CUB.
      $qtyBars = round($qtyBars, 2);
      $qtyKg = round($qtyBars * 150.0, 2);
      $scrapConceptId = get_default_scrap_concept_id($pdo, $companyId);
      if ($scrapConceptId <= 0) {
        jexit(['ok'=>false,'error'=>'No se encontró/creó concepto SCRAP para registrar la baja de sobrante.'], 500);
      }

      $pdo->beginTransaction();
      try {
        $scrapNotes = 'SCRAP de sobrante cubicado SOB-CUB' .
          ' | Baja ' . number_format($qtyBars, 2, '.', '') . ' barras' .
          ' | ' . number_format($qtyKg, 2, '.', '') . ' kg' .
          ($reason ? (' | Motivo: ' . $reason) : '') .
          ($notes ? (' | ' . $notes) : '');

        $st = $pdo->prepare("
          INSERT INTO quality_entries
            (company_id,location_id,work_date,shift,product_id,concept_type,concept_id,qty,notes,created_by,created_at)
          VALUES
            (?,?,?,?,?,'SCRAP',?,?,?,?,NOW())
        ");
        $st->execute([$companyId, $locationId, $date, $shift, (int)$prod['id'], $scrapConceptId, $qtyBars, $scrapNotes, $uid]);
        $qualityEntryScrapId = (int)$pdo->lastInsertId();

        $st = $pdo->prepare("
          INSERT INTO inventory_moves
          (company_id, location_id, warehouse_id, work_date, product_id, qty, move_type, ref_type, notes, created_by, created_at)
          VALUES
          (?,?,?,?,?,?, 'OUT', 'QUALITY_SOB_CUB_SCRAP', ?, ?, NOW())
        ");
        $st->execute([
          $companyId, $locationId, (int)$stock['warehouse_id'], $date, (int)$prod['id'],
          $qtyBars, $scrapNotes, $uid
        ]);
        $moveId = (int)$pdo->lastInsertId();

        $st = $pdo->prepare("
          INSERT INTO quality_cubing_leftover_disposals
          (company_id, location_id, work_date, shift, disposal_time, product_id, warehouse_id, qty_bars, qty_kg, reason, notes, inventory_move_id, quality_entry_scrap_id, created_by, created_at)
          VALUES
          (?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())
        ");
        $st->execute([
          $companyId, $locationId, $date, $shift, $time, (int)$prod['id'], (int)$stock['warehouse_id'],
          $qtyBars, $qtyKg, $reason, $notes, $moveId, $qualityEntryScrapId, $uid
        ]);
        $disposalId = (int)$pdo->lastInsertId();

        create_inv_ledger_if_exists($pdo, $companyId, $locationId, (int)$prod['id'], 'SCRAP_OUT', $qtyBars, 'quality_cubing_leftover_disposals', $disposalId, $scrapNotes, $uid);

        $pdo->commit();
        jexit([
          'ok'=>true,
          'id'=>$disposalId,
          'inventory_move_id'=>$moveId,
          'quality_entry_scrap_id'=>$qualityEntryScrapId,
          'qty_bars'=>$qtyBars,
          'qty_kg'=>$qtyKg,
          'message'=>'Baja de sobrante registrada como SCRAP y descontada de SOB-CUB.'
        ]);
      } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        jexit(['ok'=>false,'error'=>$e->getMessage()], 500);
      }
    }

    if ($action === 'leftover_delete') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);

      $d = read_json();
      $id = (int)($d['id'] ?? 0);
      if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);

      $st = $pdo->prepare("
        SELECT inventory_move_id, quality_entry_scrap_id
        FROM quality_cubing_leftover_disposals
        WHERE id=? AND company_id=? AND location_id=?
        LIMIT 1
      ");
      $st->execute([$id,$companyId,$locationId]);
      $row = $st->fetch(PDO::FETCH_ASSOC);
      if (!$row) jexit(['ok'=>false,'error'=>'Registro no encontrado'], 404);

      $pdo->beginTransaction();
      try {
        if (!empty($row['inventory_move_id'])) {
          $st = $pdo->prepare("DELETE FROM inventory_moves WHERE id=? AND company_id=? AND location_id=?");
          $st->execute([(int)$row['inventory_move_id'],$companyId,$locationId]);
        }
        if (!empty($row['quality_entry_scrap_id'])) {
          $st = $pdo->prepare("DELETE FROM quality_entries WHERE id=? AND company_id=? AND location_id=? AND concept_type='SCRAP'");
          $st->execute([(int)$row['quality_entry_scrap_id'],$companyId,$locationId]);
        }
        if (table_exists($pdo, 'inv_ledger')) {
          $st = $pdo->prepare("DELETE FROM inv_ledger WHERE ref_table='quality_cubing_leftover_disposals' AND ref_id=? AND company_id=? AND location_id=?");
          $st->execute([$id, $companyId, $locationId]);
        }

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

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

    if ($action === 'monitor_points') {
      $st = $pdo->prepare("
        SELECT qmp.id, qmp.point_code, qmp.point_name, qmp.point_type, qmp.unit_label,
               qmp.min_allowed, qmp.max_allowed, qmp.ok_min, qmp.ok_max, qmp.warn_min, qmp.warn_max,
               qmp.warehouse_id, w.code AS warehouse_code, w.name AS warehouse_name,
               qmp.sort_order
        FROM quality_monitor_points qmp
        LEFT JOIN warehouses w ON w.id = qmp.warehouse_id
        WHERE qmp.company_id=? AND qmp.location_id=? AND qmp.active=1
        ORDER BY qmp.sort_order, qmp.point_name
      ");
      $st->execute([$companyId, $locationId]);
      jexit(['ok'=>true,'rows'=>$st->fetchAll(PDO::FETCH_ASSOC)]);
    }

    if ($action === 'monitor_dashboard') {
      $date  = (string)($_GET['date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($_GET['shift'] ?? 1));

      $st = $pdo->prepare("
        SELECT qmp.id, qmp.point_code, qmp.point_name, qmp.point_type, qmp.unit_label,
               qmp.min_allowed, qmp.max_allowed, qmp.ok_min, qmp.ok_max, qmp.warn_min, qmp.warn_max,
               qmp.sort_order
        FROM quality_monitor_points qmp
        WHERE qmp.company_id=? AND qmp.location_id=? AND qmp.active=1
        ORDER BY qmp.sort_order, qmp.point_name
      ");
      $st->execute([$companyId, $locationId]);
      $points = $st->fetchAll(PDO::FETCH_ASSOC);

      $slots = shift_periodic_slots($shift);
      $handoverSlot = shift_handover_slot($shift);

      $str = $pdo->prepare("
        SELECT *
        FROM quality_monitor_readings
        WHERE company_id=? AND location_id=? AND work_date=? AND shift=?
      ");
      $str->execute([$companyId, $locationId, $date, $shift]);
      $readingRows = $str->fetchAll(PDO::FETCH_ASSOC);

      $map = [];
      foreach ($readingRows as $r) {
        $key = $r['point_id'] . '|' . $r['reading_kind'] . '|' . $r['slot_label'];
        $map[$key] = $r;
      }

      $prevCtx = get_previous_shift_context($date, $shift);
      $sp = $pdo->prepare("
        SELECT r.*
        FROM quality_monitor_readings r
        INNER JOIN (
          SELECT point_id, MAX(id) AS max_id
          FROM quality_monitor_readings
          WHERE company_id=? AND location_id=? AND work_date=? AND shift=?
          GROUP BY point_id
        ) x ON x.max_id = r.id
      ");
      $sp->execute([$companyId, $locationId, $prevCtx['work_date'], $prevCtx['shift']]);
      $prevRows = $sp->fetchAll(PDO::FETCH_ASSOC);

      $prevMap = [];
      foreach ($prevRows as $r) {
        $prevMap[(int)$r['point_id']] = $r;
      }

      $summary = ['ok'=>0,'warn'=>0,'alert'=>0];

      $outPoints = [];
      foreach ($points as $p) {
        $pid = (int)$p['id'];

        $handover = $map[$pid.'|HANDOVER|'.$handoverSlot] ?? null;
        $previous = $prevMap[$pid] ?? null;

        $periodic = [];
        foreach ($slots as $slot) {
          $rr = $map[$pid.'|PERIODIC|'.$slot] ?? null;
          if ($rr) {
            if ($rr['status_code'] === 'OK') $summary['ok']++;
            if ($rr['status_code'] === 'WARN') $summary['warn']++;
            if ($rr['status_code'] === 'ALERT') $summary['alert']++;
          }
          $periodic[] = [
            'slot'   => $slot,
            'row'    => $rr
          ];
        }

        if ($handover) {
          if ($handover['status_code'] === 'OK') $summary['ok']++;
          if ($handover['status_code'] === 'WARN') $summary['warn']++;
          if ($handover['status_code'] === 'ALERT') $summary['alert']++;
        }

        $outPoints[] = [
          'point'       => $p,
          'handover'    => $handover,
          'previous'    => $previous,
          'periodic'    => $periodic
        ];
      }

      jexit([
        'ok'            => true,
        'date'          => $date,
        'shift'         => $shift,
        'shift_label'   => shift_label($shift),
        'handover_slot' => $handoverSlot,
        'slots'         => $slots,
        'points'        => $outPoints,
        'summary'       => $summary,
        'can_write'     => $canWrite
      ]);
    }

    if ($action === 'save_monitor') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);

      $d = read_json();

      $id           = (int)($d['id'] ?? 0);
      $workDate     = (string)($d['work_date'] ?? date('Y-m-d'));
      $shift        = norm_shift((int)($d['shift'] ?? 1));
      $pointId      = (int)($d['point_id'] ?? 0);
      $readingKind  = strtoupper(trim((string)($d['reading_kind'] ?? 'PERIODIC')));
      $slotLabel    = trim((string)($d['slot_label'] ?? ''));
      $capturedHhmm = normalize_hhmm((string)($d['captured_hhmm'] ?? ''));
      $readingValue = (float)($d['reading_value'] ?? 0);
      $notes        = trim((string)($d['notes'] ?? ''));
      if ($notes === '') $notes = null;

      if (!in_array($readingKind, ['HANDOVER','PERIODIC'], true)) {
        jexit(['ok'=>false,'error'=>'reading_kind inválido'], 400);
      }
      if ($pointId <= 0) {
        jexit(['ok'=>false,'error'=>'point_id requerido'], 400);
      }
      if ($slotLabel === '') {
        jexit(['ok'=>false,'error'=>'slot_label requerido'], 400);
      }
      if ($capturedHhmm === null) {
        jexit(['ok'=>false,'error'=>'Hora inválida. Usa formato HH:MM'], 400);
      }

      $stp = $pdo->prepare("
        SELECT id, point_type
        FROM quality_monitor_points
        WHERE id=? AND company_id=? AND location_id=? AND active=1
        LIMIT 1
      ");
      $stp->execute([$pointId, $companyId, $locationId]);
      $point = $stp->fetch(PDO::FETCH_ASSOC);
      if (!$point) {
        jexit(['ok'=>false,'error'=>'Punto de monitoreo inválido'], 400);
      }

      $statusCode = calc_monitor_status((string)$point['point_type'], $readingValue);
      $readingAt = combine_manual_datetime($workDate, $capturedHhmm, $shift);

      if ($id > 0) {
        $st = $pdo->prepare("
          UPDATE quality_monitor_readings
          SET work_date=?, shift=?, point_id=?, reading_kind=?, slot_label=?, captured_hhmm=?, reading_at=?,
              reading_value=?, status_code=?, notes=?, updated_by=?, updated_at=NOW()
          WHERE id=? AND company_id=? AND location_id=?
        ");
        $st->execute([
          $workDate, $shift, $pointId, $readingKind, $slotLabel, $capturedHhmm, $readingAt,
          $readingValue, $statusCode, $notes, $uid, $id, $companyId, $locationId
        ]);
        jexit(['ok'=>true,'id'=>$id,'status_code'=>$statusCode,'captured_hhmm'=>$capturedHhmm]);
      }

      $find = $pdo->prepare("
        SELECT id
        FROM quality_monitor_readings
        WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND point_id=? AND reading_kind=? AND slot_label=?
        LIMIT 1
      ");
      $find->execute([$companyId, $locationId, $workDate, $shift, $pointId, $readingKind, $slotLabel]);
      $exists = $find->fetch(PDO::FETCH_ASSOC);

      if ($exists) {
        $rid = (int)$exists['id'];
        $up = $pdo->prepare("
          UPDATE quality_monitor_readings
          SET captured_hhmm=?, reading_at=?, reading_value=?, status_code=?, notes=?, updated_by=?, updated_at=NOW()
          WHERE id=? AND company_id=? AND location_id=?
        ");
        $up->execute([$capturedHhmm, $readingAt, $readingValue, $statusCode, $notes, $uid, $rid, $companyId, $locationId]);
        jexit(['ok'=>true,'id'=>$rid,'status_code'=>$statusCode,'captured_hhmm'=>$capturedHhmm]);
      }

      $ins = $pdo->prepare("
        INSERT INTO quality_monitor_readings
        (
          company_id, location_id, work_date, shift, point_id,
          reading_kind, slot_label, captured_hhmm, reading_at, reading_value, status_code,
          notes, created_by, created_at
        )
        VALUES
        (?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())
      ");
      $ins->execute([
        $companyId, $locationId, $workDate, $shift, $pointId,
        $readingKind, $slotLabel, $capturedHhmm, $readingAt, $readingValue, $statusCode,
        $notes, $uid
      ]);

      jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId(),'status_code'=>$statusCode,'captured_hhmm'=>$capturedHhmm]);
    }

    if ($action === 'delete_monitor') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);
      $d = read_json();
      $id = (int)($d['id'] ?? 0);
      if ($id <= 0) jexit(['ok'=>false,'error'=>'id requerido'], 400);

      $st = $pdo->prepare("DELETE FROM quality_monitor_readings WHERE id=? AND company_id=? AND location_id=?");
      $st->execute([$id, $companyId, $locationId]);
      jexit(['ok'=>true]);
    }

    if ($action === 'brine_dashboard') {
      $date  = (string)($_GET['date'] ?? date('Y-m-d'));
      $shift = norm_shift((int)($_GET['shift'] ?? 1));

      $slots = shift_brine_slots($shift);
      $primarySlot = brine_primary_slot($shift);
      $legacyShiftDegree = get_shift_brine_degree($pdo, $companyId, $locationId, $date, $shift);
      $tankDegreeMap = get_tank_brine_degrees($pdo, $companyId, $locationId, $date, $shift);

      $st = $pdo->prepare("
        SELECT id, tank_code, tank_name, sort_order
        FROM quality_brine_tanks
        WHERE company_id=? AND location_id=? AND active=1
        ORDER BY sort_order, tank_code
      ");
      $st->execute([$companyId,$locationId]);
      $tanks = $st->fetchAll(PDO::FETCH_ASSOC);

      $st = $pdo->prepare("
        SELECT *
        FROM quality_brine_readings
        WHERE company_id=? AND location_id=? AND work_date=? AND shift=?
      ");
      $st->execute([$companyId,$locationId,$date,$shift]);
      $rows = $st->fetchAll(PDO::FETCH_ASSOC);

      $map = [];
      foreach ($rows as $r) {
        $tankId = (int)$r['tank_id'];
        if (isset($tankDegreeMap[$tankId])) {
          $r['brine_degree'] = $tankDegreeMap[$tankId]['degree_value'];
          $r['brine_status'] = $tankDegreeMap[$tankId]['status_code'];
        }
        $map[$tankId . '|' . $r['slot_label']] = $r;
      }

      $summary = ['ok'=>0,'warn'=>0,'alert'=>0];
      foreach ($rows as $r) {
        $code = (string)$r['temp_status'];
        if ($code === 'OK') $summary['ok']++;
        if ($code === 'WARN') $summary['warn']++;
        if ($code === 'ALERT') $summary['alert']++;
      }
      foreach ($tankDegreeMap as $degRow) {
        $code = (string)$degRow['status_code'];
        if ($code === 'OK') $summary['ok']++;
        if ($code === 'WARN') $summary['warn']++;
        if ($code === 'ALERT') $summary['alert']++;
      }

      jexit([
        'ok' => true,
        'date' => $date,
        'shift' => $shift,
        'slots' => $slots,
        'primary_slot' => $primarySlot,
        'shift_degree' => $legacyShiftDegree,
        'tank_degrees' => $tankDegreeMap,
        'tanks' => $tanks,
        'map' => $map,
        'summary' => $summary,
        'can_write' => $canWrite
      ]);
    }

    if ($action === 'save_brine_degree') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);

      $d = read_json();
      $workDate = (string)($d['work_date'] ?? date('Y-m-d'));
      $shift    = norm_shift((int)($d['shift'] ?? 1));
      $tankId   = (int)($d['tank_id'] ?? 0);
      $degree   = isset($d['degree_value']) ? (float)$d['degree_value'] : null;
      $hhmm     = normalize_hhmm((string)($d['captured_hhmm'] ?? ''));
      $notes    = trim((string)($d['notes'] ?? ''));
      if ($notes === '') $notes = null;

      if ($tankId <= 0) jexit(['ok'=>false,'error'=>'tank_id requerido'], 400);
      if ($degree === null) jexit(['ok'=>false,'error'=>'degree_value requerido'], 400);
      if ($hhmm === null) jexit(['ok'=>false,'error'=>'captured_hhmm inválido'], 400);

      $stTank = $pdo->prepare("
        SELECT id, tank_code, tank_name
        FROM quality_brine_tanks
        WHERE id=? AND company_id=? AND location_id=? AND active=1
        LIMIT 1
      ");
      $stTank->execute([$tankId, $companyId, $locationId]);
      $tank = $stTank->fetch(PDO::FETCH_ASSOC);
      if (!$tank) {
        jexit(['ok'=>false,'error'=>'Tanque inválido'], 400);
      }

      $capturedAt = combine_manual_datetime($workDate, $hhmm, $shift);
      $statusCode = calc_brine_status_deg($degree);

      $find = $pdo->prepare("
        SELECT id
        FROM quality_brine_tank_degree
        WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND tank_id=?
        LIMIT 1
      ");
      $find->execute([$companyId,$locationId,$workDate,$shift,$tankId]);
      $exists = $find->fetch(PDO::FETCH_ASSOC);

      if ($exists) {
        $rid = (int)$exists['id'];
        $up = $pdo->prepare("
          UPDATE quality_brine_tank_degree
          SET degree_value=?, status_code=?, captured_at=?, notes=?, updated_by=?, updated_at=NOW()
          WHERE id=? AND company_id=? AND location_id=?
        ");
        $up->execute([$degree,$statusCode,$capturedAt,$notes,$uid,$rid,$companyId,$locationId]);
      } else {
        $ins = $pdo->prepare("
          INSERT INTO quality_brine_tank_degree
          (company_id, location_id, work_date, shift, tank_id, degree_value, status_code, captured_at, notes, created_by, created_at)
          VALUES
          (?,?,?,?,?,?,?,?,?,?,NOW())
        ");
        $ins->execute([$companyId,$locationId,$workDate,$shift,$tankId,$degree,$statusCode,$capturedAt,$notes,$uid]);
        $rid = (int)$pdo->lastInsertId();
      }

      jexit([
        'ok'=>true,
        'id'=>$rid,
        'tank_id'=>$tankId,
        'degree_value'=>$degree,
        'status_code'=>$statusCode,
        'captured_at'=>$capturedAt,
        'captured_hhmm'=>$hhmm
      ]);
    }

    if ($action === 'save_brine') {
      if (!$canWrite) jexit(['ok'=>false,'error'=>'Sin permiso (quality/admin)'], 403);

      $d = read_json();
      $id       = (int)($d['id'] ?? 0);
      $workDate = (string)($d['work_date'] ?? date('Y-m-d'));
      $shift    = norm_shift((int)($d['shift'] ?? 1));
      $tankId   = (int)($d['tank_id'] ?? 0);
      $slot     = trim((string)($d['slot_label'] ?? ''));
      $temp     = (float)($d['temp_c'] ?? 0);
      $hhmm     = normalize_hhmm((string)($d['captured_hhmm'] ?? ''));
      $notes    = trim((string)($d['notes'] ?? ''));
      if ($notes === '') $notes = null;

      if ($tankId <= 0 || $slot === '') {
        jexit(['ok'=>false,'error'=>'tank_id y slot_label son requeridos'], 400);
      }
      if ($hhmm === null) {
        jexit(['ok'=>false,'error'=>'Hora inválida. Usa formato HH:MM'], 400);
      }

      $allowedSlots = shift_brine_slots($shift);
      if (!in_array($slot, $allowedSlots, true)) {
        jexit(['ok'=>false,'error'=>'slot_label inválido para el turno'], 400);
      }

      $degreeRow = get_tank_brine_degree($pdo, $companyId, $locationId, $workDate, $shift, $tankId);
      if (!$degreeRow) {
        jexit(['ok'=>false,'error'=>'Primero captura el grado de salmuera del tanque'], 400);
      }

      $deg = (float)$degreeRow['degree_value'];
      $degStatus = calc_brine_status_deg($deg);
      $tmpStatus = calc_brine_status_temp($temp);
      $readingAt = combine_manual_datetime($workDate, $hhmm, $shift);

      $find = $pdo->prepare("
        SELECT id
        FROM quality_brine_readings
        WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND tank_id=? AND slot_label=?
        LIMIT 1
      ");
      $find->execute([$companyId,$locationId,$workDate,$shift,$tankId,$slot]);
      $exists = $find->fetch(PDO::FETCH_ASSOC);

      if ($id > 0 || $exists) {
        $rid = $id > 0 ? $id : (int)$exists['id'];
        $st = $pdo->prepare("
          UPDATE quality_brine_readings
          SET reading_at=?, captured_hhmm=?, brine_degree=?, temp_c=?, brine_status=?, temp_status=?, notes=?, updated_by=?, updated_at=NOW()
          WHERE id=? AND company_id=? AND location_id=?
        ");
        $st->execute([$readingAt,$hhmm,$deg,$temp,$degStatus,$tmpStatus,$notes,$uid,$rid,$companyId,$locationId]);

        jexit(['ok'=>true,'id'=>$rid,'brine_degree'=>$deg,'captured_hhmm'=>$hhmm]);
      }

      $st = $pdo->prepare("
        INSERT INTO quality_brine_readings
        (company_id, location_id, work_date, shift, tank_id, slot_label, captured_hhmm, reading_at, brine_degree, temp_c, brine_status, temp_status, notes, created_by, created_at)
        VALUES
        (?,?,?,?,?,?,?,?,?,?,?,?,?,?,NOW())
      ");
      $st->execute([$companyId,$locationId,$workDate,$shift,$tankId,$slot,$hhmm,$readingAt,$deg,$temp,$degStatus,$tmpStatus,$notes,$uid]);

      jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId(),'brine_degree'=>$deg,'captured_hhmm'=>$hhmm]);
    }

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

  } catch (Throwable $e) {
    jexit([
      'ok'      => false,
      'error'   => 'SERVER_ERROR',
      'message' => $e->getMessage()
    ], 500);
  }
}

$csrf = csrf_token();
?>
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>BReport · Calidad</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{
      --bg-0:#f4f7fb;
      --bg-1:#ffffff;
      --bg-2:#eef4fb;
      --card:#ffffff;
      --card-2:#f8fbff;
      --line:rgba(15,35,61,.10);
      --line-soft:rgba(15,35,61,.06);
      --text:#10233d;
      --muted:#64748b;
      --accent:#165dff;
      --accent-2:#0ea5e9;
      --success:#16a34a;
      --warn:#d97706;
      --danger:#dc2626;
      --shadow:0 14px 34px rgba(15,35,61,.08);
      --radius:22px;
    }

    *{box-sizing:border-box}
    html,body{min-height:100%}
    body{
      margin:0;
      color:var(--text);
      background:
        radial-gradient(circle at top right, rgba(22,93,255,.08), transparent 24%),
        linear-gradient(180deg, #f9fbff 0%, var(--bg-0) 100%);
      font-family: Inter, system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
    }

    .mono{
      font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
    }

    .navbar{
      background:rgba(255,255,255,.96) !important;
      border-bottom:1px solid rgba(15,35,61,.08)!important;
      box-shadow:0 8px 22px rgba(15,35,61,.06);
      backdrop-filter: blur(10px);
    }
    .navbar-brand{color:var(--text)!important;font-weight:800}
    .navbar .btn{border-radius:12px}

    .stickyTop{
      position:sticky;
      top:0;
      z-index:20;
      background:linear-gradient(180deg, rgba(244,247,251,.96) 70%, rgba(244,247,251,0));
      padding-top:10px;
      backdrop-filter: blur(6px);
    }

    .cardSoft{
      background:linear-gradient(180deg, var(--card), var(--card-2));
      border:1px solid var(--line-soft);
      border-radius:var(--radius);
      box-shadow:var(--shadow);
      overflow:hidden;
      animation:fadeUp .35s ease;
    }

    .cardSoft .card-body{
      padding:18px 18px 16px;
    }

    .kpiBox{
      background:linear-gradient(180deg, #ffffff, #f7fbff);
      border:1px solid rgba(15,35,61,.08);
      border-radius:18px;
      padding:14px 16px;
      box-shadow: inset 0 1px 0 rgba(255,255,255,.70);
      transition:transform .18s ease, border-color .18s ease, box-shadow .18s ease;
    }
    .kpiBox:hover{
      transform:translateY(-2px);
      border-color:rgba(22,93,255,.28);
      box-shadow:0 12px 26px rgba(15,35,61,.10);
    }

    .muted,.text-muted{color:var(--muted)!important}
    .readonlyNote{
      background:rgba(217,119,6,.08);
      border:1px solid rgba(217,119,6,.18);
      border-radius:14px;
      padding:10px 12px;
      color:#8a4b00;
    }

    .pill{border-radius:999px!important}
    .btn{transition:all .18s ease}
    .btn:hover{transform:translateY(-1px)}

    .btn-outline-primary{
      color:#dcebff;
      border-color:rgba(94,145,255,.55);
      background:rgba(25,79,187,.10);
    }
    .btn-outline-primary:hover{
      color:#fff;
      border-color:#4d8fff;
      background:#165dff;
    }

    .btn-outline-secondary{
      color:#d9e4f5;
      border-color:rgba(255,255,255,.18);
      background:rgba(255,255,255,.03);
    }
    .btn-outline-secondary:hover{
      background:rgba(255,255,255,.08);
      color:#fff;
      border-color:rgba(255,255,255,.28);
    }

    .btn-outline-danger{
      color:#ffcad2;
      border-color:rgba(255,93,115,.55);
      background:rgba(255,93,115,.08);
    }
    .btn-outline-danger:hover{
      color:#fff;
      background:rgba(255,93,115,.22);
      border-color:#ff5d73;
    }

    .btn-success{
      background:linear-gradient(180deg, #1677ff, #0b5fe0);
      border-color:#0b5fe0;
      box-shadow:0 8px 18px rgba(21,94,255,.20);
    }
    .btn-success:hover{
      background:linear-gradient(180deg, #2b84ff, #156cff);
      border-color:#156cff;
    }

    .btn-group .btn,
    .btn-outline-dark{
      border-color:rgba(255,255,255,.12)!important;
      background:rgba(255,255,255,.03)!important;
      color:#dbe7fa!important;
    }
    .btn-group .btn:hover,
    .btn-outline-dark:hover{
      background:rgba(255,255,255,.08)!important;
      color:#fff!important;
      border-color:rgba(255,255,255,.18)!important;
    }
    .btn-group .btn.active{
      background:linear-gradient(180deg, rgba(32,91,255,.95), rgba(11,95,224,.95))!important;
      color:#fff!important;
      border-color:rgba(92,142,255,.95)!important;
      box-shadow:0 8px 18px rgba(21,94,255,.22);
    }

    .card,
    .card *{
      color:var(--text);
    }
    .card .text-muted,
    .card .small.text-muted,
    .card .form-text{
      color:#9fb2cf!important;
    }
    .card input,
    .card select,
    .card textarea{
      color:#fff!important;
    }
    .form-control:disabled,
    .form-select:disabled{
      color:#dce8fb!important;
      -webkit-text-fill-color:#dce8fb;
      opacity:1;
    }

    .form-label{
      color:#d9e6fb;
      font-weight:600;
    }
    .form-control,
    .form-select{
      background:rgba(255,255,255,.04);
      color:#fff;
      border:1px solid rgba(255,255,255,.11);
      border-radius:14px;
      min-height:44px;
    }
    .form-control:focus,
    .form-select:focus{
      background:rgba(255,255,255,.06);
      color:#fff;
      border-color:rgba(83,136,255,.75);
      box-shadow:0 0 0 .2rem rgba(31,111,255,.16);
    }
    .form-control::placeholder{color:#95a8c5}
    .form-select option{
      color:#fff;
      background:#0b1f3f;
    }

    .table-responsive{
      border-radius:18px;
      overflow:hidden;
    }

    .table{
      --bs-table-bg: transparent;
      --bs-table-color: var(--text);
      --bs-table-border-color: rgba(255,255,255,.08);
      margin-bottom:0;
    }

    .table thead th{
      background:linear-gradient(180deg, rgba(10,27,52,.98), rgba(7,22,44,.98));
      color:#dfe9fb;
      font-weight:800;
      border-bottom:1px solid rgba(255,255,255,.10);
      padding:.9rem .85rem;
      white-space:nowrap;
    }

    .table tbody td{
      background:rgba(255,255,255,.02);
      color:#f5f8ff;
      padding:.85rem .85rem;
      border-bottom:1px solid rgba(255,255,255,.06);
    }

    .table tbody tr:hover td{
      background:rgba(255,255,255,.05);
    }

    .badge{
      border-radius:999px;
      padding:.48rem .7rem;
      font-weight:700;
      letter-spacing:.02em;
      color:#eef4ff!important;
    }

    .text-bg-warning{
      background:rgba(255,176,32,.18)!important;
      color:#ffd782!important;
      border:1px solid rgba(255,176,32,.34);
    }
    .text-bg-secondary{
      background:rgba(255,255,255,.10)!important;
      color:#eef3ff!important;
      border:1px solid rgba(255,255,255,.14);
    }
    .text-bg-info{
      background:rgba(0,194,255,.14)!important;
      color:#8fe5ff!important;
      border:1px solid rgba(0,194,255,.24);
    }
    .text-bg-success{
      background:rgba(22,199,132,.16)!important;
      color:#8ef0c3!important;
      border:1px solid rgba(22,199,132,.28);
    }
    .text-bg-danger{
      background:rgba(255,93,115,.16)!important;
      color:#ffb1bc!important;
      border:1px solid rgba(255,93,115,.28);
    }

    .status-ok{background:rgba(22,199,132,.10)!important}
    .status-warn{background:rgba(255,176,32,.10)!important}
    .status-alert{background:rgba(255,93,115,.10)!important}

    .modal-content{
      background:#ffffff;
      color:var(--text);
      border:1px solid rgba(15,35,61,.10);
      border-radius:22px;
      box-shadow:0 18px 40px rgba(15,35,61,.16);
    }
    .modal-header,.modal-footer{border-color:rgba(15,35,61,.08)}
    .btn-close{filter:none}
    .modal-title,.modal-content .form-label,.modal-content .small,.modal-content .text-muted,.modal-content .muted{color:var(--text)!important}

    #monitorSection .table th,
    #monitorSection .table td,
    #brineSection .table th,
    #brineSection .table td{
      text-align:center;
    }
    #monitorSection .table td:first-child,
    #monitorSection .table th:first-child,
    #brineSection .table td:first-child,
    #brineSection .table th:first-child{
      text-align:left;
      min-width:220px;
    }

    .monitorCell{min-width:120px}
    .sectionHidden{display:none!important}

    @keyframes fadeUp{
      from{opacity:0;transform:translateY(8px)}
      to{opacity:1;transform:translateY(0)}
    }

    @media (max-width: 991.98px){
      .cardSoft .card-body{padding:16px}
      .table thead th,.table tbody td{padding:.7rem .65rem}
    }
  
    .btn-outline-primary{
      color:var(--accent);
      border-color:rgba(22,93,255,.28);
      background:rgba(22,93,255,.06);
    }
    .btn-outline-primary:hover{ color:#fff; border-color:var(--accent); background:var(--accent); }
    .btn-outline-secondary{
      color:#475569;
      border-color:rgba(15,35,61,.12);
      background:#fff;
    }
    .btn-outline-secondary:hover{
      background:#f8fafc;
      color:var(--text);
      border-color:rgba(15,35,61,.18);
    }
    .btn-outline-danger{
      color:var(--danger);
      border-color:rgba(220,38,38,.22);
      background:rgba(220,38,38,.04);
    }
    .btn-outline-danger:hover{ color:#fff; background:var(--danger); border-color:var(--danger); }
    .btn-outline-dark{
      color:var(--text);
      border-color:rgba(15,35,61,.10);
      background:#fff;
    }
    .btn-outline-dark:hover,
    .btn-outline-dark.active{
      color:#fff;
      background:var(--accent);
      border-color:var(--accent);
    }
    .btn-success{
      background:linear-gradient(135deg, #16a34a, #22c55e);
      border-color:#16a34a;
    }
    .heroPanel{
      background:linear-gradient(135deg,#102743 0%,#173c62 55%,#2360a1 100%);
      color:#fff;
      border:1px solid rgba(255,255,255,.08);
      border-radius:24px;
      box-shadow:var(--shadow);
      overflow:hidden;
    }
    .heroPanel .muted,
    .heroPanel .text-muted{ color:rgba(255,255,255,.78)!important; }
    .heroPanel .form-control,
    .heroPanel .form-select{
      background:rgba(255,255,255,.95);
      color:var(--text);
      border-color:rgba(255,255,255,.18);
    }
    .table{
      --bs-table-bg: transparent;
      --bs-table-color: var(--text);
      --bs-table-border-color: rgba(15,35,61,.08);
      --bs-table-hover-bg: rgba(22,93,255,.04);
    }
    .table thead th{
      background:#f6f9fc;
      color:var(--text);
      border-bottom:1px solid rgba(15,35,61,.10);
      white-space:nowrap;
    }
    .form-control, .form-select{
      background:#fff;
      color:var(--text);
      border:1px solid rgba(15,35,61,.12);
    }
    .form-control:focus, .form-select:focus{
      border-color:rgba(22,93,255,.35);
      box-shadow:0 0 0 .2rem rgba(22,93,255,.12);
    }
    .modal-content{
      background:#fff;
      color:var(--text);
      border:1px solid rgba(15,35,61,.10);
      box-shadow:0 24px 60px rgba(15,35,61,.16);
      border-radius:20px;
    }
    .modal-header,.modal-footer{ border-color:rgba(15,35,61,.08); }
    .btn-close{ filter:none; }
    .quality-row-click{ cursor:pointer; }
    .quality-row-click:hover td{ background:rgba(22,93,255,.04)!important; }


    /* ===== Ajuste final de legibilidad: fondo blanco + textos oscuros ===== */
    body, .container-fluid, .container{ color:var(--text); }
    .cardSoft,
    .cardSoft .card-body,
    .card,
    .card *:not(.heroPanel *),
    .table,
    .table *:not(.badge):not(.btn):not(.heroPanel *),
    .modal-content,
    .modal-content *:not(.btn):not(.badge){
      color:var(--text);
    }

    .card .text-muted,
    .card .small.text-muted,
    .card .muted,
    .table .text-muted,
    .table .muted,
    .modal-content .text-muted,
    .modal-content .muted{
      color:var(--muted)!important;
    }

    .card input,
    .card select,
    .card textarea,
    .modal-content input,
    .modal-content select,
    .modal-content textarea{
      color:var(--text)!important;
      -webkit-text-fill-color:var(--text);
    }

    .form-label{ color:var(--text); }
    .form-control::placeholder{ color:#94a3b8; }
    .form-select option{ color:var(--text); background:#fff; }

    .table thead th{
      background:#f3f6fb;
      color:var(--text);
      border-bottom:1px solid rgba(15,35,61,.10);
    }
    .table tbody td{
      background:#fff;
      color:var(--text);
      border-bottom:1px solid rgba(15,35,61,.08);
    }
    .table tbody tr:hover td{
      background:rgba(22,93,255,.04);
    }

    .kpiBox,
    #prodBox,
    #leftoverBox,
    #brineShiftDegreeBox{
      background:linear-gradient(180deg, #ffffff 0%, #f8fbff 100%);
      color:var(--text);
      border:1px solid rgba(15,35,61,.08);
    }

    .heroPanel{ color:#fff; }
    .heroPanel .fw-bold,
    .heroPanel .fw-semibold,
    .heroPanel .form-label,
    .heroPanel .btn-outline-dark.active,
    .heroPanel .btn-outline-dark:hover,
    .heroPanel .badge,
    .heroPanel .btn-success{
      color:#fff!important;
    }
    .heroPanel .btn-outline-dark{
      color:#e8f1ff!important;
      background:rgba(255,255,255,.06)!important;
      border-color:rgba(255,255,255,.18)!important;
    }
    .heroPanel .btn-outline-dark:not(.active):hover{
      background:rgba(255,255,255,.12)!important;
      border-color:rgba(255,255,255,.24)!important;
    }
    .heroPanel .text-muted,
    .heroPanel .muted,
    .heroPanel .small.text-muted{ color:rgba(255,255,255,.82)!important; }

    .badge.text-bg-warning{ color:#9a6700!important; background:#fff3d6!important; border-color:#f3cc74!important; }
    .badge.text-bg-secondary{ color:#475569!important; background:#eef2f7!important; border-color:#d7dfeb!important; }
    .badge.text-bg-info{ color:#0c4a6e!important; background:#dff4ff!important; border-color:#a9def8!important; }
    .badge.text-bg-success{ color:#166534!important; background:#dcfce7!important; border-color:#86efac!important; }
    .badge.text-bg-danger{ color:#991b1b!important; background:#fee2e2!important; border-color:#fecaca!important; }
    .softBadge{
      display:inline-flex;
      align-items:center;
      gap:6px;
      padding:6px 10px;
      border-radius:999px;
      background:#eef5ff;
      border:1px solid rgba(22,93,255,.10);
      color:#2b5fb8;
      font-size:.78rem;
      font-weight:700;
    }
    .detailCard{
      background:linear-gradient(180deg,#ffffff,#f8fbff);
      border:1px solid rgba(15,35,61,.08);
      border-radius:18px;
      padding:14px;
    }

  
    .cardSoft, .cardSoft .card-body, .kpiBox, .table, .table td, .table th, .table-responsive, .modal-content, .modal-body, .modal-footer, .modal-header{
      color:var(--text) !important;
    }
    .cardSoft .fw-bold, .cardSoft .fw-semibold, .kpiBox .fw-bold, .kpiBox .fw-semibold, .modal-content .fw-bold, .modal-content .fw-semibold{
      color:var(--text) !important;
    }
    .cardSoft .mono, .kpiBox .mono, .table .mono{
      color:var(--text) !important;
    }
    .table tbody tr td{
      background:transparent;
      color:var(--text) !important;
    }
    .table thead th{
      color:var(--text) !important;
    }
    .badge.text-bg-warning{ color:#7a4b00 !important; background:#fff3d6 !important; }
    .badge.text-bg-secondary{ color:#334155 !important; background:#e9eef5 !important; }
    .badge.text-bg-info{ color:#0b5d75 !important; background:#dff3fb !important; }
    .btn-outline-warning{ color:#9a6700; background:#fff; border-color:rgba(217,119,6,.35); }
    .btn-outline-warning:hover{ color:#fff; background:#d97706; border-color:#d97706; }

  
/* ============================================================
   AJUSTE FINAL DE LEGIBILIDAD · TEMA CLARO
   Evita texto blanco sobre fondo blanco y texto oscuro en banner.
============================================================ */
:root{
  --br-bg:#f5f8fc;
  --br-panel:#ffffff;
  --br-border:#d8e2ee;
  --br-border-soft:#edf2f7;
  --br-text:#0f172a;
  --br-text-2:#1f2937;
  --br-muted:#475569;
  --br-blue:#12395f;
  --br-blue-2:#1f5f99;
  --br-green:#138a4d;
  --br-yellow:#b7791f;
}

html, body{
  background:var(--br-bg)!important;
  color:var(--br-text)!important;
}

.navbar{
  background:#ffffff!important;
  border-bottom:1px solid var(--br-border)!important;
  box-shadow:0 4px 18px rgba(15,23,42,.06)!important;
}
.navbar-brand,
.navbar .small,
.navbar .text-muted{
  color:var(--br-text)!important;
}

.heroPanel{
  background:linear-gradient(135deg,var(--br-blue),var(--br-blue-2))!important;
  color:#ffffff!important;
  border:0!important;
  box-shadow:0 14px 38px rgba(18,57,95,.22)!important;
}
.heroPanel .card-body,
.heroPanel .fw-bold,
.heroPanel .fs-4,
.heroPanel label,
.heroPanel .form-label{
  color:#ffffff!important;
}
.heroPanel .muted,
.heroPanel .text-muted{
  color:rgba(255,255,255,.82)!important;
}
.heroPanel .form-control,
.heroPanel .form-select{
  background:#ffffff!important;
  color:#0f172a!important;
  border:1px solid rgba(255,255,255,.65)!important;
}
.heroPanel .form-control::placeholder{
  color:#64748b!important;
}

.cardSoft:not(.heroPanel),
.kpiBox,
.modal-content,
.dropdown-menu,
.offcanvas,
.alert{
  background:var(--br-panel)!important;
  color:var(--br-text)!important;
  border:1px solid var(--br-border)!important;
  box-shadow:0 10px 26px rgba(15,23,42,.07)!important;
}

.cardSoft:not(.heroPanel) .card-body,
.cardSoft:not(.heroPanel) .fw-bold,
.cardSoft:not(.heroPanel) .fw-semibold,
.cardSoft:not(.heroPanel) label,
.cardSoft:not(.heroPanel) .form-label,
.kpiBox,
.kpiBox *,
.modal-content,
.modal-content *{
  color:var(--br-text)!important;
}

.muted,
.text-muted,
.small.muted,
.cardSoft:not(.heroPanel) .muted,
.cardSoft:not(.heroPanel) .text-muted,
.modal-content .muted,
.modal-content .text-muted{
  color:var(--br-muted)!important;
}

.table,
.table th,
.table td,
.table thead,
.table tbody{
  color:var(--br-text)!important;
  background:#ffffff!important;
}
.table thead th{
  background:#f0f5fb!important;
  color:#0b1f36!important;
  border-bottom:1px solid var(--br-border)!important;
  font-weight:800!important;
}
.table tbody tr{
  border-bottom:1px solid var(--br-border-soft)!important;
}
.table-hover tbody tr:hover td{
  background:#f8fbff!important;
  color:var(--br-text)!important;
}

.table .mono,
.mono{
  color:#0f172a!important;
}
td .small,
td .muted{
  color:#475569!important;
}

.form-control,
.form-select{
  background:#ffffff!important;
  color:#0f172a!important;
  border:1px solid #cbd5e1!important;
}
.form-control:focus,
.form-select:focus{
  color:#0f172a!important;
  background:#ffffff!important;
  border-color:#2563eb!important;
  box-shadow:0 0 0 .2rem rgba(37,99,235,.14)!important;
}

.btn-outline-dark{
  color:#0f172a!important;
  border-color:#cbd5e1!important;
  background:#ffffff!important;
}
.btn-outline-dark:hover,
.btn-outline-dark.active,
.btn-outline-dark:active{
  color:#ffffff!important;
  background:#2563eb!important;
  border-color:#2563eb!important;
}
.btn-outline-primary{
  color:#0d47a1!important;
  border-color:#93c5fd!important;
  background:#ffffff!important;
}
.btn-outline-primary:hover{
  color:#ffffff!important;
  background:#2563eb!important;
  border-color:#2563eb!important;
}
.btn-outline-warning{
  color:#92400e!important;
  background:#ffffff!important;
  border-color:#f59e0b!important;
}
.btn-outline-warning:hover{
  color:#111827!important;
  background:#fbbf24!important;
}
.btn-success{
  background:#16a34a!important;
  border-color:#16a34a!important;
  color:#ffffff!important;
}

.badge,
.badge *{
  color:inherit!important;
}
.text-bg-warning{
  color:#78350f!important;
  background:#fef3c7!important;
  border:1px solid #fde68a!important;
}
.text-bg-secondary{
  color:#334155!important;
  background:#e2e8f0!important;
}
.text-bg-info{
  color:#075985!important;
  background:#e0f2fe!important;
}
.text-bg-success{
  color:#064e3b!important;
  background:#d1fae5!important;
}
.text-bg-danger{
  color:#7f1d1d!important;
  background:#fee2e2!important;
}

.merma-action-note{
  background:#f8fafc!important;
  border:1px solid #dbe6f2!important;
  border-radius:14px!important;
  padding:12px!important;
  color:#334155!important;
}

.disposal-split-box{
  border:1px solid #dbe6f2;
  background:#f8fbff;
  border-radius:16px;
  padding:14px;
}
.disposal-total-ok{
  color:#047857!important;
  font-weight:800;
}
.disposal-total-bad{
  color:#b91c1c!important;
  font-weight:800;
}
.dispose-summary-card{
  border:1px solid #dbe6f2;
  background:linear-gradient(180deg,#ffffff,#f8fbff);
  border-radius:18px;
  padding:16px;
  box-shadow:0 12px 28px rgba(15,23,42,.08);
}
.dispose-summary-line{
  display:flex;
  justify-content:space-between;
  gap:12px;
  padding:8px 0;
  border-bottom:1px dashed #dbe6f2;
}
.dispose-summary-line:last-child{border-bottom:0;}
.dispose-summary-label{color:#64748b;font-weight:700;}
.dispose-summary-value{color:#0f172a;font-weight:800;text-align:right;}
.modal-content{
  border:1px solid #dbe6f2!important;
  border-radius:22px!important;
  box-shadow:0 24px 70px rgba(15,23,42,.22)!important;
}
.modal-header{
  background:linear-gradient(135deg,#f8fbff,#eef6ff)!important;
  border-bottom:1px solid #dbe6f2!important;
}
.modal-footer{
  background:#f8fafc!important;
  border-top:1px solid #dbe6f2!important;
}
.readonly-soft{
  background:#f1f5f9!important;
  color:#0f172a!important;
  font-weight:800!important;
}



/* ============================================================
   REDISEÑO FINAL CALIDAD · Tema claro consistente BReport
   ============================================================ */
:root{--bg-0:#f4f7fb!important;--bg-1:#ffffff!important;--bg-2:#eef4fb!important;--card:#ffffff!important;--card-2:#fbfdff!important;--line:#dbe6f2!important;--line-soft:#e8eef7!important;--text:#0f172a!important;--muted:#64748b!important;--accent:#2563eb!important;--accent-2:#0ea5e9!important;--success:#16a34a!important;--warn:#d97706!important;--danger:#dc2626!important;--shadow:0 18px 42px rgba(15,23,42,.08)!important;--radius:22px!important;}
html,body{background:radial-gradient(circle at 8% 8%,rgba(37,99,235,.08),transparent 24%),radial-gradient(circle at 92% 80%,rgba(14,165,233,.10),transparent 25%),linear-gradient(135deg,#f8fafc 0%,#eef4fb 100%)!important;color:var(--text)!important;font-family:Inter,system-ui,-apple-system,Segoe UI,Roboto,Arial,sans-serif!important;}
.container{max-width:1460px!important;}.navbar{background:rgba(255,255,255,.94)!important;border-bottom:1px solid var(--line)!important;box-shadow:0 10px 28px rgba(15,23,42,.07)!important;backdrop-filter:blur(12px)!important;}.navbar-brand{color:#0f172a!important;font-weight:850!important;letter-spacing:-.02em!important;}.navbar .btn{border-radius:14px!important;font-weight:700!important;}
.stickyTop{position:sticky!important;top:0!important;z-index:20!important;background:linear-gradient(180deg,rgba(244,247,251,.96) 72%,rgba(244,247,251,0))!important;padding-top:12px!important;padding-bottom:10px!important;backdrop-filter:blur(8px)!important;}
.cardSoft,.cardSoft:not(.heroPanel),.card{background:#fff!important;border:1px solid var(--line)!important;border-radius:24px!important;box-shadow:var(--shadow)!important;color:var(--text)!important;overflow:hidden!important;}.cardSoft .card-body{padding:20px!important;}
.heroPanel{background:linear-gradient(180deg,#ffffff 0%,#f8fbff 100%)!important;color:var(--text)!important;border:1px solid #dbe6f2!important;border-radius:26px!important;box-shadow:0 18px 45px rgba(15,23,42,.10)!important;position:relative!important;overflow:hidden!important;}.heroPanel::before{content:'';position:absolute;inset:0 auto 0 0;width:8px;background:linear-gradient(180deg,var(--accent),var(--accent-2));}.heroPanel::after{content:'';position:absolute;width:260px;height:260px;right:-90px;top:-130px;border-radius:999px;background:radial-gradient(circle,rgba(37,99,235,.14),rgba(14,165,233,.06) 55%,transparent 70%);pointer-events:none;}.heroPanel .card-body{position:relative;z-index:1;padding:24px 24px 22px!important;}.heroPanel .fw-bold.fs-4{color:#0f172a!important;font-size:1.72rem!important;letter-spacing:-.03em!important;display:flex;align-items:center;gap:10px;}.heroPanel .fw-bold.fs-4::before{content:'✓';width:38px;height:38px;border-radius:14px;display:inline-flex;align-items:center;justify-content:center;color:#fff;font-weight:900;font-size:1.05rem;background:linear-gradient(135deg,var(--accent),var(--accent-2));box-shadow:0 10px 22px rgba(37,99,235,.22);}
.heroPanel .muted,.heroPanel .text-muted,.heroPanel .small.text-muted,.heroPanel .form-label.small.muted{color:#64748b!important;-webkit-text-fill-color:#64748b!important;}.heroPanel label,.heroPanel .form-label,.cardSoft label,.cardSoft .form-label,.modal-content label,.modal-content .form-label{color:#334155!important;font-weight:800!important;}
.form-control,.form-select,.heroPanel .form-control,.heroPanel .form-select,.card input,.card select,.card textarea,.modal-content input,.modal-content select,.modal-content textarea{background:#fff!important;color:#0f172a!important;-webkit-text-fill-color:#0f172a!important;border:1px solid #cbd5e1!important;border-radius:16px!important;min-height:48px!important;box-shadow:0 1px 0 rgba(15,23,42,.02)!important;}.form-control:focus,.form-select:focus{border-color:#60a5fa!important;box-shadow:0 0 0 .22rem rgba(37,99,235,.13)!important;}.form-control:disabled,.form-select:disabled,.readonly-soft{background:#f1f5f9!important;color:#0f172a!important;-webkit-text-fill-color:#0f172a!important;opacity:1!important;}.form-select option{background:#fff!important;color:#0f172a!important;}
.btn{border-radius:15px!important;font-weight:800!important;box-shadow:none!important;}.btn:hover{transform:translateY(-1px)!important;}.btn-outline-primary,.heroPanel .btn-outline-primary{background:#eff6ff!important;color:#1d4ed8!important;border:1px solid #bfdbfe!important;}.btn-outline-primary:hover,.heroPanel .btn-outline-primary:hover{background:linear-gradient(135deg,#2563eb,#0ea5e9)!important;color:#fff!important;border-color:#2563eb!important;}.btn-success,.heroPanel .btn-success{background:linear-gradient(135deg,#16a34a,#22c55e)!important;color:#fff!important;border:0!important;box-shadow:0 12px 24px rgba(22,163,74,.20)!important;}.btn-outline-secondary{background:#fff!important;color:#334155!important;border:1px solid #cbd5e1!important;}.btn-outline-secondary:hover{background:#f8fafc!important;color:#0f172a!important;border-color:#94a3b8!important;}.btn-outline-danger{background:#fff!important;color:#dc2626!important;border:1px solid #fecaca!important;}.btn-outline-danger:hover{background:#dc2626!important;color:#fff!important;border-color:#dc2626!important;}
.btn-group{gap:8px!important;flex-wrap:wrap!important;}.btn-group .btn,.btn-outline-dark,.heroPanel .btn-outline-dark{background:#fff!important;color:#334155!important;border:1px solid #cbd5e1!important;border-radius:999px!important;font-weight:800!important;}.btn-group .btn:hover,.btn-outline-dark:hover,.heroPanel .btn-outline-dark:hover{background:#eff6ff!important;color:#1d4ed8!important;border-color:#bfdbfe!important;}.btn-group .btn.active,.btn-outline-dark.active,.heroPanel .btn-outline-dark.active{background:linear-gradient(135deg,#2563eb,#0ea5e9)!important;color:#fff!important;border-color:transparent!important;box-shadow:0 12px 24px rgba(37,99,235,.22)!important;}.btn-group .btn i,.btn-outline-dark i{color:inherit!important;}
.kpiBox,#prodBox,#leftoverBox,#brineShiftDegreeBox{background:linear-gradient(180deg,#fff,#f8fbff)!important;border:1px solid #dbe6f2!important;border-radius:20px!important;color:#0f172a!important;box-shadow:0 10px 24px rgba(15,23,42,.05)!important;}#kpisRow .kpiBox{min-height:86px!important;display:flex!important;flex-direction:column!important;justify-content:center!important;}#kpisRow .kpiBox .fw-bold,#kpisRow .kpiBox .fw-semibold{font-size:1.25rem!important;color:#0f172a!important;}#kpisRow .kpiBox .softBadge{align-self:flex-start!important;margin-bottom:8px!important;}
.table-responsive,.table-wrap{border:1px solid #dbe6f2!important;border-radius:22px!important;overflow:auto!important;background:#fff!important;}.table{margin:0!important;--bs-table-bg:#fff!important;--bs-table-color:#0f172a!important;--bs-table-border-color:#e2e8f0!important;}.table thead th,#monitorSection .table thead th,#brineSection .table thead th{background:#eef5ff!important;color:#0f172a!important;font-weight:850!important;border-bottom:1px solid #dbe6f2!important;padding:1rem .9rem!important;}.table tbody td,#monitorSection .table tbody td,#brineSection .table tbody td{background:#fff!important;color:#0f172a!important;border-bottom:1px solid #e2e8f0!important;padding:.95rem .9rem!important;vertical-align:middle!important;}.table tbody tr:hover td{background:#f8fbff!important;}#monitorSection .table td:first-child,#brineSection .table td:first-child{background:#fbfdff!important;font-weight:800!important;}
.badge,.softBadge{border-radius:999px!important;font-weight:850!important;letter-spacing:0!important;}.badge.text-bg-success,.text-bg-success{background:#dcfce7!important;color:#166534!important;border:1px solid #bbf7d0!important;}.badge.text-bg-danger,.text-bg-danger{background:#fee2e2!important;color:#991b1b!important;border:1px solid #fecaca!important;}.badge.text-bg-warning,.text-bg-warning{background:#fef3c7!important;color:#92400e!important;border:1px solid #fde68a!important;}.badge.text-bg-info,.text-bg-info{background:#e0f2fe!important;color:#075985!important;border:1px solid #bae6fd!important;}.badge.text-bg-secondary,.text-bg-secondary{background:#f1f5f9!important;color:#475569!important;border:1px solid #e2e8f0!important;}.softBadge{display:inline-flex!important;align-items:center!important;gap:6px!important;padding:7px 12px!important;background:#e0f2fe!important;border:1px solid #bae6fd!important;color:#075985!important;}
.status-ok{background:#f0fdf4!important;}.status-warn{background:#fffbeb!important;}.status-alert{background:#fef2f2!important;}.section-title,.fw-semibold{color:#0f172a!important;}.text-muted,.muted,.small.text-muted,.form-text{color:#64748b!important;-webkit-text-fill-color:#64748b!important;}.readonlyNote{background:#fffbeb!important;color:#92400e!important;border:1px solid #fde68a!important;border-radius:16px!important;}
.modal-content{background:#fff!important;color:#0f172a!important;border:1px solid #dbe6f2!important;border-radius:26px!important;box-shadow:0 24px 70px rgba(15,23,42,.20)!important;}.modal-header{background:#f8fbff!important;border-bottom:1px solid #dbe6f2!important;}.modal-footer{background:#fbfdff!important;border-top:1px solid #dbe6f2!important;}.modal-title{font-weight:850!important;color:#0f172a!important;}.modal-content *:not(.btn):not(.badge):not(.text-bg-success):not(.text-bg-danger):not(.text-bg-warning):not(.text-bg-info):not(.text-bg-secondary){color:inherit;}.detailCard,.dispose-summary-card{background:linear-gradient(180deg,#fff,#f8fbff)!important;border:1px solid #dbe6f2!important;border-radius:22px!important;box-shadow:0 10px 24px rgba(15,23,42,.05)!important;}
@media(max-width:991.98px){.heroPanel .card-body{padding:18px!important;}.heroPanel .fw-bold.fs-4{font-size:1.45rem!important;}.btn-group{width:100%!important;}.btn-group .btn{flex:1 1 auto!important;}}

</style>
</head>
<body>
<nav class="navbar border-bottom">
  <div class="container-fluid">
    <span class="navbar-brand fw-bold">BReport · Calidad</span>
    <div class="ms-auto d-flex gap-2 align-items-center">
      <span class="text-muted small"><?=h($u['full_name'])?></span>
      <a class="btn btn-outline-secondary btn-sm" href="dashboard.php">Dashboard</a>
      <a class="btn btn-outline-danger btn-sm" href="logout.php">Salir</a>
    </div>
  </div>
</nav>

<div class="container py-4">

  <div class="stickyTop">
    <div class="row g-3">
      <div class="col-12">
        <div class="card cardSoft shadow-sm heroPanel">
          <div class="card-body">
            <div class="d-flex flex-wrap gap-2 align-items-center">
              <div>
                <div class="fw-bold fs-4">Calidad</div>
                <div class="muted small">Merma, Scrap, Sobrante Cubicado, Monitoreo y Salmuera</div>
              </div>
              <div class="ms-auto d-flex gap-2 align-items-center">
                <button id="btnReload" class="btn btn-outline-primary btn-sm pill">
                  <i class="bi bi-arrow-repeat me-1"></i>Recargar
                </button>
                <?php if (!$canWrite): ?>
                  <div class="readonlyNote small">
                    <i class="bi bi-lock me-1"></i>
                    Modo solo lectura
                  </div>
                <?php endif; ?>
              </div>
            </div>

            <div class="row g-2 mt-2">
              <div class="col-12 col-md-4">
                <label class="form-label small muted mb-1">Fecha</label>
                <input type="date" id="fDate" class="form-control" value="<?=h(date('Y-m-d'))?>">
              </div>

              <div class="col-12 col-md-3">
                <label class="form-label small muted mb-1">Turno</label>
                <select id="fShift" class="form-select">
                  <option value="1">Turno 1</option>
                  <option value="2">Turno 2</option>
                  <option value="3">Turno 3</option>
                </select>
              </div>

              <div class="col-12 col-md-5" id="productFilterWrap">
                <label class="form-label small muted mb-1">Producto (opcional)</label>
                <select id="fProduct" class="form-select"></select>
              </div>
            </div>

            <div class="d-flex flex-wrap gap-2 align-items-center mt-3">
              <div class="btn-group flex-wrap" role="group">
                <button class="btn btn-outline-dark pill active" data-mode="MERMA" id="btnMerma">
                  <i class="bi bi-droplet-half me-1"></i>Merma
                </button>
                <button class="btn btn-outline-dark pill" data-mode="SCRAP" id="btnScrap">
                  <i class="bi bi-trash3 me-1"></i>Scrap
                </button>
                <button class="btn btn-outline-dark pill" data-mode="LEFTOVER" id="btnLeftover">
                  <i class="bi bi-box-seam me-1"></i>Sobrante cubicado
                </button>
                <button class="btn btn-outline-dark pill" data-mode="MONITOR" id="btnMonitor">
                  <i class="bi bi-thermometer-snow me-1"></i>Monitoreo
                </button>
                <button class="btn btn-outline-dark pill" data-mode="BRINE" id="btnBrine">
                  <i class="bi bi-moisture me-1"></i>Salmuera
                </button>
              </div>

              <div class="ms-auto d-flex gap-2">
                <button class="btn btn-success pill" id="btnAdd" <?=(!$canWrite?'disabled':'')?>>
                  <i class="bi bi-plus-lg me-1"></i>Registrar
                </button>
              </div>
            </div>

            <div class="row g-2 mt-3" id="kpisRow"></div>
          </div>
        </div>
      </div>
    </div>
  </div>

  <!-- MERMA / SCRAP -->
  <div id="qualitySection">
    <div class="row g-3 mt-1">
      <div class="col-12 col-lg-4">
        <div class="card cardSoft shadow-sm">
          <div class="card-body">
            <div class="fw-semibold"><i class="bi bi-info-circle me-1"></i>Producción (solo lectura)</div>
            <div class="muted small mb-2">Referencia del mismo día y turno.</div>
            <div id="prodBox" class="kpiBox small muted">Sin datos</div>
          </div>
        </div>
      </div>
    </div>

    <div class="card cardSoft shadow-sm mt-3">
      <div class="card-body">
        <div class="table-responsive">
          <table class="table table-sm align-middle table-hover">
            <thead id="qualityHead"></thead>
            <tbody id="tbody"></tbody>
          </table>
        </div>
        <div class="text-muted small" id="emptyHint"></div>
      </div>
    </div>
  </div>

  <!-- SOBRANTE -->
  <div id="leftoverSection" class="sectionHidden">
    <div class="row g-3 mt-1">
      <div class="col-12 col-lg-4">
        <div class="card cardSoft shadow-sm">
          <div class="card-body">
            <div class="fw-semibold"><i class="bi bi-box-seam me-1"></i>Sobrante disponible</div>
            <div class="muted small mb-2">Disponible actual en warehouse SOB-CUB.</div>
            <div id="leftoverBox" class="kpiBox small muted">Sin datos</div>
            <button class="btn btn-danger w-100 mt-3" id="btnLeftoverScrap" type="button" <?=!$canWrite?'disabled':''?>>
              <i class="bi bi-trash3 me-1"></i>Dar de baja a SCRAP
            </button>
          </div>
        </div>
      </div>
    </div>

    <div class="card cardSoft shadow-sm mt-3">
      <div class="card-body">
        <div class="table-responsive">
          <table class="table table-sm align-middle table-hover">
            <thead>
              <tr>
                <th>Hora</th>
                <th>Turno</th>
                <th>Producto</th>
                <th>Motivo</th>
                <th class="text-end">Barras</th>
                <th class="text-end">Kg</th>
                <th>Notas</th>
                <th class="text-end">Acciones</th>
              </tr>
            </thead>
            <tbody id="leftoverBody"></tbody>
          </table>
        </div>
        <div class="text-muted small" id="leftoverHint"></div>
      </div>
    </div>
  </div>

  <!-- MONITOREO -->
  <div id="monitorSection" class="sectionHidden">
    <div class="card cardSoft shadow-sm mt-3">
      <div class="card-body">
        <div class="d-flex flex-wrap gap-2 align-items-center mb-3">
          <div>
            <div class="fw-bold">Monitoreo de Temperaturas y PPM</div>
            <div class="muted small">Recepción de turno + revisiones cada 2 horas</div>
          </div>
        </div>
        <div class="table-responsive">
          <table class="table table-bordered align-middle table-hover">
            <thead id="monitorHead"></thead>
            <tbody id="monitorBody"></tbody>
          </table>
        </div>
        <div class="small text-muted" id="monitorHint"></div>
      </div>
    </div>
  </div>

  <!-- SALMUERA -->
  <div id="brineSection" class="sectionHidden">
    <div class="card cardSoft shadow-sm mt-3">
      <div class="card-body">
        <div class="d-flex flex-wrap gap-2 align-items-center mb-3">
          <div>
            <div class="fw-bold">Monitoreo de Salmuera</div>
            <div class="muted small">Tanques A-F · cada 2 horas por turno · Temperatura por tanque + grado único por turno</div>
          </div>
        </div>
        <div id="brineShiftDegreeBox" class="mb-3"></div>
        <div class="table-responsive">
          <table class="table table-bordered align-middle table-hover">
            <thead id="brineHead"></thead>
            <tbody id="brineBody"></tbody>
          </table>
        </div>
        <div class="small text-muted" id="brineHint"></div>
      </div>
    </div>
  </div>

</div>

<!-- Modal merma/scrap -->
<div class="modal fade" id="editModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered">
    <form class="modal-content" id="editForm">
      <div class="modal-header">
        <div class="modal-title fw-bold" id="mTitle">Registrar</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="mBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-success" id="mSave">Guardar</button>
      </div>
    </form>
  </div>
</div>


<!-- Modal disposición merma -->
<div class="modal fade" id="disposeModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered">
    <form class="modal-content" id="disposeForm">
      <div class="modal-header">
        <div class="modal-title fw-bold">Disponer merma</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="disposeModalBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-success">Guardar</button>
      </div>
    </form>
  </div>
</div>

<!-- Modal confirmar disposición merma -->
<div class="modal fade" id="disposeConfirmModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <div class="modal-title fw-bold"><i class="bi bi-shield-check text-primary me-2"></i>Confirmar disposición de merma</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="disposeConfirmBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" id="btnDisposeBack">Regresar</button>
        <button type="button" class="btn btn-success" id="btnDisposeAccept"><i class="bi bi-check2-circle me-1"></i>Aceptar y registrar</button>
      </div>
    </div>
  </div>
</div>

<!-- Modal resumen disposición merma -->
<div class="modal fade" id="disposeSummaryModal" tabindex="-1" data-bs-backdrop="static" data-bs-keyboard="false">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content">
      <div class="modal-header">
        <div class="modal-title fw-bold"><i class="bi bi-clipboard-check text-success me-2"></i>Movimiento realizado</div>
      </div>
      <div class="modal-body" id="disposeSummaryBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-success" id="btnDisposeFinish"><i class="bi bi-check-lg me-1"></i>Finalizar</button>
      </div>
    </div>
  </div>
</div>

<!-- Modal monitoreo -->
<div class="modal fade" id="monitorModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered">
    <form class="modal-content" id="monitorForm">
      <div class="modal-header">
        <div class="modal-title fw-bold" id="mmTitle">Lectura</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="mmBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-success" id="mmSave">Guardar</button>
      </div>
    </form>
  </div>
</div>

<!-- Modal sobrante -->
<div class="modal fade" id="leftoverModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered">
    <form class="modal-content" id="leftoverForm">
      <div class="modal-header">
        <div class="modal-title fw-bold">Registrar baja de sobrante</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="leftoverModalBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-success">Guardar</button>
      </div>
    </form>
  </div>
</div>

<!-- Modal salmuera -->
<div class="modal fade" id="brineModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered">
    <form class="modal-content" id="brineForm">
      <div class="modal-header">
        <div class="modal-title fw-bold">Lectura de salmuera</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="brineModalBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-success">Guardar</button>
      </div>
    </form>
  </div>
</div>

<!-- Modal grado salmuera por tanque -->
<div class="modal fade" id="brineDegreeModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered">
    <form class="modal-content" id="brineDegreeForm">
      <div class="modal-header">
        <div class="modal-title fw-bold">Grado de salmuera por tanque</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="brineDegreeModalBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-success">Guardar</button>
      </div>
    </form>
  </div>
</div>


<!-- Modal detalle de merma -->
<div class="modal fade" id="mermaDetailModal" tabindex="-1">
  <div class="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable">
    <div class="modal-content">
      <div class="modal-header">
        <div>
          <div class="modal-title fw-bold">Detalle de capturas de merma</div>
          <div class="small text-muted">Hora registrada por pieza/captura del listado seleccionado.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="mermaDetailBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>

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

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'});
  return await r.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||{})
  });
  return await r.json();
}
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 hhmmValue(row){
  return String(row?.captured_hhmm || row?.reading_at?.slice(11,16) || '');
}
const $ = (id)=>document.getElementById(id);

let modal, disposeModal, disposeConfirmModal, disposeSummaryModal, monitorModal, leftoverModal, brineModal, brineDegreeModal;
let products = [];
let concepts = [];
let mode = 'MERMA';
let lastRows = [];
let mermaRows = [];
let monitorData = null;
let mermaDetailModal;
let leftoverData = null;
let brineData = null;
let currentDisposeRow = null;
let pendingDisposePayload = null;

async function loadProducts(){
  const j = await apiGet('products');
  products = j.ok ? (j.rows||[]) : [];
  const sel = $('fProduct');
  sel.innerHTML = `<option value="0">Todos</option>`;
  for(const p of products){
    sel.insertAdjacentHTML('beforeend', `<option value="${p.id}">${h(p.sku)} · ${h(p.name)}</option>`);
  }
}
async function loadConcepts(){
  const j = await apiGet('concepts', {concept_type:mode});
  concepts = j.ok ? (j.rows||[]) : [];
}
function setMode(m){
  mode = m;

  $('btnMerma').classList.toggle('active', mode==='MERMA');
  $('btnScrap').classList.toggle('active', mode==='SCRAP');
  $('btnLeftover').classList.toggle('active', mode==='LEFTOVER');
  $('btnMonitor').classList.toggle('active', mode==='MONITOR');
  $('btnBrine').classList.toggle('active', mode==='BRINE');

  $('qualitySection').classList.toggle('sectionHidden', !['MERMA','SCRAP'].includes(mode));
  $('leftoverSection').classList.toggle('sectionHidden', mode!=='LEFTOVER');
  $('monitorSection').classList.toggle('sectionHidden', mode!=='MONITOR');
  $('brineSection').classList.toggle('sectionHidden', mode!=='BRINE');

  $('productFilterWrap').classList.toggle('sectionHidden', !['MERMA','SCRAP'].includes(mode));
  $('btnAdd').disabled = (!CAN_WRITE) || ['MONITOR','BRINE'].includes(mode);

  reload();
}
function badgeType(){
  if(mode==='MERMA') return `<span class="badge text-bg-warning">MERMA</span>`;
  if(mode==='SCRAP') return `<span class="badge text-bg-secondary">SCRAP</span>`;
  if(mode==='LEFTOVER') return `<span class="badge text-bg-info">SOBRANTE CUBICADO</span>`;
  if(mode==='BRINE') return `<span class="badge text-bg-info">SALMUERA</span>`;
  return `<span class="badge text-bg-info">MONITOREO</span>`;
}
function renderQualityKpis(summary){
  const total = (summary||[]).reduce((a,x)=>a+(parseFloat(x.qty)||0),0);
  $('kpisRow').innerHTML = `
    <div class="col-12 col-md-4">
      <div class="kpiBox">
        <div class="muted small">Sección</div>
        <div class="fw-bold fs-5 mt-1">${badgeType()}</div>
      </div>
    </div>
    <div class="col-12 col-md-4">
      <div class="kpiBox">
        <div class="muted small">Fecha / Turno</div>
        <div class="fw-bold fs-5 mt-1">${h($('fDate').value)} · Turno ${h($('fShift').value)}</div>
      </div>
    </div>
    <div class="col-12 col-md-4">
      <div class="kpiBox">
        <div class="muted small">Total acumulado</div>
        <div class="fw-bold fs-3 mt-1">${total.toLocaleString(undefined,{maximumFractionDigits:3})}</div>
      </div>
    </div>
  `;
}
function renderMonitorKpis(summary){
  $('kpisRow').innerHTML = `
    <div class="col-12 col-md-3">
      <div class="kpiBox">
        <div class="muted small">Sección</div>
        <div class="fw-bold fs-5 mt-1">${badgeType()}</div>
      </div>
    </div>
    <div class="col-12 col-md-3">
      <div class="kpiBox">
        <div class="muted small">OK</div>
        <div class="fw-bold fs-2 mt-1">${summary.ok||0}</div>
      </div>
    </div>
    <div class="col-12 col-md-3">
      <div class="kpiBox">
        <div class="muted small">Warnings</div>
        <div class="fw-bold fs-2 mt-1">${summary.warn||0}</div>
      </div>
    </div>
    <div class="col-12 col-md-3">
      <div class="kpiBox">
        <div class="muted small">Alertas</div>
        <div class="fw-bold fs-2 mt-1">${summary.alert||0}</div>
      </div>
    </div>
  `;
}
function renderProductionBox(prod){
  if(!prod){
    $('prodBox').innerHTML = `<div class="muted">Sin datos</div>`;
    return;
  }
  if(!prod.length){
    $('prodBox').innerHTML = `<div class="muted">Sin producción registrada para el filtro.</div>`;
    return;
  }

  $('prodBox').innerHTML = `
    <div class="d-flex flex-column gap-2">
      ${prod.map(r=>`
        <div class="d-flex justify-content-between align-items-center p-2 rounded-3" style="background:#f8fbff;border:1px solid rgba(15,35,61,.08);">
          <div>
            <div class="mono fw-semibold">${h(r.sku||'')}</div>
            <div class="small muted">${h(r.product_name||'')}</div>
          </div>
          <div class="fw-bold fs-5">${(parseFloat(r.qty)||0).toLocaleString(undefined,{maximumFractionDigits:3})}</div>
        </div>
      `).join('')}
    </div>
  `;
}
function renderTable(rows){
  lastRows = rows || [];
  mermaRows = mode==='MERMA' ? [...lastRows] : mermaRows;
  const tb = $('tbody');
  const th = $('qualityHead');
  tb.innerHTML = '';

  if(mode==='MERMA'){
    th.innerHTML = `
      <tr>
        <th>Producto</th>
        <th>Origen de merma</th>
        <th class="text-end">Capturado</th>
        <th class="text-end">Disp. merma</th>
        <th class="text-end">Disp. scrap</th>
        <th class="text-end">Disponible</th>
        <th class="text-center">Capturas</th>
        <th class="mono">UOM</th>
        <th class="text-end">Acciones</th>
      </tr>
    `;
  }else{
    th.innerHTML = `
      <tr>
        <th>Producto</th>
        <th>Concepto</th>
        <th class="text-end">Acumulado</th>
        <th class="text-end">Capturas</th>
        <th class="mono">UOM</th>
      </tr>
    `;
  }

  if(!lastRows.length){
    $('emptyHint').textContent = 'Sin registros para este filtro.';
    return;
  }
  $('emptyHint').textContent = mode==='MERMA'
    ? 'Vista acumulada por producto y origen de merma. Da clic en cada renglón o en “Detalle” para ver la hora registrada por captura.'
    : 'Vista acumulada por producto y concepto de scrap.';

  for(const r of lastRows){
    if(mode==='MERMA'){
      const available = num(r.available_qty);
      const detailJson = JSON.stringify(r).replace(/'/g,"&#39;");
      tb.insertAdjacentHTML('beforeend', `
        <tr class="quality-row-click" data-merma-detail='${detailJson}'>
          <td>
            <div class="mono fw-semibold">${h(r.sku)}</div>
            <div class="small muted">${h(r.product_name)}</div>
          </td>
          <td>${h(r.concept_name)}</td>
          <td class="text-end fw-semibold">${num(r.captured_qty).toLocaleString(undefined,{maximumFractionDigits:3})}</td>
          <td class="text-end">${num(r.disposed_merma_qty).toLocaleString(undefined,{maximumFractionDigits:3})}</td>
          <td class="text-end">${num(r.disposed_scrap_qty).toLocaleString(undefined,{maximumFractionDigits:3})}</td>
          <td class="text-end fw-bold ${available>0 ? '' : 'text-muted'}">${available.toLocaleString(undefined,{maximumFractionDigits:3})}</td>
          <td class="text-center"><span class="softBadge"><i class="bi bi-clock-history"></i>${parseInt(r.capture_count||0,10)}</span></td>
          <td class="mono">${h(r.uom||'')}</td>
          <td class="text-end">
            <div class="d-inline-flex gap-2">
              <button type="button" class="btn btn-outline-primary btn-sm" data-merma-detail-btn='${detailJson}'>
                <i class="bi bi-clock-history"></i> Detalle
              </button>
              <button type="button" class="btn btn-outline-warning btn-sm" data-dispose-index="${lastRows.indexOf(r)}" ${(!CAN_WRITE || available<=0)?'disabled':''}>
                <i class="bi bi-box-arrow-down"></i> Disponer
              </button>
            </div>
          </td>
        </tr>
      `);
    } else {
      tb.insertAdjacentHTML('beforeend', `
        <tr>
          <td>
            <div class="mono">${h(r.sku)}</div>
            <div class="small muted">${h(r.product_name)}</div>
          </td>
          <td>${h(r.concept_name)}</td>
          <td class="text-end fw-semibold">${num(r.captured_qty).toLocaleString(undefined,{maximumFractionDigits:3})}</td>
          <td class="text-end">${parseInt(r.capture_count||0,10)}</td>
          <td class="mono">${h(r.uom||'')}</td>
        </tr>
      `);
    }
  }
}
function statusClass(code){
  if(code==='OK') return 'status-ok';
  if(code==='WARN') return 'status-warn';
  if(code==='ALERT') return 'status-alert';
  return '';
}
function statusBadge(code){
  if(code==='OK') return `<span class="badge text-bg-success">OK</span>`;
  if(code==='WARN') return `<span class="badge text-bg-warning">WARN</span>`;
  if(code==='ALERT') return `<span class="badge text-bg-danger">ALERT</span>`;
  return `<span class="badge text-bg-secondary">Pendiente</span>`;
}
function recorridoLabel(index){
  return `Recorrido ${Number(index || 1)}`;
}
function monitorRecorridoFromButton(btn){
  const kind = String(btn.dataset.kind || '').toUpperCase();
  const slot = btn.dataset.slot || '';
  if(kind === 'HANDOVER') return recorridoLabel(1);
  const slots = (monitorData && Array.isArray(monitorData.slots)) ? monitorData.slots : [];
  const idx = slots.indexOf(slot);
  return recorridoLabel(idx >= 0 ? idx + 2 : 1);
}
function brineRecorridoFromSlot(slot){
  const slots = (brineData && Array.isArray(brineData.slots)) ? brineData.slots : [];
  const idx = slots.indexOf(slot || '');
  return recorridoLabel(idx >= 0 ? idx + 1 : 1);
}
function renderMonitorCell(point, kind, slot, row){
  const captured = hhmmValue(row);
  return `
    <div class="monitorCell d-flex flex-column align-items-center justify-content-center gap-1">
      <div class="fw-semibold">${row ? `${parseFloat(row.reading_value).toLocaleString(undefined,{maximumFractionDigits:3})} ${h(point.unit_label)}` : '<span class="text-muted">Pendiente</span>'}</div>
      <div>${row ? statusBadge(row.status_code) : ''}</div>
      ${CAN_WRITE ? `
        <button class="btn btn-sm btn-outline-primary mt-1"
          data-monitor-edit="1"
          data-row-id="${row?.id || 0}"
          data-point-id="${point.id}"
          data-point-name="${h(point.point_name)}"
          data-point-type="${h(point.point_type)}"
          data-unit="${h(point.unit_label)}"
          data-kind="${kind}"
          data-slot="${slot}"
          data-captured-hhmm="${captured}"
          data-value="${row?.reading_value ?? ''}"
          data-notes="${h(row?.notes ?? '')}">
          ${row ? 'Editar' : 'Capturar'}
        </button>
      ` : ''}
    </div>
  `;
}
function renderMonitorTable(data){
  monitorData = data;
  const slots = data.slots || [];
  $('monitorHead').innerHTML = `
    <tr>
      <th>Parámetro</th>
      <th>${recorridoLabel(1)}</th>
      ${slots.map((s,i)=>`<th>${recorridoLabel(i+2)}</th>`).join('')}
    </tr>
  `;
  const rows = [];

  for(const item of (data.points||[])){
    const p = item.point;
    const receivedText = item.previous
      ? `<div class="small text-muted">Anterior: ${h(item.previous.reading_value)} ${h(p.unit_label)}</div>`
      : `<div class="small text-muted">Sin turno previo</div>`;

    const handover = item.handover;
    rows.push(`
      <tr>
        <td>
          <div class="fw-semibold">${h(p.point_name)}</div>
          <div class="small text-muted">${h(p.point_type)} · ${h(p.unit_label)}</div>
          ${receivedText}
        </td>
        <td class="${statusClass(handover?.status_code)}">
          ${renderMonitorCell(p, 'HANDOVER', data.handover_slot, handover)}
        </td>
        ${item.periodic.map(x=>`
          <td class="${statusClass(x.row?.status_code)}">
            ${renderMonitorCell(p, 'PERIODIC', x.slot, x.row)}
          </td>
        `).join('')}
      </tr>
    `);
  }

  $('monitorBody').innerHTML = rows.join('');
  $('monitorHint').textContent = 'Encabezados por recorrido. La hora real se captura únicamente dentro del modal de cada inspección. Temperaturas: OK <= -5°C, Warning > -5 y <= -1°C, Alerta > -1°C. PPM: OK 15-25.';
}
function renderLeftover(data){
  const stock = data.stock || {bars:0,kg:0};
  $('leftoverBox').innerHTML = `
    <div class="d-flex flex-column gap-2">
      <div class="d-flex justify-content-between align-items-center p-2 rounded-3" style="background:#f8fbff;border:1px solid rgba(15,35,61,.08);">
        <div>
          <div class="mono fw-semibold">${h(data.product?.sku || '')}</div>
          <div class="small muted">${h(data.product?.name || '')}</div>
        </div>
        <div class="text-end">
          <div class="fw-bold fs-5">${num(stock.bars).toFixed(2)} barras</div>
          <div class="small muted">${num(stock.kg).toFixed(2)} kg</div>
        </div>
      </div>
    </div>
  `;

  const tb = $('leftoverBody');
  tb.innerHTML = '';

  const rows = data.rows || [];
  if(!rows.length){
    $('leftoverHint').textContent = 'Sin registros de baja de sobrante para este día/turno.';
    return;
  }
  $('leftoverHint').textContent = '';

  for(const r of rows){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono">${h(String(r.disposal_time||'').replace('T',' '))}</td>
        <td>Turno ${h(r.shift)}</td>
        <td><div class="mono">${h(r.sku||'')}</div><div class="small muted">${h(r.product_name||'')}</div></td>
        <td>${h(r.reason||'')}</td>
        <td class="text-end fw-semibold">${num(r.qty_bars).toFixed(2)}</td>
        <td class="text-end fw-semibold">${num(r.qty_kg).toFixed(2)}</td>
        <td class="small">${h(r.notes||'')}</td>
        <td class="text-end">
          <button class="btn btn-outline-danger btn-sm" data-leftover-del="${r.id}" <?=!$canWrite?'disabled':''?>>
            <i class="bi bi-trash"></i>
          </button>
        </td>
      </tr>
    `);
  }
}
function brineStatusBadge(code){
  if(code==='OK') return `<span class="badge text-bg-success">OK</span>`;
  if(code==='WARN') return `<span class="badge text-bg-warning">WARN</span>`;
  if(code==='ALERT') return `<span class="badge text-bg-danger">ALERT</span>`;
  return `<span class="badge text-bg-secondary">Pendiente</span>`;
}
function renderBrine(data){
  const slots = data.slots || [];
  const primarySlot = data.primary_slot || (slots[0] || '');
  const tankDegrees = data.tank_degrees || {};

  $('brineShiftDegreeBox').innerHTML = `
    <div class="card" style="background:rgba(255,255,255,.03);border:1px solid rgba(255,255,255,.08);border-radius:18px;">
      <div class="card-body d-flex flex-column flex-lg-row justify-content-between align-items-lg-center gap-3">
        <div>
          <div class="fw-bold">Grado de salmuera por tanque</div>
          <div class="small text-muted">Ahora el grado se captura una sola vez por tanque y por turno. </div>
        </div>
        <div class="small text-muted">Captura primero el grado del tanque y luego sus temperaturas por recorrido.</div>
      </div>
    </div>
  `;

  $('brineHead').innerHTML = `
    <tr>
      <th>Tanque</th>
      <th>Grado</th>
      ${slots.map((s,i)=>`<th>${recorridoLabel(i+1)}</th>`).join('')}
    </tr>
  `;

  const rows = [];
  const map = data.map || {};

  for(const tank of (data.tanks || [])){
    const degreeRow = tankDegrees[String(tank.id)] || tankDegrees[tank.id] || null;
    const degreeCaptured = degreeRow?.captured_at ? String(degreeRow.captured_at).slice(11,16) : primarySlot;

    rows.push(`
      <tr>
        <td>
          <div class="fw-semibold">${h(tank.tank_name)}</div>
          <div class="small text-muted">Tanque ${h(tank.tank_code)}</div>
        </td>
        <td class="${degreeRow ? statusClass(degreeRow.status_code) : ''}">
          <div class="monitorCell d-flex flex-column align-items-center justify-content-center gap-1">
            <div class="fw-semibold">${degreeRow ? num(degreeRow.degree_value).toFixed(2) + '°' : '<span class="text-muted">Pendiente</span>'}</div>
            <div class="small text-muted">${degreeRow ? 'Grado registrado' : 'Sin grado'}</div>
            <div>${degreeRow ? brineStatusBadge(degreeRow.status_code) : ''}</div>
            ${CAN_WRITE ? `
              <button class="btn btn-sm btn-outline-primary mt-1"
                data-brine-degree-edit="1"
                data-row-id="${degreeRow?.id || 0}"
                data-tank-id="${tank.id}"
                data-tank-name="${h(tank.tank_name)}"
                data-primary-slot="${primarySlot}"
                data-captured-hhmm="${degreeCaptured}"
                data-degree="${degreeRow?.degree_value ?? ''}"
                data-notes="${h(degreeRow?.notes ?? '')}">
                ${degreeRow ? 'Editar grado' : 'Capturar grado'}
              </button>
            ` : ''}
          </div>
        </td>
        ${slots.map(slot=>{
          const row = map[`${tank.id}|${slot}`];
          const captured = hhmmValue(row);
          return `
            <td class="${row ? statusClass(row.temp_status) : ''}">
              <div class="monitorCell d-flex flex-column align-items-center justify-content-center gap-1">
                <div class="fw-semibold">${row ? `${num(row.temp_c).toFixed(1)}°C` : '<span class="text-muted">Pendiente</span>'}</div>
                <div>
                  ${row ? brineStatusBadge(row.temp_status) : ''}
                </div>
                ${CAN_WRITE ? `
                  <button class="btn btn-sm btn-outline-primary mt-1"
                    data-brine-edit="1"
                    data-row-id="${row?.id || 0}"
                    data-tank-id="${tank.id}"
                    data-tank-name="${h(tank.tank_name)}"
                    data-slot="${slot}"
                    data-primary-slot="${primarySlot}"
                    data-captured-hhmm="${captured}"
                    data-temp="${row?.temp_c ?? ''}"
                    data-notes="${h(row?.notes ?? '')}">
                    ${row ? 'Editar' : 'Capturar'}
                  </button>
                ` : ''}
              </div>
            </td>
          `;
        }).join('')}
      </tr>
    `);
  }

  $('brineBody').innerHTML = rows.join('');
  $('brineHint').textContent = `Encabezados por recorrido. La hora real se captura únicamente dentro del modal de cada inspección.`;
}
async function reload(){
  if(mode === 'MONITOR'){
    const j = await apiGet('monitor_dashboard', {
      date: $('fDate').value,
      shift: $('fShift').value
    });
    if(!j.ok){
      alert((j.error||'Error') + (j.message ? ("\n\n" + j.message) : ''));
      return;
    }
    renderMonitorKpis(j.summary || {});
    renderMonitorTable(j);
    return;
  }

  if(mode === 'LEFTOVER'){
    const j = await apiGet('leftover_dashboard', {
      date: $('fDate').value,
      shift: $('fShift').value
    });
    if(!j.ok){
      alert((j.error||'Error') + (j.message ? ("\n\n" + j.message) : ''));
      return;
    }
    leftoverData = j;
    renderQualityKpis([{qty:(j.summary?.bars || 0)}]);
    renderLeftover(j);
    return;
  }

  if(mode === 'BRINE'){
    const j = await apiGet('brine_dashboard', {
      date: $('fDate').value,
      shift: $('fShift').value
    });
    if(!j.ok){
      alert((j.error||'Error') + (j.message ? ("\n\n" + j.message) : ''));
      return;
    }
    brineData = j;
    renderMonitorKpis(j.summary || {});
    renderBrine(j);
    return;
  }

  await loadConcepts();

  const params = {
    date: $('fDate').value,
    shift: $('fShift').value,
    product_id: $('fProduct').value,
    concept_type: mode
  };
  const j = await apiGet('list', params);
  if(!j.ok){
    alert((j.error||'Error') + (j.message ? ("\n\n" + j.message) : ''));
    return;
  }
  renderQualityKpis(j.summary||[]);
  renderProductionBox(j.production);
  renderTable(j.rows||[]);
}

async function openMermaDetail(row){
  const params = {
    date: $('fDate').value,
    shift: $('fShift').value,
    product_id: row.product_id,
    concept_id: row.concept_id
  };
  const j = await apiGet('merma_detail', params);
  if(!j.ok){
    alert(j.error || 'No se pudo cargar el detalle de merma');
    return;
  }

  const rows = j.rows || [];
  $('mermaDetailBody').innerHTML = `
    <div class="detailCard mb-3">
      <div class="d-flex flex-wrap gap-2 align-items-center justify-content-between">
        <div>
          <div class="mono fw-bold">${h(row.sku || '')}</div>
          <div class="small text-muted">${h(row.product_name || '')}</div>
        </div>
        <div class="text-end">
          <div class="softBadge"><i class="bi bi-droplet-half"></i>${h(row.concept_name || '')}</div>
          <div class="small text-muted mt-1">Turno ${h($('fShift').value)} · ${h($('fDate').value)}</div>
        </div>
      </div>
      <div class="row g-2 mt-2">
        <div class="col-12 col-md-3"><div class="small text-muted">Capturado</div><div class="fw-bold">${num(row.captured_qty).toLocaleString(undefined,{maximumFractionDigits:3})} ${h(row.uom || '')}</div></div>
        <div class="col-12 col-md-3"><div class="small text-muted">Disponible</div><div class="fw-bold">${num(row.available_qty).toLocaleString(undefined,{maximumFractionDigits:3})} ${h(row.uom || '')}</div></div>
        <div class="col-12 col-md-3"><div class="small text-muted">Disp. merma</div><div class="fw-bold">${num(row.disposed_merma_qty).toLocaleString(undefined,{maximumFractionDigits:3})}</div></div>
        <div class="col-12 col-md-3"><div class="small text-muted">Disp. scrap</div><div class="fw-bold">${num(row.disposed_scrap_qty).toLocaleString(undefined,{maximumFractionDigits:3})}</div></div>
      </div>
    </div>

    <div class="table-responsive">
      <table class="table table-sm align-middle table-hover">
        <thead>
          <tr>
            <th>Hora registrada</th>
            <th class="text-end">Cantidad</th>
            <th>Notas</th>
          </tr>
        </thead>
        <tbody>
          ${rows.length ? rows.map(x => `
            <tr>
              <td class="mono fw-semibold">${h((x.created_at || '').slice(11,16) || '--:--')}</td>
              <td class="text-end">${num(x.qty).toLocaleString(undefined,{maximumFractionDigits:3})} ${h(x.uom || '')}</td>
              <td>${h(x.notes || '') || '<span class="text-muted">Sin notas</span>'}</td>
            </tr>
          `).join('') : `
            <tr>
              <td colspan="3" class="text-center text-muted">Sin detalle disponible para este listado.</td>
            </tr>
          `}
        </tbody>
      </table>
    </div>
  `;
  mermaDetailModal.show();
}

function openModal(row=null){
  const isNew = !row;
  $('mTitle').textContent = isNew ? 'Registrar' : 'Editar';

  const productOptions = products.map(p=>{
    const sel = row && parseInt(row.product_id,10)===parseInt(p.id,10) ? 'selected' : '';
    return `<option ${sel} value="${p.id}">${h(p.sku)} · ${h(p.name)}</option>`;
  }).join('');

  const conceptOptions = concepts.map(c=>{
    const sel = row && parseInt(row.concept_id,10)===parseInt(c.id,10) ? 'selected' : '';
    return `<option ${sel} value="${c.id}">${h(c.name)}</option>`;
  }).join('');

  $('mBody').innerHTML = `
    <input type="hidden" name="id" value="${row?row.id:0}">
    <div class="row g-2">
      <div class="col-12 col-md-6">
        <label class="form-label">Fecha</label>
        <input class="form-control" type="date" name="work_date" value="${h(row?.work_date || $('fDate').value)}" required>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Turno</label>
        <select class="form-select" name="shift">
          ${[1,2,3].map(s=>{
            const sel = String(row?.shift || $('fShift').value)===String(s) ? 'selected' : '';
            return `<option ${sel} value="${s}">Turno ${s}</option>`;
          }).join('')}
        </select>
      </div>
      <div class="col-12">
        <label class="form-label">Producto</label>
        <select class="form-select" name="product_id" required>${productOptions}</select>
      </div>
      <div class="col-12">
        <label class="form-label">Concepto (${mode})</label>
        <select class="form-select" name="concept_id" required>${conceptOptions}</select>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Cantidad</label>
        <input class="form-control mono" name="qty" inputmode="decimal" value="${h(row?.qty ?? 0)}" required>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Notas</label>
        <input class="form-control" name="notes" value="${h(row?.notes ?? '')}">
      </div>
    </div>
  `;
  modal.show();
}
function formToObj(){
  const fd = new FormData($('editForm'));
  const o = {};
  for(const [k,v] of fd.entries()) o[k]=v;
  o.id = parseInt(o.id||'0',10);
  o.shift = parseInt(o.shift||'1',10);
  o.product_id = parseInt(o.product_id||'0',10);
  o.concept_id = parseInt(o.concept_id||'0',10);
  o.qty = parseFloat(String(o.qty||'0').replace(',','.')) || 0;
  o.concept_type = mode;
  return o;
}
async function save(){
  const payload = formToObj();
  const j = await apiPost('save', payload);
  if(!j.ok){ alert(j.error||'Error'); return; }
  modal.hide();
  await reload();
}
async function del(id){
  if(!confirm('¿Eliminar registro?')) return;
  const j = await apiPost('delete', {id});
  if(!j.ok){ alert(j.error||'Error'); return; }
  await reload();
}

function openDisposeModal(row){
  currentDisposeRow = row || null;
  pendingDisposePayload = null;
  const available = num(row.available_qty);
  $('disposeModalBody').innerHTML = `
    <input type="hidden" name="work_date" value="${$('fDate').value}">
    <input type="hidden" name="shift" value="${$('fShift').value}">
    <input type="hidden" name="product_id" value="${row.product_id}">
    <input type="hidden" name="concept_id" value="${row.concept_id}">

    <div class="merma-action-note mb-3">
      <div class="fw-bold mb-1">${h(row.sku)} · ${h(row.product_name)}</div>
      <div class="small">
        Origen: <b>${h(row.concept_name)}</b> · Disponible para disponer:
        <b>${available.toLocaleString(undefined,{maximumFractionDigits:3})}</b> ${h(row.uom||'pz')}
      </div>
      <div class="small mt-1">
        Lo que captures en <b>Merma aprovechable</b> entrará al almacén
        <b>Aprovechamiento de Merma</b> como <b>Triturado</b>. Lo que captures en <b>Scrap</b> se enviará como scrap.
      </div>
    </div>

    <div class="disposal-split-box mb-3">
      <div class="row g-3">
        <div class="col-12 col-md-6">
          <label class="form-label fw-semibold">Merma aprovechable</label>
          <input class="form-control mono readonly-soft" name="qty_merma" inputmode="decimal" value="${available.toFixed(3)}" data-dispose-input="1" readonly>
          <div class="form-text">Se calcula automáticamente: disponible menos Scrap.</div>
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label fw-semibold">Scrap</label>
          <input class="form-control mono" name="qty_scrap" inputmode="decimal" value="0" data-dispose-input="1">
          <div class="form-text">Cantidad que se registrará como Scrap.</div>
        </div>
      </div>
      <div class="d-flex flex-wrap justify-content-between gap-2 mt-3">
        <div class="small text-muted">Disponible: <span class="fw-bold">${available.toLocaleString(undefined,{maximumFractionDigits:3})}</span></div>
        <div class="small">Total capturado: <span id="disposeTotalPreview" class="disposal-total-ok">${available.toLocaleString(undefined,{maximumFractionDigits:3})}</span></div>
      </div>
    </div>

    <div class="mb-2">
      <label class="form-label fw-semibold">Notas</label>
      <input class="form-control" name="notes" value="" placeholder="Opcional">
    </div>
  `;

  $('disposeModalBody').dataset.available = String(available);
  disposeModal.show();
  updateDisposePreview();
}


function updateDisposePreview(source=null){
  const body = $('disposeModalBody');
  if(!body) return;
  const available = num(body.dataset.available || 0);
  const merInput = body.querySelector('[name="qty_merma"]');
  const scrapInput = body.querySelector('[name="qty_scrap"]');
  let scrap = parseFloat(String(scrapInput?.value || '0').replace(',','.')) || 0;
  if(scrap < 0) scrap = 0;
  if(scrap > available) scrap = available;
  if(scrapInput && source && source.name === 'qty_scrap') scrapInput.value = scrap.toFixed(3);

  // Regla solicitada: Merma inicia con el total disponible y se resta automáticamente al capturar Scrap.
  const mermaCalc = Math.max(0, available - scrap);
  if(merInput) merInput.value = mermaCalc.toFixed(3);

  const total = mermaCalc + scrap;
  const el = $('disposeTotalPreview');
  if(el){
    el.textContent = total.toLocaleString(undefined,{maximumFractionDigits:3});
    el.classList.toggle('disposal-total-ok', Math.abs(total - available) <= 0.0001 && total > 0);
    el.classList.toggle('disposal-total-bad', Math.abs(total - available) > 0.0001 || total <= 0);
  }
}

function disposeFormToObj(){
  const fd = new FormData($('disposeForm'));
  const o = {};
  for(const [k,v] of fd.entries()) o[k]=v;
  o.shift = parseInt(o.shift||'1',10);
  o.product_id = parseInt(o.product_id||'0',10);
  o.concept_id = parseInt(o.concept_id||'0',10);
  o.qty_merma = parseFloat(String(o.qty_merma||'0').replace(',','.')) || 0;
  o.qty_scrap = parseFloat(String(o.qty_scrap||'0').replace(',','.')) || 0;
  return o;
}
function disposeSummaryHtml(payload, row, title='Resumen de disposición'){
  const available = num($('disposeModalBody')?.dataset.available || row?.available_qty || 0);
  const qtyMerma = num(payload.qty_merma);
  const qtyScrap = num(payload.qty_scrap);
  const total = qtyMerma + qtyScrap;
  return `
    <div class="dispose-summary-card">
      <div class="fw-bold mb-2">${h(title)}</div>
      <div class="dispose-summary-line"><span class="dispose-summary-label">Producto</span><span class="dispose-summary-value">${h(row?.sku || '')} · ${h(row?.product_name || '')}</span></div>
      <div class="dispose-summary-line"><span class="dispose-summary-label">Origen</span><span class="dispose-summary-value">${h(row?.concept_name || '')}</span></div>
      <div class="dispose-summary-line"><span class="dispose-summary-label">Fecha / turno</span><span class="dispose-summary-value">${h(payload.work_date)} · Turno ${h(payload.shift)}</span></div>
      <div class="dispose-summary-line"><span class="dispose-summary-label">Disponible original</span><span class="dispose-summary-value">${available.toLocaleString(undefined,{maximumFractionDigits:3})} ${h(row?.uom || 'pz')}</span></div>
      <div class="dispose-summary-line"><span class="dispose-summary-label">Aprovechamiento de merma (Triturado)</span><span class="dispose-summary-value text-primary">${qtyMerma.toLocaleString(undefined,{maximumFractionDigits:3})} ${h(row?.uom || 'pz')}</span></div>
      <div class="dispose-summary-line"><span class="dispose-summary-label">Scrap</span><span class="dispose-summary-value text-danger">${qtyScrap.toLocaleString(undefined,{maximumFractionDigits:3})} ${h(row?.uom || 'pz')}</span></div>
      <div class="dispose-summary-line"><span class="dispose-summary-label">Total dispuesto</span><span class="dispose-summary-value">${total.toLocaleString(undefined,{maximumFractionDigits:3})} ${h(row?.uom || 'pz')}</span></div>
      ${payload.notes ? `<div class="dispose-summary-line"><span class="dispose-summary-label">Notas</span><span class="dispose-summary-value">${h(payload.notes)}</span></div>` : ''}
    </div>
  `;
}

async function saveDispose(){
  const payload = disposeFormToObj();
  const available = num($('disposeModalBody').dataset.available || 0);
  const total = num(payload.qty_merma) + num(payload.qty_scrap);

  if(total <= 0){
    alert('Captura una cantidad a disponer.');
    return;
  }
  if(Math.abs(total - available) > 0.0001){
    alert('La suma de Merma aprovechable + Scrap debe ser igual al disponible: ' + available.toLocaleString(undefined,{maximumFractionDigits:3}));
    return;
  }

  pendingDisposePayload = payload;
  $('disposeConfirmBody').innerHTML = disposeSummaryHtml(payload, currentDisposeRow, 'Confirma el movimiento antes de registrarlo');
  disposeModal.hide();
  disposeConfirmModal.show();
}

async function confirmDisposeSave(){
  if(!pendingDisposePayload) return;
  const btn = $('btnDisposeAccept');
  const oldHtml = btn.innerHTML;
  btn.disabled = true;
  btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Registrando...';
  try{
    const j = await apiPost('dispose_merma', pendingDisposePayload);
    if(!j.ok){
      alert(j.error||'Error');
      disposeConfirmModal.hide();
      disposeModal.show();
      return;
    }
    const payload = {...pendingDisposePayload, qty_merma:j.qty_merma ?? pendingDisposePayload.qty_merma, qty_scrap:j.qty_scrap ?? pendingDisposePayload.qty_scrap};
    disposeConfirmModal.hide();
    $('disposeSummaryBody').innerHTML = `
      ${disposeSummaryHtml(payload, currentDisposeRow, 'Disposición registrada correctamente')}
      <div class="alert alert-success mt-3 mb-0">
        <div class="fw-bold"><i class="bi bi-check-circle me-1"></i>${h(j.message || 'Movimiento registrado correctamente.')}</div>
        <div class="small mt-1">Folio interno de disposición: <b>${h(j.id || '')}</b></div>
      </div>
    `;
    disposeSummaryModal.show();
  } finally {
    btn.disabled = false;
    btn.innerHTML = oldHtml;
  }
}

async function finishDisposeSummary(){
  disposeSummaryModal.hide();
  pendingDisposePayload = null;
  currentDisposeRow = null;
  await reload();
}

function openMonitorModal(btn){
  $('mmTitle').textContent = 'Captura de lectura';
  $('mmBody').innerHTML = `
    <input type="hidden" name="id" value="${btn.dataset.rowId || 0}">
    <input type="hidden" name="point_id" value="${btn.dataset.pointId}">
    <input type="hidden" name="reading_kind" value="${btn.dataset.kind}">
    <input type="hidden" name="slot_label" value="${btn.dataset.slot}">
    <input type="hidden" name="work_date" value="${$('fDate').value}">
    <input type="hidden" name="shift" value="${$('fShift').value}">
    <div class="mb-2">
      <div class="fw-semibold">${btn.dataset.pointName}</div>
      <div class="small text-muted">${monitorRecorridoFromButton(btn)}</div>
    </div>
    <div class="mb-3">
      <label class="form-label">Hora real de inspección (HH:MM)</label>
      <input class="form-control mono" name="captured_hhmm" placeholder="07:04" value="${btn.dataset.capturedHhmm || btn.dataset.slot || ''}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Valor (${btn.dataset.unit})</label>
      <input class="form-control mono" name="reading_value" inputmode="decimal" value="${btn.dataset.value || ''}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Notas</label>
      <input class="form-control" name="notes" value="${btn.dataset.notes || ''}">
    </div>
  `;
  monitorModal.show();
}
function monitorFormToObj(){
  const fd = new FormData($('monitorForm'));
  const o = {};
  for(const [k,v] of fd.entries()) o[k]=v;
  o.id = parseInt(o.id||'0',10);
  o.point_id = parseInt(o.point_id||'0',10);
  o.shift = parseInt(o.shift||'1',10);
  o.captured_hhmm = String(o.captured_hhmm||'').trim();
  o.reading_value = parseFloat(String(o.reading_value||'0').replace(',','.')) || 0;
  return o;
}
async function saveMonitor(){
  const payload = monitorFormToObj();
  const j = await apiPost('save_monitor', payload);
  if(!j.ok){ alert(j.error||'Error'); return; }
  monitorModal.hide();
  await reload();
}
function openLeftoverModal(){
  if (!leftoverData || !leftoverData.product) return;

  $('leftoverModalBody').innerHTML = `
    <input type="hidden" name="work_date" value="${$('fDate').value}">
    <input type="hidden" name="shift" value="${$('fShift').value}">
    <div class="mb-2">
      <div class="fw-semibold">${h(leftoverData.product.sku)} · ${h(leftoverData.product.name)}</div>
      <div class="small muted">Disponible actual en SOB-CUB: <b>${num(leftoverData.stock?.bars).toFixed(2)}</b> barras / <b>${num(leftoverData.stock?.kg).toFixed(2)}</b> kg</div>
    </div>
    <div class="mb-3">
      <label class="form-label">Hora</label>
      <input class="form-control" type="datetime-local" name="disposal_time" value="${$('fDate').value}T08:00">
    </div>
    <div class="mb-3">
      <label class="form-label">Cantidad a mandar a SCRAP</label>
      <div class="input-group">
        <input class="form-control mono" name="qty_bars" inputmode="decimal" type="number" step="0.01" min="0.01" max="${num(leftoverData.stock?.bars).toFixed(2)}" value="${num(leftoverData.stock?.bars).toFixed(2)}" required>
        <span class="input-group-text">barras</span>
      </div>
      <div class="form-text">Puedes dar de baja todo o parcial. Se guardará con 2 decimales y se descontará como SCRAP desde SOB-CUB.</div>
      <button type="button" class="btn btn-sm btn-outline-secondary mt-2" onclick="document.querySelector('#leftoverForm [name=qty_bars]').value='${num(leftoverData.stock?.bars).toFixed(2)}'">Usar todo disponible</button>
    </div>
    <div class="mb-3">
      <label class="form-label">Motivo</label>
      <input class="form-control" name="reason" value="SCRAP de sobrante cubicado">
    </div>
    <div class="mb-3">
      <label class="form-label">Notas</label>
      <input class="form-control" name="notes" value="">
    </div>
  `;
  leftoverModal.show();
}
function leftoverFormToObj(){
  const fd = new FormData($('leftoverForm'));
  const o = {};
  for(const [k,v] of fd.entries()) o[k]=v;
  o.shift = parseInt(o.shift||'1',10);
  o.qty_bars = Math.round((parseFloat(String(o.qty_bars||'0').replace(',','.')) || 0) * 100) / 100;
  return o;
}
async function saveLeftover(){
  const payload = leftoverFormToObj();
  const available = num(leftoverData?.stock?.bars || 0);
  if (payload.qty_bars <= 0) { alert('Captura una cantidad mayor a 0.'); return; }
  if (payload.qty_bars - available > 0.000001) { alert('La cantidad excede el disponible en SOB-CUB.'); return; }
  const kg = payload.qty_bars * 150;
  if(!confirm(`¿Confirmas mandar a SCRAP ${payload.qty_bars.toFixed(2)} barras (${kg.toFixed(2)} kg) desde SOB-CUB?`)) return;
  const j = await apiPost('leftover_save', payload);
  if(!j.ok){ alert(j.error||'Error'); return; }
  leftoverModal.hide();
  await reload();
}
async function delLeftover(id){
  if(!confirm('¿Eliminar registro de baja de sobrante?')) return;
  const j = await apiPost('leftover_delete', {id});
  if(!j.ok){ alert(j.error||'Error'); return; }
  await reload();
}
function openBrineDegreeModal(btn){
  const primarySlot = btn.dataset.primarySlot || '';
  $('brineDegreeModalBody').innerHTML = `
    <input type="hidden" name="id" value="${btn.dataset.rowId || 0}">
    <input type="hidden" name="tank_id" value="${btn.dataset.tankId}">
    <input type="hidden" name="work_date" value="${$('fDate').value}">
    <input type="hidden" name="shift" value="${$('fShift').value}">
    <div class="mb-2">
      <div class="fw-semibold">${btn.dataset.tankName}</div>
      <div class="small text-muted">Grado único por tanque para el turno seleccionado</div>
    </div>
    <div class="mb-3">
      <label class="form-label">Hora real de captura (HH:MM)</label>
      <input class="form-control mono" name="captured_hhmm" placeholder="07:04" value="${btn.dataset.capturedHhmm || primarySlot}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Grado de salmuera</label>
      <input class="form-control mono" name="degree_value" inputmode="decimal" value="${btn.dataset.degree || ''}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Notas</label>
      <input class="form-control" name="notes" value="${btn.dataset.notes || ''}">
    </div>
  `;
  brineDegreeModal.show();
}
function brineDegreeFormToObj(){
  const fd = new FormData($('brineDegreeForm'));
  const o = {};
  for(const [k,v] of fd.entries()) o[k]=v;
  o.id = parseInt(o.id||'0',10);
  o.tank_id = parseInt(o.tank_id||'0',10);
  o.shift = parseInt(o.shift||'1',10);
  o.captured_hhmm = String(o.captured_hhmm||'').trim();
  o.degree_value = parseFloat(String(o.degree_value||'0').replace(',','.'));
  return o;
}

function openBrineModal(btn){
  const slot = btn.dataset.slot || '';
  $('brineModalBody').innerHTML = `
    <input type="hidden" name="id" value="${btn.dataset.rowId || 0}">
    <input type="hidden" name="tank_id" value="${btn.dataset.tankId}">
    <input type="hidden" name="work_date" value="${$('fDate').value}">
    <input type="hidden" name="shift" value="${$('fShift').value}">
    <input type="hidden" name="slot_label" value="${slot}">
    <div class="mb-2">
      <div class="fw-semibold">${btn.dataset.tankName}</div>
      <div class="small text-muted">${brineRecorridoFromSlot(slot)}</div>
    </div>
    <div class="mb-3">
      <label class="form-label">Hora real de inspección (HH:MM)</label>
      <input class="form-control mono" name="captured_hhmm" placeholder="07:04" value="${btn.dataset.capturedHhmm || slot}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Temperatura °C</label>
      <input class="form-control mono" name="temp_c" inputmode="decimal" value="${btn.dataset.temp || ''}" required>
    </div>
    <div class="mb-3">
      <label class="form-label">Notas</label>
      <input class="form-control" name="notes" value="${btn.dataset.notes || ''}">
    </div>
  `;
  brineModal.show();
}
function brineFormToObj(){
  const fd = new FormData($('brineForm'));
  const o = {};
  for(const [k,v] of fd.entries()) o[k]=v;
  o.id = parseInt(o.id||'0',10);
  o.tank_id = parseInt(o.tank_id||'0',10);
  o.shift = parseInt(o.shift||'1',10);
  o.captured_hhmm = String(o.captured_hhmm||'').trim();
  o.temp_c = parseFloat(String(o.temp_c||'0').replace(',','.')) || 0;
  return o;
}
async function saveBrineDegree(){
  const payload = brineDegreeFormToObj();
  if(!Number.isFinite(payload.degree_value)){
    alert('Captura un grado de salmuera válido');
    return;
  }
  if(!/^([01]?\d|2[0-3]):[0-5]\d$/.test(String(payload.captured_hhmm||''))){
    alert('Captura una hora válida en formato HH:MM');
    return;
  }
  const j = await apiPost('save_brine_degree', payload);
  if(!j.ok){ alert((j.error||'Error') + (j.message ? ('\n\n' + j.message) : '')); return; }
  brineDegreeModal.hide();
  await reload();
}

async function saveBrine(){
  const payload = brineFormToObj();
  if(!/^([01]?\d|2[0-3]):[0-5]\d$/.test(String(payload.captured_hhmm||''))){
    alert('Captura una hora válida en formato HH:MM');
    return;
  }
  if(!Number.isFinite(payload.temp_c)){
    alert('Captura una temperatura válida');
    return;
  }
  const j = await apiPost('save_brine', payload);
  if(!j.ok){ alert((j.error||'Error') + (j.message ? ('\n\n' + j.message) : '')); return; }
  brineModal.hide();
  await reload();
}

document.addEventListener('DOMContentLoaded', async ()=>{
  modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('editModal'));
  disposeModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('disposeModal'));
  disposeConfirmModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('disposeConfirmModal'));
  disposeSummaryModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('disposeSummaryModal'));
  monitorModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('monitorModal'));
  leftoverModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('leftoverModal'));
  brineModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('brineModal'));
  brineDegreeModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('brineDegreeModal'));
  mermaDetailModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('mermaDetailModal'));

  await loadProducts();

  $('btnMerma').addEventListener('click', ()=>setMode('MERMA'));
  $('btnScrap').addEventListener('click', ()=>setMode('SCRAP'));
  $('btnLeftover').addEventListener('click', ()=>setMode('LEFTOVER'));
  $('btnMonitor').addEventListener('click', ()=>setMode('MONITOR'));
  $('btnBrine').addEventListener('click', ()=>setMode('BRINE'));

  $('btnReload').addEventListener('click', reload);
  $('fDate').addEventListener('change', reload);
  $('fShift').addEventListener('change', reload);
  $('fProduct').addEventListener('change', reload);

  $('btnAdd').addEventListener('click', ()=>{
    if(!CAN_WRITE) return;

    if(mode==='LEFTOVER'){
      openLeftoverModal();
      return;
    }

    if(mode==='MONITOR' || mode==='BRINE') return;

    openModal(null);
  });

  $('editForm').addEventListener('submit', (e)=>{e.preventDefault(); save();});
  $('disposeForm').addEventListener('submit', (e)=>{e.preventDefault(); saveDispose();});
  $('disposeModalBody').addEventListener('input', (e)=>{ if(e.target && e.target.matches('[data-dispose-input]')) updateDisposePreview(e.target); });
  $('btnDisposeAccept').addEventListener('click', confirmDisposeSave);
  $('btnDisposeBack').addEventListener('click', ()=>{ disposeConfirmModal.hide(); disposeModal.show(); });
  $('btnDisposeFinish').addEventListener('click', finishDisposeSummary);
  $('monitorForm').addEventListener('submit', (e)=>{e.preventDefault(); saveMonitor();});
  $('leftoverForm').addEventListener('submit', (e)=>{e.preventDefault(); saveLeftover();});
  const btnLeftoverScrap = $('btnLeftoverScrap');
  if (btnLeftoverScrap) btnLeftoverScrap.addEventListener('click', openLeftoverModal);
  $('brineForm').addEventListener('submit', (e)=>{e.preventDefault(); saveBrine();});
  $('brineDegreeForm').addEventListener('submit', (e)=>{e.preventDefault(); saveBrineDegree();});

  document.addEventListener('click', (e)=>{
    const b1 = e.target.closest('[data-edit]');
    if(b1){
      const id = parseInt(b1.dataset.edit||'0',10);
      const row = lastRows.find(x=>parseInt(x.id,10)===id);
      if(row) openModal(row);
      return;
    }

    const b2 = e.target.closest('[data-del]');
    if(b2){
      const id = parseInt(b2.dataset.del||'0',10);
      if(id>0) del(id);
      return;
    }

    const detailBtn = e.target.closest('[data-merma-detail-btn]');
    if(detailBtn){
      try {
        openMermaDetail(JSON.parse(detailBtn.getAttribute('data-merma-detail-btn')));
      } catch(err) {}
      return;
    }

    const detailRow = e.target.closest('tr[data-merma-detail]');
    if(detailRow && !e.target.closest('button')){
      try {
        openMermaDetail(JSON.parse(detailRow.getAttribute('data-merma-detail')));
      } catch(err) {}
      return;
    }

    const bd = e.target.closest('[data-dispose-index]');
    if(bd){
      const idx = parseInt(bd.getAttribute('data-dispose-index') || '-1', 10);
      if(idx >= 0 && lastRows[idx]) openDisposeModal(lastRows[idx]);
      return;
    }

    const b3 = e.target.closest('[data-monitor-edit]');
    if(b3){
      openMonitorModal(b3);
      return;
    }

    const b4 = e.target.closest('[data-leftover-del]');
    if(b4){
      const id = parseInt(b4.getAttribute('data-leftover-del') || '0',10);
      if(id>0) delLeftover(id);
      return;
    }

    const b5 = e.target.closest('[data-brine-edit]');
    if(b5){
      openBrineModal(b5);
      return;
    }

    const b6 = e.target.closest('[data-brine-degree-edit]');
    if(b6){
      openBrineDegreeModal(b6);
      return;
    }
  });

  await reload();
});
</script>
</body>
</html>