<?php
/**
 * produccion_cubicado.php (v3.4)
 * ============================================================
 * CAMBIOS v3.5
 * - Agrega consulta histórica por fecha y turno sin afectar la captura del turno activo.
 *
 * CAMBIOS v3.4
 * - Simplifica tablero principal para evitar ruido operativo:
 *      * Disponible para cubicado en CUB
 *      * Sobrante disponible en SOB-CUB
 *      * Uso y sobrante únicamente del turno activo
 * - Agrega alertas visuales de inventario bajo/crítico.
 * - Al finalizar turno muestra resumen de barras usadas y sobrante generado.
 *
 * CAMBIOS v3.3
 * - Se agrega soporte para productos especiales de origen CUB/SOB-CUB
 * - Productos especiales pueden seleccionar origen de inventario:
 *      * CUB      = barra completa
 *      * SOB-CUB  = sobrante de cubicado
 * - Si usa CUB:
 *      * Calcula bars_used, bars_taken y bars_leftover normalmente
 * - Si usa SOB-CUB:
 *      * Consume únicamente bars_used desde sobrante
 *      * NO toma barras completas de CUB
 *      * NO genera nuevo sobrante
 * - Se guarda el origen real de inventario en cubing_entries.source_mode
 * - KPI de SOB-CUB ahora descuenta lo consumido por aprovechamiento
 * - El modal muestra selector de origen solo para el producto especial
 * ============================================================
 */

declare(strict_types=1);

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

require_scope();
ensure_session();

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

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

const WH_IN_CODE        = 'CUB';
const WH_CF1_CODE       = 'CF1';
const WH_OUT_CODE       = 'CF3';
const WH_LEFT_CODE      = 'SOB-CUB';
const WH_LEFT_NAME      = 'Sobrante Cubicado';
const BAR_KG            = 150.0;
const SPECIAL_MERMA_KEY = 'APROVECHAMIENTO DE MERMA';

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

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

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

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

function has_transfer_warehouse_columns(PDO $pdo): bool {
  return table_exists($pdo, 'transfers')
      && table_exists($pdo, 'transfer_items')
      && col_exists($pdo, 'transfers', 'from_warehouse_id')
      && col_exists($pdo, 'transfers', 'to_warehouse_id');
}

function ensure_cubing_schema(PDO $pdo): void {
  if (!col_exists($pdo, 'cubing_entries', 'bars_taken')) {
    $pdo->exec("ALTER TABLE cubing_entries ADD COLUMN bars_taken DECIMAL(14,6) NOT NULL DEFAULT 0.000000 AFTER bars_used");
  }
  if (!col_exists($pdo, 'cubing_entries', 'bars_leftover')) {
    $pdo->exec("ALTER TABLE cubing_entries ADD COLUMN bars_leftover DECIMAL(14,6) NOT NULL DEFAULT 0.000000 AFTER bars_taken");
  }
  if (!col_exists($pdo, 'cubing_entries', 'leftover_warehouse_id')) {
    $pdo->exec("ALTER TABLE cubing_entries ADD COLUMN leftover_warehouse_id BIGINT(20) UNSIGNED DEFAULT NULL AFTER source_warehouse_id");
  }
  if (!col_exists($pdo, 'cubing_entries', 'source_mode')) {
    $pdo->exec("ALTER TABLE cubing_entries ADD COLUMN source_mode VARCHAR(20) NOT NULL DEFAULT 'CUB' AFTER leftover_warehouse_id");
  }
}


function ensure_cubing_return_schema(PDO $pdo): void {
  /*
   * Reubicación operativa de barras completas enviadas de más a CUB.
   * No modifica capturas de producción ni conversiones; registra un movimiento
   * auditado de almacén CUB -> CF1 y sus dos afectaciones en inventory_moves.
   */
  if (!table_exists($pdo, 'cubing_bar_returns')) {
    $pdo->exec("
      CREATE TABLE cubing_bar_returns (
        id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
        company_id BIGINT NOT NULL,
        location_id BIGINT NOT NULL,
        work_date DATE NOT NULL,
        shift TINYINT NOT NULL,
        event_time DATETIME NOT NULL,
        product_id BIGINT NOT NULL,
        from_warehouse_id BIGINT NOT NULL,
        to_warehouse_id BIGINT NOT NULL,
        qty_bars DECIMAL(14,3) NOT NULL DEFAULT 0.000,
        notes VARCHAR(255) NULL,
        created_by BIGINT NOT NULL,
        created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY idx_cbr_scope_date (company_id, location_id, work_date, shift),
        KEY idx_cbr_product (company_id, location_id, product_id),
        KEY idx_cbr_created_at (created_at)
      ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
    ");
  }
}

function get_location_timezone(PDO $pdo, int $locationId): string {
  $st = $pdo->prepare("SELECT timezone FROM locations WHERE id=? LIMIT 1");
  $st->execute([$locationId]);
  $tz = (string)($st->fetchColumn() ?: 'America/Monterrey');
  return $tz !== '' ? $tz : 'America/Monterrey';
}

function tz_today(string $tz): string {
  return (new DateTimeImmutable('now', new DateTimeZone($tz)))->format('Y-m-d');
}

function normalize_dt_local(string $dt, string $tz): string {
  $dt = trim($dt);
  if ($dt === '') {
    return (new DateTimeImmutable('now', new DateTimeZone($tz)))->format('Y-m-d H:i:s');
  }
  $dt = str_replace('T', ' ', $dt);
  if (strlen($dt) === 16) $dt .= ':00';
  return $dt;
}

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

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

function ensure_leftover_warehouse(PDO $pdo, int $companyId, int $locationId): int {
  $id = wh_id_by_code($pdo, $companyId, $locationId, WH_LEFT_CODE);
  if ($id > 0) return $id;

  $st = $pdo->prepare("
    INSERT INTO warehouses(company_id, location_id, code, name, active, created_at)
    VALUES(?,?,?,?,1,NOW())
  ");
  $st->execute([$companyId, $locationId, WH_LEFT_CODE, WH_LEFT_NAME]);

  return (int)$pdo->lastInsertId();
}

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

function is_merma_product(array $row): bool {
  $txt = strtoupper(trim((string)($row['sku'] ?? '') . ' ' . (string)($row['name'] ?? '')));
  return str_contains($txt, 'APROVECHAMIENTO') && str_contains($txt, 'MERMA');
}

function is_triturado_product(array $row): bool {
  $txt = strtoupper(trim((string)($row['sku'] ?? '') . ' ' . (string)($row['name'] ?? '')));
  return str_contains($txt, 'TRIT') || str_contains($txt, 'TRITURADO');
}

/**
 * Peso real por pieza para productos de cubicado donde el factor no representa
 * aprovechamiento completo de la barra.
 *
 * Regla confirmada por operación:
 * - Para 3 kg, 5 kg, 3 kg SEVEN/Gourmet y triturado 10 kg, la barra completa se toma
 *   desde CUB/CF1, pero solo una parte se convierte en producto terminado.
 * - Por cada barra completa tomada se recuperan 10 kg hacia SOB-CUB.
 * - El resto queda como serrín/no recuperable y NO entra a inventario.
 * - No aplica para bolsa 1/12 ni bolsa 1/4.
 */
function cubing_piece_kg_for_output(array $row): float {
  $txt = strtoupper(trim((string)($row['sku'] ?? '') . ' ' . (string)($row['name'] ?? '')));

  if (str_contains($txt, '1/12') || str_contains($txt, '1 / 12')) return 0.0;
  if (str_contains($txt, '1/4') || str_contains($txt, '1 / 4')) return 0.0;

  if (str_contains($txt, '3KG') || str_contains($txt, '3 KG')) return 3.0;
  if (str_contains($txt, '5KG') || str_contains($txt, '5 KG')) return 5.0;

  $isTriturado = str_contains($txt, 'TRIT') || str_contains($txt, 'TRITURADO');
  if ($isTriturado && (str_contains($txt, '10KG') || str_contains($txt, '10 KG'))) return 10.0;

  return 0.0;
}

function cubing_recoverable_kg_per_bar_for_output(array $row): float {
  return cubing_piece_kg_for_output($row) > 0 ? 10.0 : 0.0;
}

function allowed_source_modes_for_output(array $row): array {
  // Regla especial:
  // - Aprovechamiento de merma: puede consumir CUB o SOB-CUB.
  // - Hielo triturado: puede consumir CUB o SOB-CUB.
  // - Resto de productos: solo CUB.
  if (is_merma_product($row)) return ['CUB', 'SOB-CUB'];
  if (is_triturado_product($row)) return ['CUB', 'SOB-CUB'];
  return ['CUB'];
}

function month_range_from_date(string $date): array {
  try {
    $d = new DateTimeImmutable($date !== '' ? $date : date('Y-m-d'));
  } catch (Throwable $e) {
    $d = new DateTimeImmutable(date('Y-m-d'));
  }
  $start = $d->modify('first day of this month')->format('Y-m-d');
  $end   = $d->modify('first day of next month')->format('Y-m-d');
  return [$start, $end];
}

function warehouse_stock_from_moves(PDO $pdo, int $companyId, int $locationId, int $productId, int $warehouseId): float {
  if ($productId <= 0 || $warehouseId <= 0 || !table_exists($pdo, 'inventory_moves')) return 0.0;

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


function warehouse_stock_from_moves_as_of(PDO $pdo, int $companyId, int $locationId, int $productId, int $warehouseId, string $date): float {
  if ($productId <= 0 || $warehouseId <= 0 || !table_exists($pdo, 'inventory_moves')) return 0.0;
  $date = trim($date);
  if ($date === '') {
    return warehouse_stock_from_moves($pdo, $companyId, $locationId, $productId, $warehouseId);
  }

  /*
    Inventario disponible por fecha consultada:
    Se calcula al cierre de la fecha operativa seleccionada usando work_date.
    Esto permite que al filtrar un día histórico el KPI de CUB/SOB-CUB no muestre
    el inventario actual de hoy, sino el saldo acumulado hasta ese día.
  */
  $st = $pdo->prepare("\n    SELECT COALESCE(SUM(\n      CASE\n        WHEN move_type IN ('IN','ADJ') THEN qty\n        WHEN move_type = 'OUT' THEN -qty\n        ELSE 0\n      END\n    ),0)\n    FROM inventory_moves\n    WHERE company_id=?\n      AND location_id=?\n      AND product_id=?\n      AND warehouse_id=?\n      AND work_date <= ?\n  ");
  $st->execute([$companyId, $locationId, $productId, $warehouseId, $date]);
  return (float)$st->fetchColumn();
}

function bars_received_month(PDO $pdo, int $companyId, int $locationId, int $barProductId, int $whInId, string $date): float {
  if ($barProductId <= 0 || $whInId <= 0 || !table_exists($pdo, 'inventory_moves')) return 0.0;
  [$start, $end] = month_range_from_date($date);

  $st = $pdo->prepare("\n    SELECT COALESCE(SUM(qty),0)\n    FROM inventory_moves\n    WHERE company_id=?\n      AND location_id=?\n      AND product_id=?\n      AND warehouse_id=?\n      AND move_type IN ('IN','ADJ')\n      AND work_date >= ?\n      AND work_date < ?\n  ");
  $st->execute([$companyId, $locationId, $barProductId, $whInId, $start, $end]);
  return (float)$st->fetchColumn();
}

function create_cubing_inventory_move(PDO $pdo, int $companyId, int $locationId, int $warehouseId, string $workDate, int $productId, float $qty, string $moveType, int $refId, string $notes, int $userId): int {
  $qty = round($qty, 6);
  if ($qty <= 0) return 0;
  $moveType = strtoupper($moveType);
  if (!in_array($moveType, ['IN','OUT','ADJ'], true)) {
    throw new RuntimeException('Tipo de movimiento inválido para cubicado.');
  }

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


function cub_return_summary(PDO $pdo, int $companyId, int $locationId, int $barProductId, float $qtyBars): array {
  $qtyBars = round($qtyBars, 3);
  if ($barProductId <= 0 || $qtyBars <= 0 || abs($qtyBars - round($qtyBars)) > 0.0000001) {
    throw new RuntimeException('La devolución debe ser por barras completas.');
  }

  $cubId = wh_id_by_code($pdo, $companyId, $locationId, WH_IN_CODE);
  $cf1Id = wh_id_by_code($pdo, $companyId, $locationId, WH_CF1_CODE);
  if ($cubId <= 0 || $cf1Id <= 0) {
    throw new RuntimeException('No se encontró la configuración de almacenes CUB y CF1 para esta localidad.');
  }

  $cubBefore = warehouse_stock_from_moves($pdo, $companyId, $locationId, $barProductId, $cubId);
  $cf1Before = warehouse_stock_from_moves($pdo, $companyId, $locationId, $barProductId, $cf1Id);
  if ($qtyBars - $cubBefore > 0.0000001) {
    throw new RuntimeException('No hay barras completas suficientes en CUB. Disponible actual: ' . number_format($cubBefore, 3, '.', '') . '.');
  }

  return [
    'qty_bars' => $qtyBars,
    'kg_move' => $qtyBars * BAR_KG,
    'cub_warehouse_id' => $cubId,
    'cf1_warehouse_id' => $cf1Id,
    'cub_before' => $cubBefore,
    'cub_after' => $cubBefore - $qtyBars,
    'cf1_before' => $cf1Before,
    'cf1_after' => $cf1Before + $qtyBars,
  ];
}

function create_cub_return_inventory_move(
  PDO $pdo,
  int $companyId,
  int $locationId,
  int $warehouseId,
  string $workDate,
  int $productId,
  float $qty,
  string $moveType,
  int $returnId,
  string $notes,
  int $userId
): void {
  $qty = round($qty, 3);
  if ($qty <= 0 || !in_array($moveType, ['IN', 'OUT'], true)) {
    throw new RuntimeException('Movimiento inválido para devolución de cubicado.');
  }

  $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, 'CUB_RETURN', ?, ?, ?, NOW()
    )
  ");
  $st->execute([
    $companyId, $locationId, $warehouseId, $workDate, $productId, $qty, $moveType,
    $returnId, mb_substr($notes, 0, 255), $userId
  ]);
}

function delete_cubing_inventory_moves(PDO $pdo, int $companyId, int $locationId, int $cubingEntryId): void {
  if ($cubingEntryId <= 0 || !table_exists($pdo, 'inventory_moves')) return;
  $st = $pdo->prepare("\n    DELETE FROM inventory_moves\n    WHERE company_id=?\n      AND location_id=?\n      AND ref_type='CUBING'\n      AND ref_id=?\n  ");
  $st->execute([$companyId, $locationId, $cubingEntryId]);
}

function bars_received_total(PDO $pdo, int $companyId, int $locationId, int $barProductId, int $whInId): float {
  $st = $pdo->prepare("
    SELECT COALESCE(SUM(qty),0)
    FROM production_entries
    WHERE company_id=? AND location_id=? AND product_id=? AND warehouse_id=?
  ");
  $st->execute([$companyId,$locationId,$barProductId,$whInId]);
  $productionIn = (float)$st->fetchColumn();

  if (!has_transfer_warehouse_columns($pdo)) {
    return $productionIn;
  }

  $st = $pdo->prepare("
    SELECT COALESCE(SUM(ti.qty),0)
    FROM transfers t
    JOIN transfer_items ti ON ti.transfer_id = t.id
    WHERE t.to_company_id = ?
      AND t.to_location_id = ?
      AND t.to_warehouse_id = ?
      AND t.status = 'RECEIVED'
      AND ti.product_id = ?
  ");
  $st->execute([$companyId, $locationId, $whInId, $barProductId]);
  $transferIn = (float)$st->fetchColumn();

  $st = $pdo->prepare("
    SELECT COALESCE(SUM(ti.qty),0)
    FROM transfers t
    JOIN transfer_items ti ON ti.transfer_id = t.id
    WHERE t.from_company_id = ?
      AND t.from_location_id = ?
      AND t.from_warehouse_id = ?
      AND t.status IN ('SENT','RECEIVED')
      AND ti.product_id = ?
  ");
  $st->execute([$companyId, $locationId, $whInId, $barProductId]);
  $transferOut = (float)$st->fetchColumn();

  return $productionIn + $transferIn - $transferOut;
}

function bars_used_total(PDO $pdo, int $companyId, int $locationId, int $barProductId): float {
  $st = $pdo->prepare("
    SELECT COALESCE(SUM(bars_used),0)
    FROM cubing_entries
    WHERE company_id=? AND location_id=? AND input_product_id=?
  ");
  $st->execute([$companyId,$locationId,$barProductId]);
  return (float)$st->fetchColumn();
}

function bars_taken_total(PDO $pdo, int $companyId, int $locationId, int $barProductId, int $leftWhId): float {
  $hasBarsTaken  = col_exists($pdo, 'cubing_entries', 'bars_taken');
  $hasSourceMode = col_exists($pdo, 'cubing_entries', 'source_mode');

  if ($hasBarsTaken && $hasSourceMode) {
    $st = $pdo->prepare("
      SELECT COALESCE(SUM(
        CASE
          WHEN source_mode='SOB-CUB' OR source_warehouse_id=? THEN 0
          WHEN bars_taken > 0 THEN bars_taken
          ELSE bars_used
        END
      ),0)
      FROM cubing_entries
      WHERE company_id=? AND location_id=? AND input_product_id=?
    ");
    $st->execute([$leftWhId, $companyId,$locationId,$barProductId]);
    return (float)$st->fetchColumn();
  }

  if ($hasBarsTaken) {
    $st = $pdo->prepare("
      SELECT COALESCE(SUM(CASE WHEN bars_taken > 0 THEN bars_taken ELSE bars_used END),0)
      FROM cubing_entries
      WHERE company_id=? AND location_id=? AND input_product_id=?
    ");
    $st->execute([$companyId,$locationId,$barProductId]);
    return (float)$st->fetchColumn();
  }

  $st = $pdo->prepare("
    SELECT COALESCE(SUM(bars_used),0)
    FROM cubing_entries
    WHERE company_id=? AND location_id=? AND input_product_id=?
  ");
  $st->execute([$companyId,$locationId,$barProductId]);
  return (float)$st->fetchColumn();
}

function bars_leftover_generated_total(PDO $pdo, int $companyId, int $locationId, int $barProductId): float {
  if (!col_exists($pdo, 'cubing_entries', 'bars_leftover')) {
    return 0.0;
  }

  $st = $pdo->prepare("
    SELECT COALESCE(SUM(bars_leftover),0)
    FROM cubing_entries
    WHERE company_id=? AND location_id=? AND input_product_id=?
  ");
  $st->execute([$companyId,$locationId,$barProductId]);
  return (float)$st->fetchColumn();
}

function bars_leftover_consumed_total(PDO $pdo, int $companyId, int $locationId, int $barProductId, int $leftWhId): float {
  $hasSourceMode = col_exists($pdo, 'cubing_entries', 'source_mode');

  if ($hasSourceMode) {
    $st = $pdo->prepare("
      SELECT COALESCE(SUM(bars_used),0)
      FROM cubing_entries
      WHERE company_id=? AND location_id=? AND input_product_id=?
        AND (source_mode='SOB-CUB' OR source_warehouse_id=?)
    ");
    $st->execute([$companyId,$locationId,$barProductId,$leftWhId]);
    return (float)$st->fetchColumn();
  }

  $st = $pdo->prepare("
    SELECT COALESCE(SUM(bars_used),0)
    FROM cubing_entries
    WHERE company_id=? AND location_id=? AND input_product_id=?
      AND source_warehouse_id=?
  ");
  $st->execute([$companyId,$locationId,$barProductId,$leftWhId]);
  return (float)$st->fetchColumn();
}

function leftover_manual_out_total(PDO $pdo, int $companyId, int $locationId, int $barProductId, int $leftWhId): float {
  if (!table_exists($pdo, 'inventory_moves') || !col_exists($pdo, 'inventory_moves', 'move_type')) {
    return 0.0;
  }

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

function calc_cub_inventory(PDO $pdo, int $companyId, int $locationId, int $barProductId, int $whInId, int $leftWhId, string $date = ''): array {
  /*
    Inventario real disponible:
    - CUB y SOB-CUB se calculan desde inventory_moves por almacén.
    - Los contadores de recepción/uso se muestran por mes del date seleccionado.
  */
  $dateForStock = trim($date);
  $cubAvailReal  = max(0.0, $dateForStock !== ''
    ? warehouse_stock_from_moves_as_of($pdo, $companyId, $locationId, $barProductId, $whInId, $dateForStock)
    : warehouse_stock_from_moves($pdo, $companyId, $locationId, $barProductId, $whInId));
  $leftAvailReal = max(0.0, $dateForStock !== ''
    ? warehouse_stock_from_moves_as_of($pdo, $companyId, $locationId, $barProductId, $leftWhId, $dateForStock)
    : warehouse_stock_from_moves($pdo, $companyId, $locationId, $barProductId, $leftWhId));

  [$monthStart, $monthEnd] = month_range_from_date($date !== '' ? $date : date('Y-m-d'));

  $recvMonth = bars_received_month($pdo, $companyId, $locationId, $barProductId, $whInId, $date);

  $st = $pdo->prepare("\n    SELECT\n      COALESCE(SUM(bars_used),0) AS bars_used,\n      COALESCE(SUM(\n        CASE\n          WHEN source_mode='SOB-CUB' OR source_warehouse_id=? THEN 0\n          WHEN bars_taken > 0 THEN bars_taken\n          ELSE bars_used\n        END\n      ),0) AS bars_taken,\n      COALESCE(SUM(bars_leftover),0) AS bars_leftover_total,\n      COALESCE(SUM(CASE WHEN source_mode='SOB-CUB' OR source_warehouse_id=? THEN bars_used ELSE 0 END),0) AS bars_leftover_consumed\n    FROM cubing_entries\n    WHERE company_id=?\n      AND location_id=?\n      AND input_product_id=?\n      AND work_date >= ?\n      AND work_date < ?\n  ");
  $st->execute([$leftWhId, $leftWhId, $companyId, $locationId, $barProductId, $monthStart, $monthEnd]);
  $m = $st->fetch(PDO::FETCH_ASSOC) ?: [];

  $usedMonth         = (float)($m['bars_used'] ?? 0);
  $takenMonth        = (float)($m['bars_taken'] ?? 0);
  $leftGenMonth      = (float)($m['bars_leftover_total'] ?? 0);
  $leftConsumedMonth = (float)($m['bars_leftover_consumed'] ?? 0);

  return [
    'bars_received'            => $recvMonth,
    'bars_used'                => $usedMonth,
    'bars_taken'               => $takenMonth,
    'bars_leftover_total'      => $leftGenMonth,
    'bars_leftover_consumed'   => $leftConsumedMonth,
    'bars_cub_available'       => $cubAvailReal,
    'bars_left_available'      => $leftAvailReal,
    'kg_received'              => $recvMonth * BAR_KG,
    'kg_used'                  => $usedMonth * BAR_KG,
    'kg_taken'                 => $takenMonth * BAR_KG,
    'kg_leftover_total'        => $leftGenMonth * BAR_KG,
    'kg_leftover_consumed'     => $leftConsumedMonth * BAR_KG,
    'kg_cub_available'         => $cubAvailReal * BAR_KG,
    'kg_left_available'        => $leftAvailReal * BAR_KG,
    'pct_cub_remaining'        => $recvMonth > 0 ? ($cubAvailReal / $recvMonth) * 100.0 : 0.0,
    'month_start'              => $monthStart,
    'month_end'                => $monthEnd,
  ];
}
ensure_cubing_schema($pdo);
ensure_cubing_return_schema($pdo);
$APP_TZ = get_location_timezone($pdo, $locationId);

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

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

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

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

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

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

    if ($action === 'shift_end') {
      $d = read_json();
      $date = (string)($d['date'] ?? tz_today($APP_TZ));
      $row = get_running_shift($pdo,$companyId,$locationId,(int)$u['id'],$date);
      if (!$row) jexit(['ok'=>false,'error'=>'No hay turno activo'], 409);

      $summary = [
        'entries' => 0,
        'output_qty' => 0.0,
        'bars_used' => 0.0,
        'bars_leftover' => 0.0,
        'kg_used' => 0.0,
        'kg_leftover' => 0.0,
      ];

      $bar = guess_bar_product($pdo,$companyId);
      if ($bar) {
        $st = $pdo->prepare("
          SELECT
            COUNT(*) AS entries,
            COALESCE(SUM(output_qty),0) AS output_qty,
            COALESCE(SUM(bars_used),0) AS bars_used,
            COALESCE(SUM(bars_leftover),0) AS bars_leftover
          FROM cubing_entries
          WHERE company_id=? AND location_id=? AND work_date=? AND shift=? AND input_product_id=?
        ");
        $st->execute([$companyId,$locationId,$date,(int)$row['shift'],(int)$bar['id']]);
        $s = $st->fetch(PDO::FETCH_ASSOC) ?: [];
        $summary['entries'] = (int)($s['entries'] ?? 0);
        $summary['output_qty'] = (float)($s['output_qty'] ?? 0);
        $summary['bars_used'] = (float)($s['bars_used'] ?? 0);
        $summary['bars_leftover'] = (float)($s['bars_leftover'] ?? 0);
        $summary['kg_used'] = $summary['bars_used'] * BAR_KG;
        $summary['kg_leftover'] = $summary['bars_leftover'] * BAR_KG;
      }

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

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

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

    if ($action === 'setup') {
      $bar = guess_bar_product($pdo,$companyId);
      if (!$bar) jexit(['ok'=>false,'error'=>'No pude detectar el producto Barra 150kg. Ajusta SKU/Name.'], 409);

      $leftWhId = ensure_leftover_warehouse($pdo, $companyId, $locationId);

      $st = $pdo->prepare("
        SELECT pc.to_product_id AS output_product_id,
               pc.factor AS output_per_input,
               p.sku, p.name, p.uom
        FROM product_conversions pc
        JOIN products p ON p.id = pc.to_product_id
        WHERE pc.company_id=? AND pc.from_product_id=? AND pc.active=1
          AND p.active=1
        ORDER BY p.sku
      ");
      $st->execute([$companyId,(int)$bar['id']]);
      $outs = $st->fetchAll(PDO::FETCH_ASSOC);

      foreach ($outs as &$o) {
        $modes = allowed_source_modes_for_output($o);
        $o['allowed_source_modes'] = $modes;
        $o['allows_leftover_source'] = count($modes) > 1 || in_array('SOB-CUB', $modes, true);
        $o['source_label'] = implode(' / ', $modes);
        $o['piece_kg'] = cubing_piece_kg_for_output($o);
        $o['recoverable_kg_per_bar'] = cubing_recoverable_kg_per_bar_for_output($o);
      }
      unset($o);

      jexit([
        'ok'=>true,
        'bar'=>$bar,
        'outputs'=>$outs,
        'leftover_wh_id'=>$leftWhId,
        'leftover_wh_code'=>WH_LEFT_CODE,
        'leftover_wh_name'=>WH_LEFT_NAME,
        'can_delete'=>$GLOBALS['isAdmin'] ?? false,
        'timezone'=>$APP_TZ
      ]);
    }

    if ($action === 'day_load') {
      $date = (string)($_GET['date'] ?? tz_today($APP_TZ));
      $barId = (int)($_GET['bar_product_id'] ?? 0);
      if ($barId<=0) jexit(['ok'=>false,'error'=>'Falta bar_product_id'], 400);

      $whInId   = wh_id_by_code($pdo,$companyId,$locationId, WH_IN_CODE);
      $whCf1Id  = wh_id_by_code($pdo,$companyId,$locationId, WH_CF1_CODE);
      $whOutId  = wh_id_by_code($pdo,$companyId,$locationId, WH_OUT_CODE);
      $whLeftId = ensure_leftover_warehouse($pdo,$companyId,$locationId);

      if ($whInId<=0 || $whOutId<=0 || $whLeftId<=0) {
        jexit(['ok'=>false,'error'=>'Config warehouses: falta CUB, CF3 o SOB-CUB'], 500);
      }

      $running = get_running_shift($pdo,$companyId,$locationId,(int)$u['id'],$date);

      /*
       * Vista histórica:
       * - shift vacío = automático: si hay turno activo muestra ese turno; si no, muestra todo el día.
       * - shift 0     = todos los turnos del día seleccionado.
       * - shift 1/2/3 = turno específico del día seleccionado.
       *
       * Esto no cambia la lógica de captura; solo permite consultar movimientos históricos por fecha/turno.
       */
      $shiftRaw = trim((string)($_GET['shift'] ?? ''));
      if ($shiftRaw === '') {
        $selectedShift = $running ? (int)$running['shift'] : 0;
      } else {
        $requestedShift = (int)$shiftRaw;
        $selectedShift = in_array($requestedShift, [1,2,3], true) ? $requestedShift : 0;
      }

      $stock = calc_cub_inventory($pdo,$companyId,$locationId,$barId,$whInId,$whLeftId,$date);
      $cf1Avail = $whCf1Id > 0 ? max(0.0, warehouse_stock_from_moves_as_of($pdo,$companyId,$locationId,$barId,$whCf1Id,$date)) : 0.0;
      $stock['bars_cf1_available'] = $cf1Avail;
      $stock['kg_cf1_available'] = $cf1Avail * BAR_KG;

      $totals = [];
      $entries = [];

      $shiftWhere = '';
      $shiftParams = [];
      if ($selectedShift > 0) {
        $shiftWhere = ' AND shift=?';
        $shiftParams[] = $selectedShift;
      }

      $st = $pdo->prepare("
        SELECT
          output_product_id,
          COALESCE(SUM(output_qty),0) AS out_qty,
          COALESCE(SUM(bars_used),0) AS bars_used,
          COALESCE(SUM(
            CASE
              WHEN source_mode='SOB-CUB' OR source_warehouse_id=? THEN 0
              WHEN bars_taken > 0 THEN bars_taken
              ELSE bars_used
            END
          ),0) AS bars_taken,
          COALESCE(SUM(bars_leftover),0) AS bars_leftover
        FROM cubing_entries
        WHERE company_id=? AND location_id=? AND work_date=? {$shiftWhere} AND input_product_id=?
        GROUP BY output_product_id
        ORDER BY output_product_id
      ");
      $st->execute(array_merge([$whLeftId, $companyId,$locationId,$date], $shiftParams, [$barId]));
      $totals = $st->fetchAll(PDO::FETCH_ASSOC);

      $st = $pdo->prepare("
        SELECT
          e.id, e.shift, e.entry_time, e.output_qty, e.bars_used,
          CASE
            WHEN e.source_mode='SOB-CUB' OR e.source_warehouse_id=? THEN 0
            ELSE COALESCE(NULLIF(e.bars_taken,0), e.bars_used)
          END AS bars_taken,
          COALESCE(e.bars_leftover,0) AS bars_leftover,
          COALESCE(e.source_mode, CASE WHEN e.source_warehouse_id=? THEN 'SOB-CUB' ELSE 'CUB' END) AS source_mode,
          p.sku, p.name AS product_name
        FROM cubing_entries e
        JOIN products p ON p.id = e.output_product_id
        WHERE e.company_id=? AND e.location_id=? AND e.work_date=? {$shiftWhere} AND e.input_product_id=?
        ORDER BY e.entry_time DESC, e.id DESC
        LIMIT 300
      ");
      $st->execute(array_merge([$whLeftId, $whLeftId, $companyId,$locationId,$date], $shiftParams, [$barId]));
      $entries = $st->fetchAll(PDO::FETCH_ASSOC);

      jexit([
        'ok'=>true,
        'running'=>$running,
        'selected_shift'=>$selectedShift,
        'wh_in_id'=>$whInId,
        'wh_out_id'=>$whOutId,
        'wh_left_id'=>$whLeftId,
        'wh_left_code'=>WH_LEFT_CODE,
        'wh_left_name'=>WH_LEFT_NAME,
        'stock'=>$stock,
        'stock_as_of_date'=>$date,
        'totals'=>$totals,
        'entries'=>$entries,
        'can_delete'=>$GLOBALS['isAdmin'] ?? false,
        'timezone'=>$APP_TZ
      ]);
    }

    if ($action === 'bar_return_preview') {
      $d = read_json();
      $date  = (string)($d['date'] ?? tz_today($APP_TZ));
      $barId = (int)($d['bar_product_id'] ?? 0);
      $qty   = (float)($d['qty_bars'] ?? 0);

      if ($barId <= 0) jexit(['ok'=>false,'error'=>'Producto barra inválido.'], 400);
      if ($qty <= 0 || abs($qty - round($qty)) > 0.0000001) {
        jexit(['ok'=>false,'error'=>'Captura una cantidad válida de barras completas.'], 400);
      }

      $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
      if (!$running) {
        jexit(['ok'=>false,'error'=>'Debes iniciar turno en la fecha seleccionada antes de regresar barras a CF1.'], 409);
      }

      $summary = cub_return_summary($pdo, $companyId, $locationId, $barId, $qty);
      $summary['work_date'] = $date;
      $summary['shift'] = (int)$running['shift'];
      jexit(['ok'=>true, 'summary'=>$summary]);
    }

    if ($action === 'bar_return_save') {
      $d = read_json();
      $date  = (string)($d['date'] ?? tz_today($APP_TZ));
      $barId = (int)($d['bar_product_id'] ?? 0);
      $qty   = (float)($d['qty_bars'] ?? 0);
      $notes = trim((string)($d['notes'] ?? ''));
      $eventTime = normalize_dt_local((string)($d['event_time'] ?? ''), $APP_TZ);

      if ($barId <= 0) jexit(['ok'=>false,'error'=>'Producto barra inválido.'], 400);
      if ($qty <= 0 || abs($qty - round($qty)) > 0.0000001) {
        jexit(['ok'=>false,'error'=>'Solo se pueden regresar barras completas a CF1.'], 400);
      }

      $running = get_running_shift($pdo, $companyId, $locationId, (int)$u['id'], $date);
      if (!$running) {
        jexit(['ok'=>false,'error'=>'Debes iniciar turno en la fecha seleccionada antes de regresar barras a CF1.'], 409);
      }

      $pdo->beginTransaction();
      try {
        $summary = cub_return_summary($pdo, $companyId, $locationId, $barId, $qty);
        $fullNotes = 'Reubicación de barra recibida de más en CUB hacia CF1';
        if ($notes !== '') $fullNotes .= ' | Motivo: ' . $notes;

        $st = $pdo->prepare("
          INSERT INTO cubing_bar_returns (
            company_id, location_id, work_date, shift, event_time, product_id,
            from_warehouse_id, to_warehouse_id, qty_bars, notes, created_by, created_at
          ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
        ");
        $st->execute([
          $companyId, $locationId, $date, (int)$running['shift'], $eventTime, $barId,
          (int)$summary['cub_warehouse_id'], (int)$summary['cf1_warehouse_id'],
          $qty, ($notes !== '' ? mb_substr($notes, 0, 255) : null), (int)$u['id']
        ]);
        $returnId = (int)$pdo->lastInsertId();

        create_cub_return_inventory_move(
          $pdo, $companyId, $locationId, (int)$summary['cub_warehouse_id'], $date, $barId,
          $qty, 'OUT', $returnId, $fullNotes . ' | Salida CUB', (int)$u['id']
        );
        create_cub_return_inventory_move(
          $pdo, $companyId, $locationId, (int)$summary['cf1_warehouse_id'], $date, $barId,
          $qty, 'IN', $returnId, $fullNotes . ' | Entrada CF1', (int)$u['id']
        );

        $pdo->commit();

        jexit([
          'ok'=>true,
          'return_id'=>$returnId,
          'summary'=>$summary,
          'message'=>'Movimiento registrado correctamente. Las barras salieron de CUB y regresaron a CF1.'
        ]);
      } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $e;
      }
    }

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

      $barId       = (int)($d['bar_product_id'] ?? 0);
      $outProduct  = (int)($d['output_product_id'] ?? 0);
      $outQty      = (float)($d['output_qty'] ?? 0);
      $entryTime   = normalize_dt_local((string)($d['entry_time'] ?? ''), $APP_TZ);
      $sourceMode  = strtoupper(trim((string)($d['source_mode'] ?? 'CUB')));
      if (!in_array($sourceMode, ['CUB','SOB-CUB','CF1'], true)) $sourceMode = 'CUB';

      if ($barId<=0 || $outProduct<=0) jexit(['ok'=>false,'error'=>'Producto inválido'], 400);
      if ($outQty<=0) jexit(['ok'=>false,'error'=>'Cantidad inválida'], 400);

      /*
       * Regla de inventario comercial:
       * Los productos terminados que entran a CF3 son PIEZAS, por lo tanto
       * no se permiten fracciones. Las fracciones solo deben existir en
       * BARRA 150 KILOS dentro de SOB-CUB por sobrante de cubicado.
       */
      if (abs($outQty - round($outQty)) > 0.0000001) {
        jexit([
          'ok'=>false,
          'error'=>'La cantidad a reportar debe ser una pieza entera. Las fracciones solo aplican para SOB-CUB.',
          'detail'=>[
            'output_qty'=>$outQty,
            'suggested_qty'=>round($outQty)
          ]
        ], 409);
      }
      $outQty = (float)round($outQty);

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

      $whInId   = wh_id_by_code($pdo,$companyId,$locationId, WH_IN_CODE);
      $whCf1Id  = wh_id_by_code($pdo,$companyId,$locationId, WH_CF1_CODE);
      $whOutId  = wh_id_by_code($pdo,$companyId,$locationId, WH_OUT_CODE);
      $whLeftId = ensure_leftover_warehouse($pdo,$companyId,$locationId);

      if ($whInId<=0 || $whOutId<=0 || $whLeftId<=0) {
        jexit(['ok'=>false,'error'=>'Config warehouses: falta CUB, CF3 o SOB-CUB'], 500);
      }

      $st = $pdo->prepare("
        SELECT pc.factor, p.sku, p.name
        FROM product_conversions pc
        JOIN products p ON p.id = pc.to_product_id
        WHERE pc.company_id=? AND pc.from_product_id=? AND pc.to_product_id=? AND pc.active=1
        LIMIT 1
      ");
      $st->execute([$companyId,$barId,$outProduct]);
      $row = $st->fetch(PDO::FETCH_ASSOC);
      $factor = (float)($row['factor'] ?? 0);
      if ($factor <= 0) jexit(['ok'=>false,'error'=>'No hay conversión configurada para este producto.'], 409);

      $allowedSourceModes = $row ? allowed_source_modes_for_output($row) : ['CUB'];
      if (!in_array($sourceMode, $allowedSourceModes, true)) {
        jexit([
          'ok'=>false,
          'error'=>'Origen no permitido para este artículo.',
          'detail'=>[
            'source_mode'=>$sourceMode,
            'allowed_source_modes'=>$allowedSourceModes,
            'product'=>(string)($row['sku'] ?? '') . ' · ' . (string)($row['name'] ?? '')
          ]
        ], 409);
      }

      $pieceKg = cubing_piece_kg_for_output($row);
      $recoverableKgPerBar = cubing_recoverable_kg_per_bar_for_output($row);

      // Cantidad física que realmente se convierte a producto terminado.
      // Si el artículo tiene peso por pieza, se usa kg reales / 150 kg.
      // Si no tiene regla especial, se conserva la lógica anterior qty / factor.
      $barsUsed = ($pieceKg > 0.0)
        ? (($outQty * $pieceKg) / BAR_KG)
        : ($outQty / $factor);

      $barsTaken    = 0.0;
      $barsLeftover = 0.0;
      $sourceWhId   = $whInId;

      $stock = calc_cub_inventory($pdo,$companyId,$locationId,$barId,$whInId,$whLeftId,$date);
      $cf1Avail = $whCf1Id > 0 ? max(0.0, warehouse_stock_from_moves_as_of($pdo,$companyId,$locationId,$barId,$whCf1Id,$date)) : 0.0;
      $stock['bars_cf1_available'] = $cf1Avail;
      $stock['kg_cf1_available'] = $cf1Avail * BAR_KG;

      if ($sourceMode === 'SOB-CUB') {
        $leftAvail = (float)$stock['bars_left_available'];

        if ($barsUsed - $leftAvail > 0.0000001) {
          jexit([
            'ok'=>false,
            'error'=>'No hay sobrante suficiente en SOB-CUB para esta conversión.',
            'detail'=>[
              'bars_left_available'=>$leftAvail,
              'bars_required'=>$barsUsed,
              'kg_left_available'=>$leftAvail * BAR_KG,
              'kg_required'=>$barsUsed * BAR_KG
            ]
          ], 409);
        }

        $barsTaken    = 0.0;
        $barsLeftover = 0.0;
        $sourceWhId   = $whLeftId;
      } else {
        // Barras completas tomadas desde CUB/CF1 se calculan por el factor operativo
        // (ej. 30 bolsas de 3 kg = 1 barra tomada). El sobrante recuperable es
        // 10 kg por cada barra completa tomada, no 10 kg por reportaje.
        $barsTaken = ($outQty > 0 && $factor > 0) ? ceil(($outQty / $factor) - 0.0000001) : 0.0;

        if ($recoverableKgPerBar > 0.0 && $barsTaken > 0.0) {
          $recoverableBars = ($barsTaken * $recoverableKgPerBar) / BAR_KG;
          $physicalRemainderBars = max(0.0, $barsTaken - $barsUsed);
          $barsLeftover = min($recoverableBars, $physicalRemainderBars);
        } else {
          $barsLeftover = max(0.0, $barsTaken - $barsUsed);
        }

        $sourceWhId   = ($sourceMode === 'CF1') ? $whCf1Id : $whInId;
        $sourceAvail  = ($sourceMode === 'CF1') ? (float)$stock['bars_cf1_available'] : (float)$stock['bars_cub_available'];

        if ($sourceWhId <= 0) {
          jexit(['ok'=>false,'error'=>'No existe el almacén origen ' . $sourceMode . ' para esta localidad.'], 409);
        }

        if ($barsTaken - $sourceAvail > 0.0000001) {
          jexit([
            'ok'=>false,
            'error'=>'No hay barras completas suficientes en ' . $sourceMode . ' para esta conversión.',
            'detail'=>[
              'source_mode'=>$sourceMode,
              'bars_available'=>$sourceAvail,
              'bars_taken_required'=>$barsTaken,
              'bars_used_real'=>$barsUsed,
              'bars_leftover_generated'=>$barsLeftover,
              'kg_available'=>$sourceAvail * BAR_KG,
              'kg_taken_required'=>$barsTaken * BAR_KG
            ]
          ], 409);
        }
      }

      $st = $pdo->prepare("
        INSERT INTO cubing_entries(
          company_id,location_id,work_date,shift,entry_time,
          input_product_id,output_product_id,output_qty,bars_used,bars_taken,bars_leftover,
          warehouse_id,source_warehouse_id,leftover_warehouse_id,source_mode,created_by
        )
        VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
      ");
      $st->execute([
        $companyId,$locationId,$date,$shift,$entryTime,
        $barId,$outProduct,$outQty,$barsUsed,$barsTaken,$barsLeftover,
        $whOutId,$sourceWhId,$whLeftId,$sourceMode,(int)$u['id']
      ]);
      $cubingEntryId = (int)$pdo->lastInsertId();

      $pdo->beginTransaction();
      try {
        // Movimientos físicos reales de cubicado:
        // 1) Sale barra desde CUB o SOB-CUB.
        // 2) Entra producto terminado a CF3.
        // 3) Si se tomó barra completa, el sobrante entra a SOB-CUB.
        $qtySourceOut = ($sourceMode === 'SOB-CUB') ? $barsUsed : $barsTaken;
        create_cubing_inventory_move(
          $pdo,
          $companyId,
          $locationId,
          $sourceWhId,
          $date,
          $barId,
          $qtySourceOut,
          'OUT',
          $cubingEntryId,
          'Cubicado: consumo de BARRA 150 KILOS desde ' . $sourceMode . ' | Output producto ID ' . $outProduct,
          (int)$u['id']
        );

        create_cubing_inventory_move(
          $pdo,
          $companyId,
          $locationId,
          $whOutId,
          $date,
          $outProduct,
          $outQty,
          'IN',
          $cubingEntryId,
          'Cubicado: entrada de producto terminado a CF3 | Origen ' . $sourceMode,
          (int)$u['id']
        );

        if ($sourceMode === 'CUB' && $barsLeftover > 0.0000001) {
          create_cubing_inventory_move(
            $pdo,
            $companyId,
            $locationId,
            $whLeftId,
            $date,
            $barId,
            $barsLeftover,
            'IN',
            $cubingEntryId,
            'Cubicado: sobrante generado hacia ' . WH_LEFT_CODE,
            (int)$u['id']
          );
        }

        $pdo->commit();
      } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        $pdo->prepare("DELETE FROM cubing_entries WHERE id=? AND company_id=? AND location_id=?")->execute([$cubingEntryId, $companyId, $locationId]);
        throw $e;
      }

      jexit([
        'ok'=>true,
        'bars_used'=>$barsUsed,
        'bars_taken'=>$barsTaken,
        'bars_leftover'=>$barsLeftover,
        'kg_used'=>$barsUsed * BAR_KG,
        'kg_taken'=>$barsTaken * BAR_KG,
        'kg_leftover'=>$barsLeftover * BAR_KG,
        'kg_product_real'=>$barsUsed * BAR_KG,
        'kg_recoverable'=>$barsLeftover * BAR_KG,
        'kg_serrin_no_recuperable'=>max(0.0, ($barsTaken - $barsUsed - $barsLeftover) * BAR_KG),
        'recoverable_kg_per_bar'=>$recoverableKgPerBar,
        'source_mode'=>$sourceMode
      ]);
    }

    if ($action === 'entry_delete') {
      if (!($GLOBALS['isAdmin'] ?? false)) {
        jexit(['ok'=>false,'error'=>'Solo un usuario administrador puede borrar capturas de cubicado.'], 403);
      }

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

      $pdo->beginTransaction();
      try {
        delete_cubing_inventory_moves($pdo, $companyId, $locationId, $id);
        $st = $pdo->prepare("DELETE FROM cubing_entries WHERE id=? AND company_id=? AND location_id=?");
        $st->execute([$id,$companyId,$locationId]);
        $pdo->commit();
        jexit(['ok'=>true]);
      } catch (Throwable $e) {
        if ($pdo->inTransaction()) $pdo->rollBack();
        throw $e;
      }
    }

    jexit(['ok'=>false,'error'=>'Acción no válida'], 404);
  } catch (Throwable $e) {
    jexit([
      'ok'    => false,
      'error' => 'SERVER_EXCEPTION',
      'msg'   => $e->getMessage(),
      'line'  => $e->getLine(),
      'file'  => basename($e->getFile()),
      'action'=> $action,
    ], 500);
  }
}

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

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

$st = $pdo->prepare("SELECT name FROM locations WHERE id=?");
$st->execute([$locationId]);
$locationName = (string)($st->fetchColumn() ?: '');
?>
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>BReport · Producción Cubicado</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:#f6f8fc;
      --bg-soft:#eef4fb;
      --card:#ffffff;
      --card-soft:#f8fbff;
      --line:#dbe5f1;
      --line-strong:#cbd8ea;
      --txt:#0f172a;
      --txt-soft:#334155;
      --muted:#64748b;
      --primary:#2563eb;
      --primary-2:#0ea5e9;
      --success:#16a34a;
      --success-soft:#dcfce7;
      --info:#0ea5e9;
      --warn:#d97706;
      --warn-soft:#fef3c7;
      --danger:#dc2626;
      --danger-soft:#fee2e2;
      --radius:22px;
      --shadow:0 18px 42px rgba(15,23,42,.10);
      --field-bg:#ffffff;
      --field-border:#cbd5e1;
      --field-focus:rgba(37,99,235,.35);
      --field-text:#0f172a;
    }

    html{color-scheme:light;}
    body{
      min-height:100vh;
      color:var(--txt);
      background:
        radial-gradient(circle at 10% 12%,rgba(37,99,235,.10),transparent 24%),
        radial-gradient(circle at 92% 85%,rgba(14,165,233,.12),transparent 26%),
        linear-gradient(135deg,#f8fafc 0%,#eef4fb 100%);
      font-family:Inter, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Arial, sans-serif;
    }
    .mono{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;}
    .topbar{
      background:rgba(255,255,255,.94);
      border-bottom:1px solid #e2e8f0;
      box-shadow:0 10px 28px rgba(15,23,42,.08);
      backdrop-filter:blur(12px);
      position:sticky;
      top:0;
      z-index:1000;
    }
    .navbar-brand{color:var(--txt)!important;font-weight:800;}
    .cardX{
      background:linear-gradient(180deg,#ffffff,#f8fbff);
      border:1px solid var(--line);
      border-radius:var(--radius);
      box-shadow:var(--shadow);
      color:var(--txt);
    }
    .cardInner{
      background:#ffffff;
      border:1px solid var(--line);
      border-radius:20px;
      box-shadow:0 10px 24px rgba(15,23,42,.06);
    }
    .muted{color:var(--muted)!important;}
    .pill{
      border:1px solid #bfdbfe;
      border-radius:999px;
      padding:.34rem .76rem;
      background:#eff6ff;
      color:#1e40af;
      font-weight:700;
    }
    .badge-source{
      font-size:.72rem;
      border-radius:999px;
      padding:.25rem .62rem;
      border:1px solid #bfdbfe;
      background:#dbeafe;
      color:#1d4ed8;
      display:inline-flex;
      align-items:center;
      gap:.3rem;
      font-weight:800;
    }
    .btn-accent{
      background:linear-gradient(135deg,var(--primary),var(--primary-2));
      border:0;
      color:#fff;
      font-weight:800;
      border-radius:14px;
      box-shadow:0 12px 24px rgba(37,99,235,.22);
    }
    .btn-accent:hover{filter:brightness(1.04); color:#fff;}
    .btn-ic{display:inline-flex; align-items:center; gap:.45rem;}
    .btn-outline-light,
    .btn-outline-danger,
    .btn-outline-success{
      border-radius:14px;
      font-weight:800;
      background:#fff;
    }
    .btn-outline-light{
      color:#1e293b!important;
      border-color:#cbd5e1!important;
    }
    .btn-outline-light:hover{
      color:#0f172a!important;
      background:#f1f5f9!important;
      border-color:#94a3b8!important;
    }
    .btn-outline-danger{
      color:#b91c1c!important;
      border-color:#fecaca!important;
    }
    .btn-outline-danger:hover{
      color:#fff!important;
      background:#dc2626!important;
      border-color:#dc2626!important;
    }
    .btn-outline-success{
      color:#166534!important;
      border-color:#bbf7d0!important;
    }
    .btn-outline-success:hover{
      color:#fff!important;
      background:#16a34a!important;
      border-color:#16a34a!important;
    }
    .kpiTitle{font-size:.92rem; color:var(--muted);font-weight:800;}
    .kpiBig{font-size:2rem; font-weight:900; letter-spacing:.3px; line-height:1.1;color:var(--txt);}
    .kpiSub{font-size:.88rem; color:var(--muted);}
    .availabilityHero{
      position:relative;
      overflow:hidden;
      background:
        radial-gradient(circle at 82% 18%, rgba(37,99,235,.16), transparent 28%),
        linear-gradient(135deg,#ffffff 0%,#f8fbff 100%);
      border:1px solid var(--line);
      border-radius:24px;
      box-shadow:0 14px 34px rgba(15,23,42,.08);
    }
    .availabilityHero.actionable{
      cursor:pointer;
      transition:transform .12s ease, box-shadow .12s ease;
    }
    .availabilityHero.actionable:hover{
      transform:translateY(-1px);
      box-shadow:0 18px 36px rgba(37,99,235,.12);
    }
    .availabilityHero.actionable:focus{
      outline:3px solid rgba(37,99,235,.18);
      outline-offset:2px;
    }
    .returnHint{
      border-top:1px dashed #dbeafe;
      margin-top:.8rem;
      padding-top:.72rem;
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:.7rem;
      flex-wrap:wrap;
    }
    .availabilityHero.ok{border-color:#bbf7d0;background:linear-gradient(135deg,#ffffff 0%,#f0fdf4 100%);}
    .availabilityHero.low{border-color:#fde68a;background:linear-gradient(135deg,#ffffff 0%,#fffbeb 100%);}
    .availabilityHero.critical{border-color:#fecaca;background:linear-gradient(135deg,#ffffff 0%,#fef2f2 100%);}
    .availabilityStatus{
      border-radius:999px;
      padding:.35rem .72rem;
      font-size:.78rem;
      font-weight:900;
      display:inline-flex;
      align-items:center;
      gap:.35rem;
      border:1px solid #bfdbfe;
      background:#eff6ff;
      color:#1e40af;
    }
    .availabilityHero.ok .availabilityStatus{border-color:#bbf7d0;background:#dcfce7;color:#166534;}
    .availabilityHero.low .availabilityStatus{border-color:#fde68a;background:#fef3c7;color:#92400e;}
    .availabilityHero.critical .availabilityStatus{border-color:#fecaca;background:#fee2e2;color:#991b1b;}
    .meter{
      height:12px;
      background:#e2e8f0;
      border-radius:999px;
      overflow:hidden;
      border:1px solid rgba(148,163,184,.45);
    }
    .meter > span{
      display:block;
      height:100%;
      width:0%;
      background:linear-gradient(90deg,var(--primary),var(--primary-2));
      border-radius:999px;
      transition:width .25s ease;
    }
    .availabilityHero.ok .meter > span{background:linear-gradient(90deg,#16a34a,#22c55e);}
    .availabilityHero.low .meter > span{background:linear-gradient(90deg,#d97706,#f59e0b);}
    .availabilityHero.critical .meter > span{background:linear-gradient(90deg,#dc2626,#ef4444);}
    .miniMetric{
      background:#fff;
      border:1px solid var(--line);
      border-radius:18px;
      padding:1rem;
      box-shadow:0 10px 24px rgba(15,23,42,.05);
    }

    .form-label{font-weight:800;color:#334155;}
    .form-control, .form-select,
    .modal .form-control, .modal .form-select{
      background:var(--field-bg) !important;
      border:1px solid var(--field-border) !important;
      color:var(--field-text) !important;
      border-radius:15px;
      caret-color:var(--field-text);
      min-height:44px;
      box-shadow:none!important;
    }
    .form-control::placeholder{color:#94a3b8 !important; opacity:1;}
    .form-control:focus, .form-select:focus,
    .modal .form-control:focus, .modal .form-select:focus{
      background:#fff !important;
      border-color:#60a5fa !important;
      box-shadow:0 0 0 .22rem rgba(37,99,235,.12) !important;
      color:var(--field-text) !important;
    }
    .form-select option{
      background:#fff !important;
      color:#0f172a !important;
    }

    input[type="date"],
    input[type="datetime-local"],
    input[type="number"]{
      color-scheme:light;
      color:var(--field-text) !important;
      background:var(--field-bg) !important;
    }
    input[type="date"]::-webkit-calendar-picker-indicator,
    input[type="datetime-local"]::-webkit-calendar-picker-indicator{
      filter:none;
      opacity:.9;
      cursor:pointer;
    }

    .tableWrap{
      background:#fff;
      border:1px solid var(--line);
      border-radius:18px;
      overflow:hidden;
      box-shadow:0 10px 24px rgba(15,23,42,.05);
    }
    .table{
      --bs-table-bg:#fff !important;
      --bs-table-color:#0f172a !important;
      --bs-table-border-color:#e2e8f0 !important;
      --bs-table-hover-bg:#f8fafc !important;
      --bs-table-hover-color:#0f172a !important;
      color:#0f172a!important;
      margin-bottom:0;
      background:#fff!important;
    }
    .table thead,.table tbody,.table tr,.table th,.table td{
      color:#0f172a!important;
      box-shadow:none!important;
    }
    .table thead th{
      background:#eff6ff!important;
      color:#0f172a!important;
      border-color:#dbe5f1!important;
      font-weight:900;
      white-space:nowrap;
      padding:.9rem .8rem;
    }
    .table td{
      background:#fff!important;
      border-color:#e2e8f0!important;
      vertical-align:middle;
      padding:.82rem .8rem;
    }
    .table tbody tr:hover td{
      background:#f8fafc!important;
    }
    .stickyHead thead th{
      position:sticky;
      top:0;
      z-index:2;
      background:#eff6ff!important;
    }
    .divider{height:1px; background:#e2e8f0; margin:16px 0;}
    .modal-content.darkModal{
      background:#ffffff;
      border:1px solid var(--line);
      color:var(--txt);
      border-radius:26px;
      box-shadow:0 24px 60px rgba(15,23,42,.18);
    }
    .modal-header, .modal-footer{border-color:#e2e8f0!important;}
    .modal-title{font-weight:900;color:#0f172a;}
    .btn-close-white{filter:none!important;}
    .alert-info{
      background:#eff6ff;
      color:#1e3a8a;
      border:1px solid #bfdbfe;
      border-radius:16px;
    }
    .text-bg-secondary{
      background:#e2e8f0!important;
      color:#334155!important;
      border:1px solid #cbd5e1;
    }
    .text-bg-success{
      background:#dcfce7!important;
      color:#166534!important;
      border:1px solid #bbf7d0;
    }
    .hidden{display:none!important;}
    @media(max-width:768px){
      .kpiBig{font-size:1.55rem;}
      .cardX{border-radius:18px;}
    }
  </style>
</head>
<body>

<nav class="navbar topbar">
  <div class="container-fluid px-3">
    <div class="d-flex align-items-center gap-2 flex-wrap">
      <span class="navbar-brand fw-bold mb-0" style="color:var(--txt);">BReport · Producción Cubicado</span>
      <span class="pill small"><?=h($companyName)?> · <?=h($locationName)?></span>
      <span class="pill small"><i class="bi bi-clock-history"></i> <?=h($APP_TZ)?></span>
    </div>
    <div class="ms-auto d-flex gap-2 align-items-center flex-wrap">
      <a class="btn btn-sm btn-outline-light btn-ic" href="produccion.php"><i class="bi bi-arrow-left"></i>Producción Barras</a>
      <a class="btn btn-sm btn-outline-light btn-ic" href="dashboard.php"><i class="bi bi-speedometer2"></i>Dashboard</a>
      <a class="btn btn-sm btn-outline-danger btn-ic" href="logout.php"><i class="bi bi-box-arrow-right"></i>Salir</a>
    </div>
  </div>
</nav>

<div class="container-fluid px-3 py-3">
  <div class="row g-3">

    <div class="col-12 col-xl-4">
      <div class="cardX p-3 h-100">
        <div class="d-flex align-items-start justify-content-between gap-2 flex-wrap">
          <div>
            <div class="fw-bold fs-4">Control de cubicado</div>
            <div class="muted small">
              CUB maneja barras completas. El sobrante generado por conversión se manda a <b><?=h(WH_LEFT_CODE)?></b>.
            </div>
          </div>
        </div>

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

          <div class="col-12">
            <label class="form-label small muted mb-1">Consultar movimientos</label>
            <select class="form-select" id="shiftViewSel">
              <option value="">Automático · turno activo / todo el día</option>
              <option value="0">Todos los turnos del día</option>
              <option value="1">Turno 1</option>
              <option value="2">Turno 2</option>
              <option value="3">Turno 3</option>
            </select>
            <div class="muted small mt-1" id="viewHint">Selecciona fecha y turno para revisar históricos.</div>
          </div>

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

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

        <div class="availabilityHero p-3 actionable" id="availabilityCard" role="button" tabindex="0" title="Regresar barras completas de CUB a CF1">
          <div class="d-flex align-items-start justify-content-between gap-2 flex-wrap">
            <div>
              <div class="kpiTitle">Disponible para cubicado</div>
              <div class="kpiBig mono" id="barsCubAvail">0.00</div>
              <div class="kpiSub mono" id="kgCubAvail">0.00 kg en CUB</div>
            </div>
            <span class="availabilityStatus" id="availabilityStatus"><i class="bi bi-circle-fill"></i> Sin evaluar</span>
          </div>
          <div class="meter mt-3" title="Nivel visual de disponibilidad">
            <span id="availabilityBar"></span>
          </div>
          <div class="muted small mt-2" id="availabilityNote"></div>
          <div class="returnHint">
            <span class="muted small"><i class="bi bi-arrow-return-left"></i> ¿Se enviaron barras de más a CUB?</span>
            <button type="button" class="btn btn-sm btn-outline-success btn-ic" id="btnReturnBars">
              <i class="bi bi-arrow-left-right"></i> Regresar a CF1
            </button>
          </div>
        </div>

        <div class="row g-3 mt-1">
          <div class="col-12 col-md-6">
            <div class="miniMetric h-100">
              <div class="kpiTitle">Sobrante disponible</div>
              <div class="kpiBig mono" id="barsLeftAvail">0.00</div>
              <div class="kpiSub mono" id="kgLeftAvail">0.00 kg en <?=h(WH_LEFT_CODE)?></div>
            </div>
          </div>

          <div class="col-12 col-md-6">
            <div class="miniMetric h-100">
              <div class="kpiTitle">Piezas reportadas turno</div>
              <div class="kpiBig mono" id="shiftOutput">0</div>
              <div class="kpiSub">Solo turno activo</div>
            </div>
          </div>

          <div class="col-12 col-md-6">
            <div class="miniMetric h-100">
              <div class="kpiTitle">Barras usadas turno</div>
              <div class="kpiBig mono" id="shiftUsed">0.00</div>
              <div class="kpiSub">Consumo real del turno</div>
            </div>
          </div>

          <div class="col-12 col-md-6">
            <div class="miniMetric h-100">
              <div class="kpiTitle">Sobrante generado turno</div>
              <div class="kpiBig mono" id="shiftLeftTotal">0.00</div>
              <div class="kpiSub">Se envía a <?=h(WH_LEFT_CODE)?></div>
            </div>
          </div>
        </div>

        <div class="divider"></div>
        <div class="muted small" id="hintLeft"></div>
      </div>
    </div>

    <div class="col-12 col-xl-8">

      <div class="cardX p-3">
        <div class="d-flex align-items-center justify-content-between gap-2 flex-wrap">
          <div>
            <div class="fw-semibold fs-5">Productos de cubicado</div>
            <div class="muted small" id="productViewSub">Consulta por fecha y turno.</div>
          </div>
          <span class="pill mono" id="prodCount">0 productos</span>
        </div>

        <div class="tableWrap mt-3">
          <div class="table-responsive stickyHead">
            <table class="table table-sm align-middle mb-0">
              <thead>
                <tr>
                  <th>Producto</th>
                  <th class="text-end mono">Factor</th>
                  <th class="text-end mono">Producido turno</th>
                  <th class="text-end mono">Barras usadas</th>
                  <th class="text-end mono">Sobrante</th>
                  <th class="text-end">Acción</th>
                </tr>
              </thead>
              <tbody id="prodBody"></tbody>
            </table>
          </div>
        </div>
      </div>

      <div class="cardX p-3 mt-3">
        <div class="d-flex align-items-center justify-content-between gap-2 flex-wrap">
          <div>
            <div class="fw-semibold fs-5">Bitácora del turno</div>
            <div class="muted small" id="logViewSub">Consulta por fecha y turno.</div>
          </div>
          <span class="pill mono" id="logCount">0 registros</span>
        </div>

        <div class="tableWrap mt-3">
          <div class="table-responsive stickyHead">
            <table class="table table-sm align-middle mb-0">
              <thead>
                <tr>
                  <th>Hora</th>
                  <th>Turno</th>
                  <th>Producto</th>
                  <th>Origen</th>
                  <th class="text-end mono">Piezas</th>
                  <th class="text-end mono">Barras usadas</th>
                  <th class="text-end mono">Sobrante</th>
                  <th class="text-end mono">Kg usados</th>
                  <th></th>
                </tr>
              </thead>
              <tbody id="logBody"></tbody>
            </table>
          </div>
        </div>
      </div>

    </div>
  </div>
</div>

<div class="modal fade" id="mdlShift" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-play-fill"></i> Iniciar turno</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <label class="form-label small muted">Turno</label>
        <select class="form-select" id="shiftSel">
          <option value="1">Turno 1 (07:00 - 15:00)</option>
          <option value="2">Turno 2 (15:00 - 22:30)</option>
          <option value="3">Turno 3 (22:30 - 07:00)</option>
        </select>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-accent" id="doShiftStart">Iniciar</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdlAdd" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-plus-circle"></i> Reportar cubicado</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-info">
          La <b>cantidad reportada</b> es la cantidad real de piezas que entrará a inventario comercial en <b>CF3</b>.
          El sistema solo calcula barras usadas, barras tomadas y sobrante para afectar <b>CUB / SOB-CUB</b>.
        </div>

        <div class="fw-semibold" id="mTitle">Producto</div>
        <div class="muted small mono" id="mSku"></div>

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

          <div class="col-12 hidden" id="srcWrap">
            <label class="form-label small muted">Tomar hielo desde</label>
            <select class="form-select" id="mSourceMode">
              <option value="CUB">CUB · Barra completa</option>
                            <option value="SOB-CUB"><?=h(WH_LEFT_CODE)?> · Sobrante de cubicado</option>
            </select>
            <div class="muted small mt-1">Disponible solo para productos con regla especial de origen.</div>
          </div>

          <div class="col-12">
            <label class="form-label small muted">Piezas a reportar / agregar a CF3</label>
            <input type="number" step="1" min="1" class="form-control mono" id="mQty" value="0" placeholder="Cantidad real capturada por usuario">
            <div class="muted small mt-1">Esta cantidad se guarda exactamente como producto terminado disponible. No se calcula desde barras.</div>
          </div>
        </div>

        <div class="cardInner p-3 mt-3">
          <div class="d-flex justify-content-between"><span class="muted">Piezas que entran a CF3</span><b class="mono" id="mPiecesCf3">0</b></div>
          <div class="d-flex justify-content-between mt-2"><span class="muted">Barras completas a tomar</span><b class="mono" id="mBarsTaken">0.00</b></div>
          <div class="d-flex justify-content-between mt-2"><span class="muted">Producto aprovechado</span><b class="mono"><span id="mKgUsed">0.00</span> kg</b></div>
          <div class="d-flex justify-content-between mt-2"><span class="muted">Recuperable a SOB-CUB</span><b class="mono"><span id="mKgRecoverable">0.00</span> kg</b></div>
          <div class="d-flex justify-content-between mt-2"><span class="muted">Serrín/no recuperable</span><b class="mono"><span id="mKgScrap">0.00</span> kg</b></div>
          <span class="d-none" id="mSourceLbl">CUB</span>
          <span class="d-none" id="mBarsUsed">0.00</span>
          <span class="d-none" id="mBarsLeft">0.00</span>
          <span class="d-none" id="mBarsAvailCub">0.00</span>
          <span class="d-none" id="mBarsAvailLeft">0.00</span>
          <span class="d-none" id="mKgAvailCub">0.00</span>
          <span class="d-none" id="mKgAvailLeft">0.00</span>
        </div>

        <div class="muted small mt-2" id="mWarn"></div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-accent" id="mSave">Guardar</button>
      </div>
    </div>
  </div>
</div>


<div class="modal fade" id="mdlReturnBars" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-arrow-left-right"></i> Regresar barras de CUB a CF1</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-info">
          Esta opción se utiliza cuando se enviaron <b>barras completas de más</b> a CUB.
          El movimiento no borra registros de producción: registra una salida de <b>CUB</b> y una entrada a <b>CF1</b>.
        </div>
        <div class="cardInner p-3 mb-3">
          <div class="d-flex justify-content-between"><span class="muted">Disponible actual en CUB</span><b class="mono"><span id="rCubAvailable">0.00</span> barras</b></div>
          <div class="d-flex justify-content-between mt-2"><span class="muted">Disponible actual en CF1</span><b class="mono"><span id="rCf1Available">0.00</span> barras</b></div>
        </div>
        <div class="mb-3">
          <label class="form-label small muted">Barras completas a regresar a CF1</label>
          <input type="number" step="1" min="1" class="form-control mono" id="rQty" value="1">
          <div class="muted small mt-1">Solo se permiten barras completas. No afecta sobrante ni producto ya cubicado.</div>
        </div>
        <div class="mb-0">
          <label class="form-label small muted">Comentario / motivo</label>
          <textarea class="form-control" id="rNotes" rows="2" maxlength="255" placeholder="Ejemplo: se enviaron barras de más a cubicado"></textarea>
        </div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-accent" id="rPreview"><i class="bi bi-check2-square"></i> Revisar movimiento</button>
      </div>
    </div>
  </div>
</div>

<div class="modal fade" id="mdlReturnConfirm" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-dialog-centered">
    <div class="modal-content darkModal">
      <div class="modal-header">
        <h5 class="modal-title"><i class="bi bi-check-circle"></i> Confirmar movimiento</h5>
        <button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-info">
          Confirma la reubicación de <b><span id="rcQty">0</span> barras</b>
          (<span id="rcKg">0.00</span> kg) de <b>CUB</b> hacia <b>CF1</b>.
        </div>
        <div class="tableWrap">
          <table class="table align-middle">
            <thead>
              <tr>
                <th>Almacén</th>
                <th class="text-end">Anterior</th>
                <th class="text-end">Movimiento</th>
                <th class="text-end">Actual</th>
              </tr>
            </thead>
            <tbody>
              <tr>
                <td><b>CUB</b></td>
                <td class="text-end mono" id="rcCubBefore">0.00</td>
                <td class="text-end mono text-danger" id="rcCubMove">-0.00</td>
                <td class="text-end mono fw-bold" id="rcCubAfter">0.00</td>
              </tr>
              <tr>
                <td><b>CF1</b></td>
                <td class="text-end mono" id="rcCf1Before">0.00</td>
                <td class="text-end mono text-success" id="rcCf1Move">+0.00</td>
                <td class="text-end mono fw-bold" id="rcCf1After">0.00</td>
              </tr>
            </tbody>
          </table>
        </div>
        <div class="muted small mt-2">
          Se generará trazabilidad con tipo de movimiento <b>CUB_RETURN</b> en inventario.
        </div>
      </div>
      <div class="modal-footer">
        <button class="btn btn-outline-light" id="rcBack">Regresar</button>
        <button class="btn btn-accent" id="rcSave"><i class="bi bi-check-lg"></i> Confirmar y guardar</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 BAR_KG = 150.0;
const APP_TZ = <?=json_encode($APP_TZ)?>;
const WH_LEFT_CODE = <?=json_encode(WH_LEFT_CODE)?>;

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

function pieceKgForOutput(out){
  const txt = `${out?.sku || ''} ${out?.name || ''}`.toUpperCase();
  if (txt.includes('1/12') || txt.includes('1 / 12')) return 0;
  if (txt.includes('1/4') || txt.includes('1 / 4')) return 0;
  if (txt.includes('3KG') || txt.includes('3 KG')) return 3;
  if (txt.includes('5KG') || txt.includes('5 KG')) return 5;
  const isTriturado = txt.includes('TRIT') || txt.includes('TRITURADO');
  if (isTriturado && (txt.includes('10KG') || txt.includes('10 KG'))) return 10;
  return 0;
}
function recoverableKgPerBarForOutput(out){
  return pieceKgForOutput(out) > 0 ? 10 : 0;
}

async function parseResponse(r, action){
  const txt = await r.text();
  let data = null;
  try { data = JSON.parse(txt); }
  catch(e){
    console.error('Respuesta no JSON:', txt);
    return { ok:false, error:'NO_JSON', action, http_status:r.status, raw:txt };
  }
  if (!r.ok) {
    data.ok = false;
    data.http_status = r.status;
    data.action = action;
  }
  return data;
}

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(), {
    method:'GET',
    credentials:'same-origin',
    headers:{'Accept':'application/json'}
  });
  return parseResponse(r, action);
}

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',
      'Accept':'application/json',
      'X-CSRF':CSRF
    },
    body: JSON.stringify(payload || {})
  });
  return parseResponse(r, action);
}

function showErr(j, fallback='Error'){
  const parts = [];
  if (j?.error) parts.push(j.error);
  if (j?.msg) parts.push(j.msg);
  if (j?.action) parts.push('Acción: ' + j.action);
  if (j?.http_status) parts.push('HTTP: ' + j.http_status);
  const text = parts.length ? parts.join('\n') : fallback;
  alert(text);
  console.error('API error:', j);
}

let bar = null;
let outputs = [];
let running = null;
let selectedShift = 0;
let totalsByOut = new Map();
let entries = [];
let stock = {
  bars_received:0,
  bars_used:0,
  bars_taken:0,
  bars_leftover_total:0,
  bars_leftover_consumed:0,
  bars_cub_available:0,
  bars_left_available:0,
  bars_cf1_available:0,
  kg_received:0,
  kg_used:0,
  kg_taken:0,
  kg_leftover_total:0,
  kg_leftover_consumed:0,
  kg_cub_available:0,
  kg_left_available:0,
  kg_cf1_available:0,
  pct_cub_remaining:0
};

let modalAdd;
let modalShift;
let modalReturnBars;
let modalReturnConfirm;
let pendingReturn = null;
let curOut = null;
let canDeleteEntries = false;

function setNowLocal(id){
  const d = new Date();
  const pad = (n)=>String(n).padStart(2,'0');
  const s = d.getFullYear()+'-'+pad(d.getMonth()+1)+'-'+pad(d.getDate())+'T'+pad(d.getHours())+':'+pad(d.getMinutes());
  $(id).value = s;
}

function calcShiftSummary(){
  const s = {entries: entries.length, output_qty:0, bars_used:0, bars_leftover:0, kg_used:0, kg_leftover:0};
  for (const e of entries){
    s.output_qty += num(e.output_qty);
    s.bars_used += num(e.bars_used);
    s.bars_leftover += num(e.bars_leftover);
  }
  s.kg_used = s.bars_used * BAR_KG;
  s.kg_leftover = s.bars_leftover * BAR_KG;
  return s;
}

function selectedDate(){
  return $('datePick')?.value || '';
}

function selectedShiftLabel(){
  return selectedShift > 0 ? `Turno ${selectedShift}` : 'Todos los turnos';
}

function isViewingActiveShift(){
  return !!running
    && selectedShift > 0
    && parseInt(running.shift || 0, 10) === selectedShift
    && String(running.work_date || '') === selectedDate();
}

function inventoryLevel(available){
  available = num(available);
  if (available <= 5) return {cls:'critical', label:'Crítico', icon:'bi-exclamation-octagon-fill', note:'Quedan muy pocas barras disponibles para cubicado. Revisa producción o recepción antes de continuar.'};
  if (available <= 15) return {cls:'low', label:'Bajo', icon:'bi-exclamation-triangle-fill', note:'El inventario disponible para cubicado está bajo. Conviene programar más barras.'};
  return {cls:'ok', label:'Disponible', icon:'bi-check-circle-fill', note:'Inventario suficiente para continuar el cubicado.'};
}

function applyShiftUI(){
  const has = !!running;
  const viewingActive = isViewingActiveShift();
  const viewLabel = selectedShiftLabel();
  const date = selectedDate();

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

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

  if ($('viewHint')) {
    $('viewHint').textContent = `Vista actual: ${date} · ${viewLabel}. ${viewingActive ? 'Captura habilitada.' : 'Solo consulta histórica.'}`;
  }
  if ($('productViewSub')) {
    $('productViewSub').textContent = `Totales consultados: ${date} · ${viewLabel}.`;
  }
  if ($('logViewSub')) {
    $('logViewSub').textContent = `Movimientos consultados: ${date} · ${viewLabel}.`;
  }

  $('hintLeft').innerHTML = viewingActive
    ? `Turno activo: puedes reportar cubicado. Las barras usadas y el sobrante generado se acumulan en este turno.`
    : `<i class="bi bi-info-circle"></i> Vista histórica: puedes consultar movimientos por fecha y turno. Para capturar, selecciona el turno activo del día.`;
}
function renderKpis(){
  const shift = calcShiftSummary();
  const cubAvail = num(stock.bars_cub_available);
  const leftAvail = num(stock.bars_left_available);
  const level = inventoryLevel(cubAvail);

  $('availabilityCard').classList.remove('ok','low','critical');
  $('availabilityCard').classList.add(level.cls);
  $('availabilityStatus').innerHTML = `<i class="bi ${level.icon}"></i> ${level.label}`;
  $('availabilityNote').textContent = level.note;

  const maxVisual = Math.max(25, cubAvail);
  const pct = Math.max(0, Math.min(100, (cubAvail / maxVisual) * 100));
  $('availabilityBar').style.width = `${pct}%`;

  $('barsCubAvail').textContent = fmt2(cubAvail);
  const stockDateLabel = selectedDate() ? ` · corte ${selectedDate()}` : '';
  $('kgCubAvail').textContent = `${fmt2(stock.kg_cub_available)} kg en CUB${stockDateLabel}`;
  $('barsLeftAvail').textContent = fmt2(leftAvail);
  $('kgLeftAvail').textContent = `${fmt2(stock.kg_left_available)} kg en ${WH_LEFT_CODE}${stockDateLabel}`;

  $('shiftOutput').textContent = fmtPz(shift.output_qty);
  $('shiftUsed').textContent = fmt2(shift.bars_used);
  $('shiftLeftTotal').textContent = fmt2(shift.bars_leftover);

  const canReturn = isViewingActiveShift() && cubAvail >= 1;
  if ($('btnReturnBars')) {
    $('btnReturnBars').disabled = !canReturn;
    $('btnReturnBars').title = canReturn
      ? 'Regresar barras completas de CUB a CF1'
      : 'Inicia/selecciona el turno activo y valida disponibilidad en CUB';
  }
  $('availabilityCard').classList.toggle('actionable', canReturn);
  $('availabilityCard').setAttribute('aria-disabled', canReturn ? 'false' : 'true');
}

function renderProducts(){
  const tb = $('prodBody');
  tb.innerHTML = '';
  $('prodCount').textContent = `${outputs.length} productos`;

  if (!outputs.length){
    tb.innerHTML = `<tr><td colspan="6" class="muted">No hay conversiones configuradas.</td></tr>`;
    return;
  }

  for(const o of outputs){
    const outId = String(o.output_product_id);
    const factor = num(o.output_per_input);
    const agg = totalsByOut.get(outId) || {out_qty:0, bars_used:0, bars_taken:0, bars_leftover:0};

    const out_qty = num(agg.out_qty);
    const bars_used = num(agg.bars_used);
    const bars_leftover = num(agg.bars_leftover);

    const sourceModes = Array.isArray(o.allowed_source_modes) && o.allowed_source_modes.length ? o.allowed_source_modes : ['CUB'];
    const canReport = isViewingActiveShift() && sourceModes.some(mode => {
      if (mode === 'SOB-CUB') return num(stock.bars_left_available) > 0.000001;
      if (mode === 'CF1') return num(stock.bars_cf1_available) > 0.000001;
      return num(stock.bars_cub_available) > 0.000001;
    });

    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td>
          <div class="fw-semibold">${h(o.name)}</div>
          <div class="muted small mono">${h(o.sku)}</div>
          ${o.allows_leftover_source ? `<div class="mt-1"><span class="badge-source"><i class="bi bi-layers"></i> Permite ${h(o.source_label || (Array.isArray(o.allowed_source_modes) ? o.allowed_source_modes.join(' / ') : 'CUB / SOB-CUB'))}</span></div>` : ''}
        </td>
        <td class="mono text-end">${factor.toFixed(3)}</td>
        <td class="mono text-end">${fmtPz(out_qty)}</td>
        <td class="mono text-end">${fmt2(bars_used)}</td>
        <td class="mono text-end">${fmt2(bars_leftover)}</td>
        <td class="text-end">
          <button class="btn btn-sm btn-accent" data-report="${h(outId)}" ${canReport?'':'disabled'}>
            <i class="bi bi-plus-circle"></i> Reportar
          </button>
        </td>
      </tr>
    `);
  }
}

function renderLog(){
  const tb = $('logBody');
  tb.innerHTML = '';
  $('logCount').textContent = `${entries.length} registros`;

  if (!entries.length){
    tb.innerHTML = `<tr><td colspan="9" class="muted">Sin capturas para la fecha y turno seleccionados.</td></tr>`;
    return;
  }

  for(const e of entries){
    const t = String(e.entry_time||'').replace('T',' ');
    const barsUsed = num(e.bars_used);
    const barsLeft = num(e.bars_leftover);
    const kgUsed = barsUsed * BAR_KG;

    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="mono">${h(t)}</td>
        <td><span class="badge text-bg-secondary">T${h(e.shift || '')}</span></td>
        <td>
          <div class="fw-semibold">${h(e.product_name)}</div>
          <div class="muted small mono">${h(e.sku||'')}</div>
        </td>
        <td><span class="badge-source">${h(e.source_mode || 'CUB')}</span></td>
        <td class="mono text-end">${fmtPz(e.output_qty)}</td>
        <td class="mono text-end">${fmt2(barsUsed)}</td>
        <td class="mono text-end">${fmt2(barsLeft)}</td>
        <td class="mono text-end">${fmt2(kgUsed)}</td>
        <td class="text-end">
          ${canDeleteEntries ? `<button class="btn btn-sm btn-outline-danger" data-del="${h(e.id)}">Borrar</button>` : '<span class="muted small">Solo admin</span>'}
        </td>
      </tr>
    `);
  }
}

async function loadSetup(){
  const j = await apiPost('setup', {});
  if(!j.ok){ showErr(j, 'Error setup'); return false; }
  bar = j.bar;
  outputs = j.outputs || [];
  canDeleteEntries = !!j.can_delete;
  return true;
}

async function loadDay(){
  if (!bar?.id) return;

  const date = $('datePick').value;
  const params = {date, bar_product_id: bar.id};
  const shiftView = $('shiftViewSel') ? $('shiftViewSel').value : '';
  if (shiftView !== '') params.shift = shiftView;
  const j = await apiGet('day_load', params);
  if(!j.ok){ showErr(j, 'Error día'); return; }

  running = j.running || null;
  selectedShift = parseInt(j.selected_shift || 0, 10) || 0;
  stock = j.stock || stock;
  canDeleteEntries = !!j.can_delete;

  totalsByOut = new Map();
  for(const r of (j.totals||[])){
    totalsByOut.set(String(r.output_product_id), {
      out_qty: num(r.out_qty),
      bars_used: num(r.bars_used),
      bars_taken: num(r.bars_taken),
      bars_leftover: num(r.bars_leftover)
    });
  }

  entries = j.entries || [];

  applyShiftUI();
  renderKpis();
  renderProducts();
  renderLog();
}

async function startShift(){
  const date = $('datePick').value;
  const shift = parseInt($('shiftSel').value,10);
  const j = await apiPost('shift_start', {date, shift});
  if(!j.ok){ showErr(j, 'Error al iniciar turno'); return; }
  modalShift.hide();
  await loadDay();
}

async function endShift(){
  const date = $('datePick').value;
  const current = calcShiftSummary();
  const msg =
    `¿Finalizar turno?\n\n` +
    `Resumen actual:\n` +
    `Piezas reportadas: ${fmtPz(current.output_qty)}\n` +
    `Barras usadas: ${fmt2(current.bars_used)}\n` +
    `Sobrante generado: ${fmt2(current.bars_leftover)}\n` +
    `Kg usados: ${fmt2(current.kg_used)}`;
  if(!confirm(msg)) return;

  const j = await apiPost('shift_end', {date});
  if(!j.ok){ showErr(j, 'Error al finalizar turno'); return; }

  const s = j.summary || current;
  alert(
    `Turno finalizado correctamente.\n\n` +
    `Piezas reportadas: ${fmtPz(s.output_qty)}\n` +
    `Barras usadas: ${fmt2(s.bars_used)}\n` +
    `Sobrante generado: ${fmt2(s.bars_leftover)}\n` +
    `Kg usados: ${fmt2(s.kg_used)}`
  );
  await loadDay();
}

function openReportModal(outId){
  const o = outputs.find(x => String(x.output_product_id) === String(outId));
  if (!o) return;

  curOut = {
    id: parseInt(o.output_product_id,10),
    factor: num(o.output_per_input),
    sku: o.sku,
    name: o.name,
    allows_leftover_source: !!o.allows_leftover_source,
    allowed_source_modes: Array.isArray(o.allowed_source_modes) && o.allowed_source_modes.length ? o.allowed_source_modes : ['CUB']
  };

  $('mTitle').textContent = curOut.name;
  $('mSku').textContent = curOut.sku;
  if ($('mFactor')) $('mFactor').textContent = curOut.factor.toFixed(3);
  $('mQty').value = '0';
  $('mWarn').textContent = '';
  for (const opt of Array.from($('mSourceMode').options)) {
    opt.hidden = !curOut.allowed_source_modes.includes(opt.value);
    opt.disabled = !curOut.allowed_source_modes.includes(opt.value);
  }
  $('mSourceMode').value = curOut.allowed_source_modes[0] || 'CUB';
  $('srcWrap').classList.toggle('hidden', curOut.allowed_source_modes.length <= 1);
  setNowLocal('mTime');
  recalcModal();

  modalAdd.show();
  setTimeout(()=> $('mQty').focus(), 180);
}

function recalcModal(){
  if (!curOut) return;
  const qty = num($('mQty').value);
  const sourceMode = (curOut.allowed_source_modes.length > 1) ? $('mSourceMode').value : (curOut.allowed_source_modes[0] || 'CUB');

  const pieceKg = pieceKgForOutput(curOut);
  const recoverableKgPerBar = recoverableKgPerBarForOutput(curOut);

  const barsUsed = (pieceKg > 0 && sourceMode !== 'SOB-CUB')
    ? ((qty * pieceKg) / BAR_KG)
    : ((curOut.factor > 0) ? (qty / curOut.factor) : 0);

  const barsTaken = (sourceMode === 'SOB-CUB')
    ? 0
    : ((curOut.factor > 0 && qty > 0) ? Math.ceil((qty / curOut.factor) - 0.0000001) : 0);

  const physicalRemainderBars = Math.max(0, barsTaken - barsUsed);
  const recoverableBars = (sourceMode === 'SOB-CUB') ? 0 : ((barsTaken * recoverableKgPerBar) / BAR_KG);
  const barsLeft = (recoverableKgPerBar > 0) ? Math.min(recoverableBars, physicalRemainderBars) : physicalRemainderBars;

  const kgUsed = barsUsed * BAR_KG;
  const kgRecoverable = barsLeft * BAR_KG;
  const kgScrap = Math.max(0, (barsTaken - barsUsed - barsLeft) * BAR_KG);

  if ($('mPiecesCf3')) $('mPiecesCf3').textContent = Number.isFinite(qty) ? qty.toLocaleString('es-MX', {maximumFractionDigits:0}) : '0';
  $('mSourceLbl').textContent = sourceMode;
  $('mBarsUsed').textContent = fmt2(barsUsed);
  $('mBarsTaken').textContent = fmt2(barsTaken);
  $('mBarsLeft').textContent = fmt2(barsLeft);
  $('mKgUsed').textContent = fmt2(kgUsed);
  if ($('mKgRecoverable')) $('mKgRecoverable').textContent = fmt2(kgRecoverable);
  if ($('mKgScrap')) $('mKgScrap').textContent = fmt2(kgScrap);

  $('mBarsAvailCub').textContent = fmt2(stock.bars_cub_available);
  if ($('mBarsAvailCf1')) $('mBarsAvailCf1').textContent = fmt2(stock.bars_cf1_available);
  $('mBarsAvailLeft').textContent = fmt2(stock.bars_left_available);
  $('mKgAvailCub').textContent = fmt2(stock.kg_cub_available);
  if ($('mKgAvailCf1')) $('mKgAvailCf1').textContent = fmt2(stock.kg_cf1_available);
  $('mKgAvailLeft').textContent = fmt2(stock.kg_left_available);

  if (sourceMode === 'SOB-CUB') {
    if (barsUsed - num(stock.bars_left_available) > 0.000001){
      $('mWarn').innerHTML = `<span class="text-warning"><i class="bi bi-exclamation-triangle"></i> No hay suficiente sobrante disponible en ${h(WH_LEFT_CODE)}.</span>`;
    } else {
      $('mWarn').innerHTML = `<span class="text-info"><i class="bi bi-info-circle"></i> Se consumirá sobrante existente, sin tomar barras completas de CUB.</span>`;
    }
  } else {
    const available = sourceMode === 'CF1' ? num(stock.bars_cf1_available) : num(stock.bars_cub_available);
    if (barsTaken - available > 0.000001){
      $('mWarn').innerHTML = `<span class="text-warning"><i class="bi bi-exclamation-triangle"></i> No hay barras completas suficientes en ${h(sourceMode)}.</span>`;
    } else if (recoverableKgPerBar > 0 && barsTaken > 0){
      $('mWarn').innerHTML = `<span class="text-info"><i class="bi bi-info-circle"></i> Regla: ${fmt2(recoverableKgPerBar)} kg recuperables por barra tomada. Total recuperable: ${fmt2(kgRecoverable)} kg a ${h(WH_LEFT_CODE)}.</span>`;
    } else if (barsLeft > 0.000001){
      $('mWarn').innerHTML = `<span class="text-info"><i class="bi bi-info-circle"></i> El sobrante se enviará automáticamente a ${h(WH_LEFT_CODE)}.</span>`;
    } else {
      $('mWarn').textContent = '';
    }
  }
}

function openReturnBarsModal(){
  if (!isViewingActiveShift()) {
    alert('Para regresar barras a CF1 debes consultar el turno activo del día.');
    return;
  }
  if (num(stock.bars_cub_available) < 1) {
    alert('No existen barras completas disponibles en CUB para regresar a CF1.');
    return;
  }
  pendingReturn = null;
  $('rCubAvailable').textContent = fmt2(stock.bars_cub_available);
  $('rCf1Available').textContent = fmt2(stock.bars_cf1_available);
  $('rQty').value = '1';
  $('rQty').max = String(Math.floor(num(stock.bars_cub_available)));
  $('rNotes').value = '';
  modalReturnBars.show();
  setTimeout(()=> $('rQty').focus(), 160);
}

async function previewBarReturn(){
  const qty = num($('rQty').value);
  if (qty <= 0 || Math.abs(qty - Math.round(qty)) > 0.0000001) {
    alert('Indica una cantidad de barras completas.');
    return;
  }

  const j = await apiPost('bar_return_preview', {
    date: $('datePick').value,
    bar_product_id: bar.id,
    qty_bars: qty
  });
  if (!j.ok) {
    showErr(j, 'No fue posible calcular el movimiento.');
    return;
  }

  pendingReturn = {
    ...j.summary,
    notes: $('rNotes').value || '',
    event_time: ''
  };
  $('rcQty').textContent = fmtPz(pendingReturn.qty_bars);
  $('rcKg').textContent = fmt2(pendingReturn.kg_move);
  $('rcCubBefore').textContent = fmt2(pendingReturn.cub_before);
  $('rcCubMove').textContent = '-' + fmt2(pendingReturn.qty_bars);
  $('rcCubAfter').textContent = fmt2(pendingReturn.cub_after);
  $('rcCf1Before').textContent = fmt2(pendingReturn.cf1_before);
  $('rcCf1Move').textContent = '+' + fmt2(pendingReturn.qty_bars);
  $('rcCf1After').textContent = fmt2(pendingReturn.cf1_after);

  modalReturnBars.hide();
  modalReturnConfirm.show();
}

async function saveBarReturn(){
  if (!pendingReturn) return;
  const j = await apiPost('bar_return_save', {
    date: $('datePick').value,
    bar_product_id: bar.id,
    qty_bars: pendingReturn.qty_bars,
    notes: pendingReturn.notes
  });
  if (!j.ok) {
    showErr(j, 'Error al registrar devolución a CF1.');
    return;
  }

  modalReturnConfirm.hide();
  pendingReturn = null;
  alert('Movimiento registrado correctamente. Las barras regresaron de CUB a CF1.');
  await loadDay();
}

async function saveReport(){
  if (!curOut) return;
  const qty = num($('mQty').value);
  if (qty <= 0){ alert('Cantidad inválida'); return; }
  if (Math.abs(qty - Math.round(qty)) > 0.0000001){ alert('La cantidad reportada debe ser pieza entera. Las fracciones solo aplican a SOB-CUB.'); return; }

  const date = $('datePick').value;
  const entry_time = $('mTime').value;
  const source_mode = (curOut.allowed_source_modes.length > 1) ? $('mSourceMode').value : (curOut.allowed_source_modes[0] || 'CUB');

  const j = await apiPost('entry_add', {
    date,
    bar_product_id: bar.id,
    output_product_id: curOut.id,
    output_qty: qty,
    entry_time,
    source_mode
  });
  if(!j.ok){
    showErr(j, 'Error al guardar');
    return;
  }

  modalAdd.hide();
  await loadDay();
}

async function delEntry(id){
  const j = await apiPost('entry_delete', {id});
  if(!j.ok){ showErr(j, 'Error al borrar'); return; }
  await loadDay();
}

document.addEventListener('DOMContentLoaded', async ()=>{
  modalAdd = bootstrap.Modal.getOrCreateInstance(document.getElementById('mdlAdd'));
  modalShift = bootstrap.Modal.getOrCreateInstance(document.getElementById('mdlShift'));
  modalReturnBars = bootstrap.Modal.getOrCreateInstance(document.getElementById('mdlReturnBars'));
  modalReturnConfirm = bootstrap.Modal.getOrCreateInstance(document.getElementById('mdlReturnConfirm'));

  const ok = await loadSetup();
  if (!ok) return;

  await loadDay();

  $('datePick').addEventListener('change', loadDay);
  if ($('shiftViewSel')) $('shiftViewSel').addEventListener('change', loadDay);

  $('btnStart').addEventListener('click', ()=> modalShift.show());
  $('doShiftStart').addEventListener('click', startShift);
  $('btnEnd').addEventListener('click', endShift);

  $('btnReturnBars').addEventListener('click', (e)=>{
    e.stopPropagation();
    openReturnBarsModal();
  });
  $('availabilityCard').addEventListener('click', (e)=>{
    if (e.target.closest('button')) return;
    if ($('btnReturnBars').disabled) return;
    openReturnBarsModal();
  });
  $('availabilityCard').addEventListener('keydown', (e)=>{
    if ((e.key === 'Enter' || e.key === ' ') && !$('btnReturnBars').disabled) {
      e.preventDefault();
      openReturnBarsModal();
    }
  });
  $('rPreview').addEventListener('click', previewBarReturn);
  $('rcBack').addEventListener('click', ()=>{
    modalReturnConfirm.hide();
    modalReturnBars.show();
  });
  $('rcSave').addEventListener('click', saveBarReturn);

  $('prodBody').addEventListener('click', (e)=>{
    const b = e.target.closest('[data-report]');
    if (!b) return;
    openReportModal(b.dataset.report);
  });

  $('mQty').addEventListener('input', recalcModal);
  $('mSourceMode').addEventListener('change', recalcModal);
  $('mSave').addEventListener('click', saveReport);

  $('logBody').addEventListener('click', (e)=>{
    const b = e.target.closest('[data-del]');
    if(!b) return;
    const id = parseInt(b.dataset.del,10);
    if(!id) return;
    if(!confirm('¿Borrar captura?')) return;
    delEntry(id);
  });

  setInterval(()=> apiPost('shift_heartbeat', {date: $('datePick').value}), 25000);
});
</script>
</body>
</html>
