<?php
/**
 * admin_config.php
 * ============================================================
 * BReport v2 · ADMIN CONFIGURACIÓN (TODO EN UN SOLO ARCHIVO)
 * - Requiere login y rol admin
 * - CRUD:
 *    * Localidades (locations)
 *    * Productos (products) por empresa (erp_id opcional)
 *    * Canales de salida (outbound_channels) por empresa
 *    * Conceptos Calidad (quality_concepts) por empresa (MERMA/SCRAP)
 *    * Usuarios (users) + asignación de roles + acceso empresa/localidad
 * - API en el mismo archivo vía ?action=...
 * - UI Bootstrap responsive (PC/tablet/cel) con cards tipo botón
 * ============================================================
 */

declare(strict_types=1);

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

require_login();
if (!has_role('admin')) {
  http_response_code(403);
  exit('Acceso denegado (admin).');
}

/*
 * ALCANCE DEL ADMINISTRADOR
 * ------------------------------------------------------------
 * La configuración pertenece a la empresa activa seleccionada en Dashboard.
 * Si el usuario ingresó desde Hielo Barra, Admin únicamente consulta y guarda
 * configuraciones de Hielo Barra; no debe iniciar mostrando Botella por orden
 * alfabético ni permitir configuración cruzada.
 */
require_scope();
ensure_session();

$pdo = pdo();
$adminScope = scope();
$adminCompanyId  = (int)($adminScope['company_id'] ?? 0);
$adminLocationId = (int)($adminScope['location_id'] ?? 0);

if ($adminCompanyId <= 0 || $adminLocationId <= 0) {
  http_response_code(400);
  exit('Selecciona primero una empresa y una localidad desde el Dashboard.');
}

$stAdminScope = $pdo->prepare("
  SELECT c.name AS company_name, c.code AS company_code,
         l.name AS location_name, l.code AS location_code
  FROM companies c
  JOIN locations l ON l.company_id = c.id AND l.id = ?
  WHERE c.id = ?
  LIMIT 1
");
$stAdminScope->execute([$adminLocationId, $adminCompanyId]);
$adminScopeRow = $stAdminScope->fetch(PDO::FETCH_ASSOC) ?: [];
$adminCompanyName  = (string)($adminScopeRow['company_name'] ?? ('Empresa #' . $adminCompanyId));
$adminLocationName = (string)($adminScopeRow['location_name'] ?? ('Localidad #' . $adminLocationId));

$action = (string)($_GET['action'] ?? '');


/* ===================== Roles base de BReport ===================== */
function ensure_breport_required_roles(PDO $pdo): void {
  $roles = [
    ['ventas', 'Ventas'],
    ['supervisor', 'Supervisor'],
    ['reparto', 'Reparto'],
  ];

  $st = $pdo->prepare("
    INSERT INTO roles (code, name)
    VALUES (?, ?)
    ON DUPLICATE KEY UPDATE name = VALUES(name)
  ");

  foreach ($roles as [$code, $name]) {
    $st->execute([$code, $name]);
  }
}

ensure_breport_required_roles($pdo);


/* ===================== Reglas de almacén por producto ===================== */
function ensure_product_warehouse_rules_schema(PDO $pdo): void {
  $pdo->exec("
    CREATE TABLE IF NOT EXISTS product_warehouse_rules (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      company_id BIGINT UNSIGNED NOT NULL,
      location_id BIGINT UNSIGNED NOT NULL,
      product_id BIGINT UNSIGNED NOT NULL,
      default_finished_warehouse_id BIGINT UNSIGNED DEFAULT NULL,
      default_sales_warehouse_id BIGINT UNSIGNED DEFAULT NULL,
      merma_warehouse_id BIGINT UNSIGNED DEFAULT NULL,
      aprovechamiento_warehouse_id BIGINT UNSIGNED DEFAULT NULL,
      cubicado_warehouse_id BIGINT UNSIGNED DEFAULT NULL,
      notes VARCHAR(255) DEFAULT NULL,
      active TINYINT(1) NOT NULL DEFAULT 1,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT NULL,
      PRIMARY KEY (id),
      UNIQUE KEY uq_pwr_scope_product (company_id, location_id, product_id),
      KEY idx_pwr_product (product_id),
      KEY idx_pwr_location (location_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  ");
}
ensure_product_warehouse_rules_schema($pdo);

/* ===================== Presentación logística por producto ===================== */
/*
 * El dump operativo ya contiene products.pieces_per_package y
 * products.packages_per_pallet; estos campos se conservan como presentación
 * principal/compatible con módulos existentes.
 *
 * Para permitir más de un embalaje del mismo artículo se utiliza
 * product_packaging_options (1 producto -> N embalajes), creada mediante
 * script SQL controlado. No se ejecutan ALTER ni consultas a
 * information_schema al abrir Admin.
 */


/* ===================== Rutas comerciales por empresa/localidad ===================== */
/*
 * route_access_codes es el catálogo único de rutas existente en BReport.
 * Sus columnas administrativas adicionales se crean mediante el script SQL
 * controlado de despliegue; NO se intentan crear desde PHP en cada carga.
 * Esto evita acceso a information_schema y no mezcla tablas de rutas.
 */


/* ===================== Auditoría de correcciones inventory_moves ===================== */
function ensure_inventory_move_admin_edits_schema(PDO $pdo): void {
  $pdo->exec("
    CREATE TABLE IF NOT EXISTS inventory_move_admin_edits (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      inventory_move_id BIGINT UNSIGNED NOT NULL,
      company_id BIGINT UNSIGNED NOT NULL,
      location_id BIGINT UNSIGNED NOT NULL,
      old_data JSON NULL,
      new_data JSON NULL,
      reason VARCHAR(255) DEFAULT NULL,
      edited_by BIGINT UNSIGNED NOT NULL,
      edited_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      PRIMARY KEY (id),
      KEY idx_imae_move (inventory_move_id),
      KEY idx_imae_scope_date (company_id, location_id, edited_at),
      KEY idx_imae_user (edited_by)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  ");
}
ensure_inventory_move_admin_edits_schema($pdo);

function inv_move_snapshot_json(array $row): string {
  return json_encode([
    'id' => (int)($row['id'] ?? 0),
    'company_id' => (int)($row['company_id'] ?? 0),
    'location_id' => (int)($row['location_id'] ?? 0),
    'warehouse_id' => isset($row['warehouse_id']) ? (int)$row['warehouse_id'] : null,
    'work_date' => (string)($row['work_date'] ?? ''),
    'created_at' => (string)($row['created_at'] ?? ''),
    'product_id' => (int)($row['product_id'] ?? 0),
    'qty' => (float)($row['qty'] ?? 0),
    'move_type' => (string)($row['move_type'] ?? ''),
    'ref_type' => (string)($row['ref_type'] ?? ''),
    'ref_id' => $row['ref_id'] ?? null,
    'notes' => (string)($row['notes'] ?? ''),
    'created_by' => isset($row['created_by']) ? (int)$row['created_by'] : null,
  ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
}


/* ===================== Helpers API ===================== */
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);
  exit;
}
function read_json(): array {
  $raw = file_get_contents('php://input');
  $d = json_decode($raw ?: '[]', true);
  return is_array($d) ? $d : [];
}
function is_post_json(): bool {
  return ($_SERVER['REQUEST_METHOD'] ?? '') !== 'GET';
}

/* ============ Scope de empresa activo para configuración ============ */
function admin_current_company_id(): int {
  $companyId = (int)($GLOBALS['adminCompanyId'] ?? 0);
  if ($companyId <= 0) {
    jexit(['ok'=>false,'error'=>'No existe empresa activa para configuración.'], 400);
  }
  return $companyId;
}
function admin_validate_write_company(int $requestedCompanyId): int {
  $activeCompanyId = admin_current_company_id();
  if ($requestedCompanyId !== $activeCompanyId) {
    jexit(['ok'=>false,'error'=>'La configuración solo puede guardarse en la empresa activa.'], 403);
  }
  return $activeCompanyId;
}

/* ============ Helpers DB para Roles/Accesos (self-contained) ============ */
function db_roles_list(PDO $pdo): array {
  $st = $pdo->query("SELECT id, code, name FROM roles ORDER BY name");
  return $st->fetchAll();
}
function db_user_roles(PDO $pdo, int $userId): array {
  $st = $pdo->prepare("
    SELECT r.code
    FROM user_roles ur
    JOIN roles r ON r.id = ur.role_id
    WHERE ur.user_id = ?
  ");
  $st->execute([$userId]);
  return array_map(fn($x) => (string)$x['code'], $st->fetchAll());
}
function db_set_user_roles(PDO $pdo, int $userId, array $roleCodes): void {
  $roleCodes = array_values(array_unique(array_filter(array_map('trim', $roleCodes))));
  $pdo->beginTransaction();
  try {
    $pdo->prepare("DELETE FROM user_roles WHERE user_id=?")->execute([$userId]);
    if ($roleCodes) {
      $in = implode(',', array_fill(0, count($roleCodes), '?'));
      $st = $pdo->prepare("SELECT id, code FROM roles WHERE code IN ($in)");
      $st->execute($roleCodes);
      $rows = $st->fetchAll();
      $ins = $pdo->prepare("INSERT INTO user_roles(user_id, role_id) VALUES(?,?)");
      foreach ($rows as $r) $ins->execute([$userId, (int)$r['id']]);
    }
    $pdo->commit();
  } catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
  }
}
function db_user_access(PDO $pdo, int $userId): array {
  $st = $pdo->prepare("SELECT company_id FROM user_company_access WHERE user_id=?");
  $st->execute([$userId]);
  $companyIds = array_map('intval', array_column($st->fetchAll(), 'company_id'));

  $st2 = $pdo->prepare("SELECT location_id FROM user_location_access WHERE user_id=?");
  $st2->execute([$userId]);
  $locationIds = array_map('intval', array_column($st2->fetchAll(), 'location_id'));

  return ['company_ids'=>$companyIds, 'location_ids'=>$locationIds];
}
function db_set_user_access(PDO $pdo, int $userId, array $companyIds, array $locationIds): void {
  $companyIds = array_values(array_unique(array_filter(array_map('intval', $companyIds))));
  $locationIds = array_values(array_unique(array_filter(array_map('intval', $locationIds))));
  $pdo->beginTransaction();
  try {
    $pdo->prepare("DELETE FROM user_company_access WHERE user_id=?")->execute([$userId]);
    $pdo->prepare("DELETE FROM user_location_access WHERE user_id=?")->execute([$userId]);

    if ($companyIds) {
      $ins = $pdo->prepare("INSERT INTO user_company_access(user_id, company_id) VALUES(?,?)");
      foreach ($companyIds as $cid) $ins->execute([$userId, $cid]);
    }
    if ($locationIds) {
      $ins2 = $pdo->prepare("INSERT INTO user_location_access(user_id, location_id) VALUES(?,?)");
      foreach ($locationIds as $lid) $ins2->execute([$userId, $lid]);
    }
    $pdo->commit();
  } catch (Throwable $e) {
    $pdo->rollBack();
    throw $e;
  }
}



/* ============ Helpers Inventario/Ajuste Almacenes ============ */
function inv_adjust_code_ok(string $code): bool {
  return hash_equals('Blanquita26', trim($code));
}

function inv_balance_sql(): string {
  return "COALESCE(SUM(CASE WHEN im.move_type IN ('IN','ADJ') THEN im.qty WHEN im.move_type='OUT' THEN -im.qty ELSE 0 END),0)";
}

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


function admin_date_ok(string $date): bool {
  return (bool)preg_match('/^\d{4}-\d{2}-\d{2}$/', $date);
}


function ensure_production_plc_admin_schema(PDO $pdo): void {
  $pdo->exec("
    CREATE TABLE IF NOT EXISTS production_lines (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      company_id BIGINT UNSIGNED NOT NULL,
      location_id BIGINT UNSIGNED NOT NULL,
      line_no TINYINT UNSIGNED NOT NULL,
      line_code VARCHAR(40) DEFAULT NULL,
      line_name VARCHAR(120) NOT NULL,
      active TINYINT(1) NOT NULL DEFAULT 1,
      sort_order INT NOT NULL DEFAULT 0,
      created_by BIGINT UNSIGNED DEFAULT NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT NULL,
      PRIMARY KEY(id),
      UNIQUE KEY uq_production_lines_scope_no (company_id, location_id, line_no),
      KEY idx_production_lines_scope_active (company_id, location_id, active)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  ");

  $pdo->exec("
    CREATE TABLE IF NOT EXISTS production_plc_tags (
      id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
      company_id BIGINT UNSIGNED NOT NULL,
      location_id BIGINT UNSIGNED NOT NULL,
      tag_code VARCHAR(80) NOT NULL,
      tag_name VARCHAR(120) NOT NULL,
      plc_name VARCHAR(120) DEFAULT NULL,
      ip_address VARCHAR(80) DEFAULT NULL,
      line_name VARCHAR(80) DEFAULT NULL,
      production_line_id BIGINT UNSIGNED DEFAULT NULL,
      production_line_no TINYINT UNSIGNED DEFAULT NULL,
      product_id BIGINT UNSIGNED DEFAULT NULL,
      packaging_option_id BIGINT UNSIGNED DEFAULT NULL,
      warehouse_id BIGINT UNSIGNED DEFAULT NULL,
      cf_wh_id BIGINT UNSIGNED DEFAULT NULL,
      qty_per_signal DECIMAL(14,3) NOT NULL DEFAULT 1.000,
      pallets_per_signal DECIMAL(14,3) NOT NULL DEFAULT 1.000,
      packages_per_signal DECIMAL(14,3) NOT NULL DEFAULT 0.000,
      pieces_per_signal DECIMAL(14,3) NOT NULL DEFAULT 0.000,
      signal_state TINYINT(1) NOT NULL DEFAULT 0,
      status VARCHAR(30) NOT NULL DEFAULT 'IDLE',
      active TINYINT(1) NOT NULL DEFAULT 1,
      enabled TINYINT(1) NOT NULL DEFAULT 1,
      last_value_at DATETIME DEFAULT NULL,
      opened_at DATETIME DEFAULT NULL,
      opened_by BIGINT UNSIGNED DEFAULT NULL,
      handled_at DATETIME DEFAULT NULL,
      handled_by BIGINT UNSIGNED DEFAULT NULL,
      last_action VARCHAR(30) DEFAULT NULL,
      notes VARCHAR(255) DEFAULT NULL,
      created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
      updated_at DATETIME DEFAULT NULL,
      PRIMARY KEY(id),
      KEY idx_prod_plc_tags_scope_line (company_id, location_id, production_line_no, active),
      KEY idx_prod_plc_tags_product (product_id)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci
  ");

  foreach ([
    ['production_plc_tags','enabled',"enabled TINYINT(1) NOT NULL DEFAULT 1 AFTER active"],
    ['production_plc_tags','production_line_id',"production_line_id BIGINT UNSIGNED DEFAULT NULL AFTER line_name"],
    ['production_plc_tags','production_line_no',"production_line_no TINYINT UNSIGNED DEFAULT NULL AFTER production_line_id"],
    ['production_plc_tags','packaging_option_id',"packaging_option_id BIGINT UNSIGNED DEFAULT NULL AFTER product_id"],
    ['production_plc_tags','warehouse_id',"warehouse_id BIGINT UNSIGNED DEFAULT NULL AFTER packaging_option_id"],
    ['production_plc_tags','pallets_per_signal',"pallets_per_signal DECIMAL(14,3) NOT NULL DEFAULT 1.000 AFTER qty_per_signal"],
    ['production_plc_tags','packages_per_signal',"packages_per_signal DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER pallets_per_signal"],
    ['production_plc_tags','pieces_per_signal',"pieces_per_signal DECIMAL(14,3) NOT NULL DEFAULT 0.000 AFTER packages_per_signal"],
  ] as [$table, $col, $ddl]) {
    if (admin_table_exists($pdo, $table) && !admin_column_exists($pdo, $table, $col)) {
      try { $pdo->exec("ALTER TABLE `$table` ADD COLUMN $ddl"); } catch (Throwable $e) {}
    }
  }

  try {
    $st = $pdo->prepare("SELECT COUNT(*) FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='production_plc_tags' AND index_name='idx_prod_plc_tags_scope_line'");
    $st->execute();
    if ((int)$st->fetchColumn() === 0) {
      $pdo->exec("ALTER TABLE production_plc_tags ADD INDEX idx_prod_plc_tags_scope_line (company_id, location_id, production_line_no, active)");
    }
  } catch (Throwable $e) {}
}
ensure_production_plc_admin_schema($pdo);

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

  if (is_post_json()) {
    $hdr = $_SERVER['HTTP_X_CSRF'] ?? '';
    if ($hdr === '') jexit(['ok'=>false,'error'=>'Falta X-CSRF'], 419);
    csrf_verify($hdr);
  }

  // --------- list companies ----------
  if ($action === 'companies') {
    $st = $pdo->prepare("SELECT id, name, code, active FROM companies WHERE id=? AND active=1 LIMIT 1");
    $st->execute([admin_current_company_id()]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  // --------- locations list ----------
  if ($action === 'locations_list') {
    $companyId = admin_current_company_id();
    $st = $pdo->prepare("
      SELECT l.id, l.company_id, c.name AS company_name, l.name, l.code, l.timezone, l.active, l.created_at
      FROM locations l
      JOIN companies c ON c.id=l.company_id
      WHERE (?=0 OR l.company_id=?)
      ORDER BY c.name, l.name
    ");
    $st->execute([$companyId, $companyId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  // --------- locations save ----------
  if ($action === 'locations_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $name = trim((string)($d['name'] ?? ''));
    $code = trim((string)($d['code'] ?? ''));
    $tz   = trim((string)($d['timezone'] ?? 'America/Monterrey'));
    $active = (int)($d['active'] ?? 1) ? 1 : 0;

    if ($companyId<=0 || $name==='' || $code==='') {
      jexit(['ok'=>false,'error'=>'company_id, name y code son requeridos'], 400);
    }

    if ($id>0) {
      $st = $pdo->prepare("UPDATE locations SET company_id=?, name=?, code=?, timezone=?, active=? WHERE id=?");
      $st->execute([$companyId, $name, $code, $tz, $active, $id]);
      jexit(['ok'=>true,'id'=>$id]);
    } else {
      $st = $pdo->prepare("INSERT INTO locations(company_id,name,code,timezone,active) VALUES(?,?,?,?,?)");
      $st->execute([$companyId, $name, $code, $tz, $active]);
      jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
    }
  }

  // --------- warehouses list ----------
  if ($action === 'warehouses_list') {
    $companyId = admin_current_company_id();
    $locationId = (int)($_GET['location_id'] ?? 0);
    $st = $pdo->prepare("
      SELECT w.id, w.company_id, c.name AS company_name,
             w.location_id, l.name AS location_name, l.code AS location_code,
             w.code, w.name, w.active, w.created_at
      FROM warehouses w
      JOIN companies c ON c.id = w.company_id
      JOIN locations l ON l.id = w.location_id
      WHERE (? = 0 OR w.company_id = ?)
        AND (? = 0 OR w.location_id = ?)
      ORDER BY c.name, l.name, FIELD(w.code,'CF1','CF2','CF3','CUB','MERMA','APROV-MERMA','SOB-CUB'), w.code, w.name
    ");
    $st->execute([$companyId, $companyId, $locationId, $locationId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  // --------- warehouses save ----------
  if ($action === 'warehouses_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $locationId = (int)($d['location_id'] ?? 0);
    $code = strtoupper(trim((string)($d['code'] ?? '')));
    $name = trim((string)($d['name'] ?? ''));
    $active = (int)($d['active'] ?? 1) ? 1 : 0;

    if ($companyId <= 0 || $locationId <= 0 || $code === '' || $name === '') {
      jexit(['ok'=>false,'error'=>'Empresa, localidad, código y nombre son requeridos.'], 400);
    }

    $st = $pdo->prepare("SELECT id FROM locations WHERE id=? AND company_id=? LIMIT 1");
    $st->execute([$locationId, $companyId]);
    if (!$st->fetch()) {
      jexit(['ok'=>false,'error'=>'La localidad no pertenece a la empresa seleccionada.'], 400);
    }

    if ($id > 0) {
      $st = $pdo->prepare("
        SELECT id FROM warehouses
        WHERE company_id=? AND location_id=? AND UPPER(code)=UPPER(?) AND id<>?
        LIMIT 1
      ");
      $st->execute([$companyId, $locationId, $code, $id]);
      if ($st->fetch()) jexit(['ok'=>false,'error'=>'Ya existe un almacén con ese código en esta localidad.'], 409);

      $st = $pdo->prepare("
        UPDATE warehouses
        SET company_id=?, location_id=?, code=?, name=?, active=?
        WHERE id=?
      ");
      $st->execute([$companyId, $locationId, $code, $name, $active, $id]);
      jexit(['ok'=>true,'id'=>$id]);
    } else {
      $st = $pdo->prepare("
        SELECT id FROM warehouses
        WHERE company_id=? AND location_id=? AND UPPER(code)=UPPER(?)
        LIMIT 1
      ");
      $st->execute([$companyId, $locationId, $code]);
      if ($st->fetch()) jexit(['ok'=>false,'error'=>'Ya existe un almacén con ese código en esta localidad.'], 409);

      $st = $pdo->prepare("
        INSERT INTO warehouses(company_id, location_id, code, name, active, created_at)
        VALUES(?, ?, ?, ?, ?, NOW())
      ");
      $st->execute([$companyId, $locationId, $code, $name, $active]);
      jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
    }
  }

  // --------- products list ----------
  if ($action === 'products_list') {
    $companyId = admin_current_company_id();

    $hasErpId = admin_column_exists($pdo, 'products', 'erp_id');
    $hasPieces = admin_column_exists($pdo, 'products', 'pieces_per_package');
    $hasPackages = admin_column_exists($pdo, 'products', 'packages_per_pallet');
    $hasCreatedAt = admin_column_exists($pdo, 'products', 'created_at');

    $erpSelect = $hasErpId ? "p.erp_id" : "NULL AS erp_id";
    $piecesSelect = $hasPieces ? "p.pieces_per_package" : "NULL AS pieces_per_package";
    $packagesSelect = $hasPackages ? "p.packages_per_pallet" : "NULL AS packages_per_pallet";
    $piecesPalletExpr = ($hasPieces && $hasPackages)
      ? "CASE WHEN COALESCE(p.pieces_per_package,0) > 0 AND COALESCE(p.packages_per_pallet,0) > 0 THEN p.pieces_per_package * p.packages_per_pallet ELSE NULL END"
      : "NULL";
    $createdSelect = $hasCreatedAt ? "p.created_at" : "NULL AS created_at";

    $st = $pdo->prepare("\n      SELECT p.id, p.company_id, c.name AS company_name,\n             p.sku, p.name, {$erpSelect}, p.uom,\n             {$piecesSelect}, {$packagesSelect},\n             {$piecesPalletExpr} AS pieces_per_pallet,\n             p.active, {$createdSelect}\n      FROM products p\n      JOIN companies c ON c.id=p.company_id\n      WHERE (?=0 OR p.company_id=?)\n      ORDER BY c.name, p.sku\n    ");
    $st->execute([$companyId, $companyId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  // --------- products save ----------
  if ($action === 'products_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $sku  = trim((string)($d['sku'] ?? ''));
    $name = trim((string)($d['name'] ?? ''));
    $uom  = trim((string)($d['uom'] ?? 'pz'));
    $active = (int)($d['active'] ?? 1) ? 1 : 0;
    $erpId = trim((string)($d['erp_id'] ?? ''));
    if ($erpId === '') $erpId = null;
    $piecesPerPackage = (int)($d['pieces_per_package'] ?? 0);
    $packagesPerPallet = (int)($d['packages_per_pallet'] ?? 0);
    $piecesPerPackage = $piecesPerPackage > 0 ? $piecesPerPackage : null;
    $packagesPerPallet = $packagesPerPallet > 0 ? $packagesPerPallet : null;

    if ($companyId<=0 || $sku==='' || $name==='') {
      jexit(['ok'=>false,'error'=>'Empresa, artículo y descripción son requeridos.'], 400);
    }

    try {
      $dupe = $pdo->prepare("\n        SELECT id\n        FROM products\n        WHERE company_id = ?\n          AND UPPER(sku) = UPPER(?)\n          AND id <> ?\n        LIMIT 1\n      ");
      $dupe->execute([$companyId, $sku, $id]);
      if ($dupe->fetch()) {
        jexit(['ok'=>false,'error'=>'Ya existe un artículo con ese nombre/SKU en la empresa activa.'], 409);
      }

      $hasErpId = admin_column_exists($pdo, 'products', 'erp_id');
      $hasPieces = admin_column_exists($pdo, 'products', 'pieces_per_package');
      $hasPackages = admin_column_exists($pdo, 'products', 'packages_per_pallet');

      if ($id>0) {
        $sets = ['company_id=?', 'sku=?', 'name=?', 'uom=?', 'active=?'];
        $params = [$companyId, $sku, $name, $uom, $active];
        if ($hasErpId) { $sets[] = 'erp_id=?'; $params[] = $erpId; }
        if ($hasPieces) { $sets[] = 'pieces_per_package=?'; $params[] = $piecesPerPackage; }
        if ($hasPackages) { $sets[] = 'packages_per_pallet=?'; $params[] = $packagesPerPallet; }
        if (admin_column_exists($pdo, 'products', 'updated_at')) { $sets[] = 'updated_at=NOW()'; }
        $params[] = $id;

        $st = $pdo->prepare("UPDATE products SET " . implode(', ', $sets) . " WHERE id=?");
        $st->execute($params);
        jexit(['ok'=>true,'id'=>$id]);
      }

      $cols = ['company_id', 'sku', 'name', 'uom', 'active'];
      $vals = ['?', '?', '?', '?', '?'];
      $params = [$companyId, $sku, $name, $uom, $active];
      if ($hasErpId) { $cols[]='erp_id'; $vals[]='?'; $params[]=$erpId; }
      if ($hasPieces) { $cols[]='pieces_per_package'; $vals[]='?'; $params[]=$piecesPerPackage; }
      if ($hasPackages) { $cols[]='packages_per_pallet'; $vals[]='?'; $params[]=$packagesPerPallet; }
      if (admin_column_exists($pdo, 'products', 'created_at')) { $cols[]='created_at'; $vals[]='NOW()'; }
      if (admin_column_exists($pdo, 'products', 'updated_at')) { $cols[]='updated_at'; $vals[]='NOW()'; }

      $st = $pdo->prepare("INSERT INTO products (" . implode(',', $cols) . ") VALUES (" . implode(',', $vals) . ")");
      $st->execute($params);
      jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
    } catch (PDOException $e) {
      if ((string)$e->getCode() === '23000') {
        jexit(['ok'=>false,'error'=>'No se pudo guardar: el artículo/SKU ya existe o viola una regla única de la base de datos.'], 409);
      }
      jexit(['ok'=>false,'error'=>'No se pudo guardar el artículo: '.$e->getMessage()], 500);
    } catch (Throwable $e) {
      jexit(['ok'=>false,'error'=>'No se pudo guardar el artículo: '.$e->getMessage()], 500);
    }
  }

  // --------- packaging options list: múltiples embalajes por artículo ----------
  if ($action === 'packaging_options_list') {
    $companyId = admin_current_company_id();
    if ($companyId <= 0) {
      jexit(['ok'=>false,'error'=>'Selecciona una empresa para consultar sus embalajes.'], 400);
    }

    try {
      $st = $pdo->prepare("
        SELECT
          p.id AS product_id, p.company_id, p.sku, p.name AS product_name,
          p.erp_id, p.uom,
          po.id AS packaging_id, po.packaging_type, po.packaging_name,
          po.pieces_per_package, po.packages_per_pallet,
          CASE
            WHEN po.pieces_per_package > 0 AND po.packages_per_pallet > 0
            THEN po.pieces_per_package * po.packages_per_pallet ELSE NULL
          END AS pieces_per_pallet,
          po.is_default, po.active AS packaging_active, po.notes
        FROM products p
        LEFT JOIN product_packaging_options po
          ON po.company_id = p.company_id AND po.product_id = p.id
        WHERE p.company_id = ? AND p.active = 1
        ORDER BY p.sku,
                 CASE WHEN po.is_default = 1 THEN 0 ELSE 1 END,
                 po.packaging_type, pieces_per_pallet, po.id
      ");
      $st->execute([$companyId]);
      jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
    } catch (PDOException $e) {
      $msg = $e->getMessage();
      if (stripos($msg, 'product_packaging_options') !== false
          || stripos($msg, 'Base table or view not found') !== false
          || stripos($msg, "doesn't exist") !== false) {
        jexit([
          'ok'=>false,
          'setup_required'=>true,
          'error'=>'Falta instalar la tabla product_packaging_options. Ejecuta el script SQL de embalajes múltiples antes de usar esta sección.'
        ], 409);
      }
      jexit(['ok'=>false,'error'=>'No fue posible consultar los embalajes: '.$msg], 500);
    }
  }

  // --------- packaging option save: principal o adicional ----------
  if ($action === 'packaging_option_save') {
    $d = read_json();
    $id = (int)($d['packaging_id'] ?? $d['id'] ?? 0);
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $productId = (int)($d['product_id'] ?? 0);
    $type = strtoupper(trim((string)($d['packaging_type'] ?? 'TARIMA')));
    $name = trim((string)($d['packaging_name'] ?? ''));
    $piecesPerPackage = (int)($d['pieces_per_package'] ?? 0);
    $packagesPerPallet = (int)($d['packages_per_pallet'] ?? 0);
    $isDefault = (int)($d['is_default'] ?? 0) ? 1 : 0;
    $active = (int)($d['active'] ?? 1) ? 1 : 0;
    $notes = trim((string)($d['notes'] ?? ''));

    if (!in_array($type, ['TARIMA','RACK','CAJA','CONTENEDOR','OTRO'], true)) {
      $type = 'TARIMA';
    }
    if ($companyId <= 0 || $productId <= 0 || $piecesPerPackage <= 0 || $packagesPerPallet <= 0) {
      jexit(['ok'=>false,'error'=>'Empresa, artículo, piezas por paquete y paquetes por tarima son requeridos.'], 400);
    }

    $st = $pdo->prepare("SELECT id, sku, name FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $st->execute([$productId, $companyId]);
    if (!$st->fetch()) {
      jexit(['ok'=>false,'error'=>'El artículo no pertenece a la empresa seleccionada o no está activo.'], 400);
    }

    if ($name === '') $name = ucfirst(strtolower($type)) . ' ' . ($piecesPerPackage * $packagesPerPallet) . ' pzas';
    $name = mb_substr($name, 0, 100);
    $notes = $notes !== '' ? mb_substr($notes, 0, 255) : null;

    $pdo->beginTransaction();
    try {
      if ($id > 0) {
        $chk = $pdo->prepare("SELECT id FROM product_packaging_options WHERE id=? AND company_id=? AND product_id=? LIMIT 1");
        $chk->execute([$id, $companyId, $productId]);
        if (!$chk->fetch()) throw new RuntimeException('La presentación de empaque no pertenece al artículo seleccionado.');
      }

      if ($isDefault === 1) {
        $pdo->prepare("UPDATE product_packaging_options SET is_default=0, updated_at=NOW() WHERE company_id=? AND product_id=?")
            ->execute([$companyId, $productId]);
      }

      if ($id > 0) {
        $st = $pdo->prepare("
          UPDATE product_packaging_options
          SET packaging_type=?, packaging_name=?, pieces_per_package=?,
              packages_per_pallet=?, is_default=?, active=?, notes=?, updated_at=NOW()
          WHERE id=? AND company_id=? AND product_id=?
        ");
        $st->execute([$type, $name, $piecesPerPackage, $packagesPerPallet, $isDefault, $active, $notes, $id, $companyId, $productId]);
      } else {
        $st = $pdo->prepare("
          INSERT INTO product_packaging_options (
            company_id, product_id, packaging_type, packaging_name,
            pieces_per_package, packages_per_pallet, is_default, active,
            notes, created_at, updated_at
          ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
        ");
        $st->execute([$companyId, $productId, $type, $name, $piecesPerPackage, $packagesPerPallet, $isDefault, $active, $notes]);
        $id = (int)$pdo->lastInsertId();
      }

      /* Mantener compatibilidad: solo el empaque principal actualiza products. */
      if ($isDefault === 1) {
        $st = $pdo->prepare("UPDATE products SET pieces_per_package=?, packages_per_pallet=? WHERE id=? AND company_id=?");
        $st->execute([$piecesPerPackage, $packagesPerPallet, $productId, $companyId]);
      }

      $pdo->commit();
      jexit(['ok'=>true,'id'=>$id,'product_id'=>$productId]);
    } catch (PDOException $e) {
      if ($pdo->inTransaction()) $pdo->rollBack();
      $msg = $e->getMessage();
      if (stripos($msg, 'product_packaging_options') !== false
          || stripos($msg, 'Base table or view not found') !== false
          || stripos($msg, "doesn't exist") !== false) {
        jexit([
          'ok'=>false,
          'setup_required'=>true,
          'error'=>'Falta instalar la tabla product_packaging_options. Ejecuta el script SQL de embalajes múltiples antes de guardar.'
        ], 409);
      }
      if ((string)$e->getCode() === '23000') {
        jexit(['ok'=>false,'error'=>'Ya existe un embalaje con la misma combinación para este artículo.'], 409);
      }
      jexit(['ok'=>false,'error'=>'No fue posible guardar el embalaje: '.$msg], 500);
    } catch (Throwable $e) {
      if ($pdo->inTransaction()) $pdo->rollBack();
      jexit(['ok'=>false,'error'=>$e->getMessage()], 400);
    }
  }

  // --------- outbound channels list ----------
  if ($action === 'channels_list') {
    $companyId = admin_current_company_id();
    $st = $pdo->prepare("
      SELECT oc.id, oc.company_id, c.name AS company_name, oc.name, oc.channel_type, oc.sort_order, oc.active
      FROM outbound_channels oc
      JOIN companies c ON c.id=oc.company_id
      WHERE (?=0 OR oc.company_id=?)
      ORDER BY c.name, oc.sort_order, oc.name
    ");
    $st->execute([$companyId, $companyId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  // --------- outbound channels save ----------
  if ($action === 'channels_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $name = trim((string)($d['name'] ?? ''));
    $type = (string)($d['channel_type'] ?? 'SALE');
    $sort = (int)($d['sort_order'] ?? 0);
    $active = (int)($d['active'] ?? 1) ? 1 : 0;

    $allowed = ['SALE','TRANSFER','INTERNAL'];
    if (!in_array($type, $allowed, true)) $type = 'SALE';

    if ($companyId<=0 || $name==='') {
      jexit(['ok'=>false,'error'=>'company_id y name son requeridos'], 400);
    }

    if ($id>0) {
      $st = $pdo->prepare("UPDATE outbound_channels SET company_id=?, name=?, channel_type=?, sort_order=?, active=? WHERE id=?");
      $st->execute([$companyId, $name, $type, $sort, $active, $id]);
      jexit(['ok'=>true,'id'=>$id]);
    } else {
      $st = $pdo->prepare("INSERT INTO outbound_channels(company_id,name,channel_type,sort_order,active) VALUES(?,?,?,?,?)");
      $st->execute([$companyId, $name, $type, $sort, $active]);
      jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
    }
  }

  /* ===================== Rutas comerciales por empresa/localidad ===================== */
  if ($action === 'routes_list') {
    $companyId = admin_current_company_id();
    if ($companyId <= 0) {
      jexit(['ok'=>false,'error'=>'Selecciona una empresa para consultar sus rutas.'], 400);
    }

    $st = $pdo->prepare("
      SELECT
        r.id, r.company_id, c.name AS company_name,
        r.location_id, l.name AS location_name, l.code AS location_code,
        r.route_no,
        COALESCE(NULLIF(r.route_code,''), CONCAT('RUTA-', LPAD(r.route_no,2,'0'))) AS code,
        COALESCE(NULLIF(r.route_name,''), CONCAT('Ruta ', r.route_no)) AS name,
        r.description, r.driver_name, r.vehicle_unit, r.delivery_days,
        r.sort_order, r.active, r.valid_from, r.valid_to, r.created_at, r.updated_at
      FROM route_access_codes r
      JOIN companies c ON c.id = r.company_id
      JOIN locations l ON l.id = r.location_id AND l.company_id = r.company_id
      WHERE r.company_id = ?
      ORDER BY l.name, r.sort_order, r.route_no
    ");
    $st->execute([$companyId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  if ($action === 'routes_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $locationId = (int)($d['location_id'] ?? 0);
    $routeNo = (int)($d['route_no'] ?? 0);
    $routeCode = strtoupper(trim((string)($d['code'] ?? '')));
    $routeName = trim((string)($d['name'] ?? ''));
    $accessCode = trim((string)($d['access_code'] ?? ''));
    $description = trim((string)($d['description'] ?? ''));
    $driverName = trim((string)($d['driver_name'] ?? ''));
    $vehicleUnit = trim((string)($d['vehicle_unit'] ?? ''));
    $deliveryDays = trim((string)($d['delivery_days'] ?? ''));
    $sortOrder = (int)($d['sort_order'] ?? 0);
    $active = (int)($d['active'] ?? 1) ? 1 : 0;

    if ($companyId <= 0 || $locationId <= 0 || $routeNo <= 0 || $routeNo > 255 || $routeCode === '' || $routeName === '') {
      jexit(['ok'=>false,'error'=>'Empresa, localidad, número de ruta (1 a 255), código y nombre son requeridos.'], 400);
    }
    if ($id <= 0 && $accessCode === '') {
      jexit(['ok'=>false,'error'=>'Para una nueva ruta captura el código de acceso del repartidor.'], 400);
    }

    $loc = $pdo->prepare("SELECT id FROM locations WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $loc->execute([$locationId, $companyId]);
    if (!$loc->fetch()) {
      jexit(['ok'=>false,'error'=>'La localidad no pertenece a la empresa seleccionada o no está activa.'], 400);
    }

    $dupe = $pdo->prepare("
      SELECT id FROM route_access_codes
      WHERE company_id=? AND location_id=?
        AND (route_no=? OR UPPER(COALESCE(route_code,''))=UPPER(?))
        AND id<>?
      LIMIT 1
    ");
    $dupe->execute([$companyId, $locationId, $routeNo, $routeCode, $id]);
    if ($dupe->fetch()) {
      jexit(['ok'=>false,'error'=>'Ya existe una ruta con el mismo número o código en esta localidad.'], 409);
    }

    $description = $description !== '' ? mb_substr($description, 0, 255) : null;
    $driverName = $driverName !== '' ? mb_substr($driverName, 0, 120) : null;
    $vehicleUnit = $vehicleUnit !== '' ? mb_substr($vehicleUnit, 0, 80) : null;
    $deliveryDays = $deliveryDays !== '' ? mb_substr($deliveryDays, 0, 120) : null;
    $u = current_user();

    if ($id > 0) {
      if ($accessCode !== '') {
        $st = $pdo->prepare("
          UPDATE route_access_codes
          SET company_id=?, location_id=?, route_no=?, route_code=?, route_name=?,
              description=?, driver_name=?, vehicle_unit=?, delivery_days=?,
              sort_order=?, active=?, access_code_sha256=?, updated_at=NOW()
          WHERE id=?
        ");
        $st->execute([
          $companyId, $locationId, $routeNo, $routeCode, $routeName,
          $description, $driverName, $vehicleUnit, $deliveryDays,
          $sortOrder, $active, hash('sha256', $accessCode), $id
        ]);
      } else {
        $st = $pdo->prepare("
          UPDATE route_access_codes
          SET company_id=?, location_id=?, route_no=?, route_code=?, route_name=?,
              description=?, driver_name=?, vehicle_unit=?, delivery_days=?,
              sort_order=?, active=?, updated_at=NOW()
          WHERE id=?
        ");
        $st->execute([
          $companyId, $locationId, $routeNo, $routeCode, $routeName,
          $description, $driverName, $vehicleUnit, $deliveryDays,
          $sortOrder, $active, $id
        ]);
      }
      jexit(['ok'=>true,'id'=>$id]);
    }

    $st = $pdo->prepare("
      INSERT INTO route_access_codes (
        company_id, location_id, route_no, route_code, route_name, description,
        access_code_sha256, driver_name, vehicle_unit, delivery_days,
        sort_order, active, created_by, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
    ");
    $st->execute([
      $companyId, $locationId, $routeNo, $routeCode, $routeName, $description,
      hash('sha256', $accessCode), $driverName, $vehicleUnit, $deliveryDays,
      $sortOrder, $active, (int)$u['id']
    ]);
    jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
  }



  /* ===================== Producción Botella: Líneas y Tags PLC ===================== */
  if ($action === 'production_lines_list') {
    $companyId = admin_current_company_id();
    $locationId = (int)($GLOBALS['adminLocationId'] ?? 0);
    $st = $pdo->prepare("
      SELECT pl.*, c.name AS company_name, l.name AS location_name, l.code AS location_code
      FROM production_lines pl
      JOIN companies c ON c.id = pl.company_id
      JOIN locations l ON l.id = pl.location_id AND l.company_id = pl.company_id
      WHERE pl.company_id = ? AND pl.location_id = ?
      ORDER BY pl.sort_order, pl.line_no
    ");
    $st->execute([$companyId, $locationId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  if ($action === 'production_lines_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = admin_validate_write_company((int)($d['company_id'] ?? 0));
    $locationId = (int)($d['location_id'] ?? ($GLOBALS['adminLocationId'] ?? 0));
    $lineNo = (int)($d['line_no'] ?? 0);
    $lineCode = strtoupper(trim((string)($d['line_code'] ?? '')));
    $lineName = trim((string)($d['line_name'] ?? ''));
    $sortOrder = (int)($d['sort_order'] ?? $lineNo);
    $active = (int)($d['active'] ?? 1) ? 1 : 0;
    if ($companyId <= 0 || $locationId <= 0 || $lineNo <= 0 || $lineNo > 255 || $lineName === '') {
      jexit(['ok'=>false,'error'=>'Empresa, planta, número y nombre de línea son requeridos.'], 400);
    }
    if ($lineCode === '') $lineCode = 'L' . $lineNo;
    $loc = $pdo->prepare("SELECT id FROM locations WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $loc->execute([$locationId, $companyId]);
    if (!$loc->fetch()) jexit(['ok'=>false,'error'=>'La planta activa no pertenece a la empresa seleccionada.'], 400);

    $dupe = $pdo->prepare("SELECT id FROM production_lines WHERE company_id=? AND location_id=? AND line_no=? AND id<>? LIMIT 1");
    $dupe->execute([$companyId, $locationId, $lineNo, $id]);
    if ($dupe->fetch()) jexit(['ok'=>false,'error'=>'Ya existe una línea con ese número en esta planta.'], 409);

    $u = current_user();
    if ($id > 0) {
      $st = $pdo->prepare("UPDATE production_lines SET line_no=?, line_code=?, line_name=?, active=?, sort_order=?, updated_at=NOW() WHERE id=? AND company_id=? AND location_id=?");
      $st->execute([$lineNo, $lineCode, $lineName, $active, $sortOrder, $id, $companyId, $locationId]);
      jexit(['ok'=>true,'id'=>$id]);
    }
    $st = $pdo->prepare("INSERT INTO production_lines(company_id, location_id, line_no, line_code, line_name, active, sort_order, created_by, created_at) VALUES(?,?,?,?,?,?,?,?,NOW())");
    $st->execute([$companyId, $locationId, $lineNo, $lineCode, $lineName, $active, $sortOrder, (int)$u['id']]);
    jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
  }

  if ($action === 'plc_config_refs') {
    $companyId = admin_current_company_id();
    $locationId = (int)($GLOBALS['adminLocationId'] ?? 0);
    $st = $pdo->prepare("SELECT id, line_no, line_code, line_name, active FROM production_lines WHERE company_id=? AND location_id=? AND active=1 ORDER BY sort_order, line_no");
    $st->execute([$companyId, $locationId]);
    $lines = $st->fetchAll();

    $st = $pdo->prepare("SELECT id, sku, name, uom, active FROM products WHERE company_id=? AND active=1 ORDER BY sku, name");
    $st->execute([$companyId]);
    $products = $st->fetchAll();

    $packaging = [];
    if (admin_table_exists($pdo, 'product_packaging_options')) {
      $st = $pdo->prepare("SELECT id, product_id, packaging_name, pieces_per_package, packages_per_pallet, is_default, active FROM product_packaging_options WHERE company_id=? AND active=1 ORDER BY product_id, is_default DESC, id");
      $st->execute([$companyId]);
      $packaging = $st->fetchAll();
    }

    $st = $pdo->prepare("
      SELECT id, code, name
      FROM warehouses
      WHERE company_id=? AND location_id=? AND active=1
        AND UPPER(COALESCE(code,'')) NOT REGEXP '^RUTA[-_ ]?[0-9]+$'
        AND (
          UPPER(COALESCE(code,''))='ALM01'
          OR UPPER(COALESCE(code,'')) IN ('CUARENTENA','CUA','QUA','HLD')
          OR UPPER(COALESCE(name,'')) LIKE '%CUARENTENA%'
        )
      ORDER BY FIELD(UPPER(code),'ALM01','CUARENTENA','CUA','QUA','HLD'), code, name
    ");
    $st->execute([$companyId, $locationId]);
    $warehouses = $st->fetchAll();

    jexit(['ok'=>true,'lines'=>$lines,'products'=>$products,'packaging'=>$packaging,'warehouses'=>$warehouses]);
  }

  if ($action === 'plc_tags_list') {
    $companyId = admin_current_company_id();
    $locationId = (int)($GLOBALS['adminLocationId'] ?? 0);
    $st = $pdo->prepare("
      SELECT t.*,
             COALESCE(pl.line_name, t.line_name, CONCAT('Línea ', t.production_line_no)) AS configured_line_name,
             p.sku, p.name AS product_name,
             po.packaging_name,
             w.code AS warehouse_code, w.name AS warehouse_name
      FROM production_plc_tags t
      LEFT JOIN production_lines pl ON pl.id=t.production_line_id AND pl.company_id=t.company_id AND pl.location_id=t.location_id
      LEFT JOIN products p ON p.id=t.product_id AND p.company_id=t.company_id
      LEFT JOIN product_packaging_options po ON po.id=t.packaging_option_id AND po.company_id=t.company_id AND po.product_id=t.product_id
      LEFT JOIN warehouses w ON w.id=COALESCE(t.warehouse_id,t.cf_wh_id) AND w.company_id=t.company_id AND w.location_id=t.location_id
      WHERE t.company_id=? AND t.location_id=?
      ORDER BY t.production_line_no, t.tag_code, t.id
    ");
    $st->execute([$companyId, $locationId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  if ($action === 'plc_tags_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = admin_validate_write_company((int)($d['company_id'] ?? 0));
    $locationId = (int)($d['location_id'] ?? ($GLOBALS['adminLocationId'] ?? 0));
    $tagCode = trim((string)($d['tag_code'] ?? ''));
    $tagName = trim((string)($d['tag_name'] ?? ''));
    $plcName = trim((string)($d['plc_name'] ?? ''));
    $ipAddress = trim((string)($d['ip_address'] ?? ''));
    $lineId = (int)($d['production_line_id'] ?? 0);
    $productId = (int)($d['product_id'] ?? 0);
    $packagingId = (int)($d['packaging_option_id'] ?? 0);
    $warehouseId = (int)($d['warehouse_id'] ?? 0);
    $qtyPerSignal = max(0, (float)($d['qty_per_signal'] ?? 1));
    $palletsPerSignal = max(0, (float)($d['pallets_per_signal'] ?? 1));
    $packagesPerSignal = max(0, (float)($d['packages_per_signal'] ?? 0));
    $piecesPerSignal = max(0, (float)($d['pieces_per_signal'] ?? 0));
    $enabled = (int)($d['enabled'] ?? 1) ? 1 : 0;
    $active = (int)($d['active'] ?? 1) ? 1 : 0;
    $notes = trim((string)($d['notes'] ?? ''));

    if ($companyId <= 0 || $locationId <= 0 || $tagCode === '' || $tagName === '' || $lineId <= 0) {
      jexit(['ok'=>false,'error'=>'Empresa, planta, código TAG, nombre TAG y línea son requeridos.'], 400);
    }
    $lineSt = $pdo->prepare("SELECT id, line_no, line_name FROM production_lines WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
    $lineSt->execute([$lineId, $companyId, $locationId]);
    $line = $lineSt->fetch();
    if (!$line) jexit(['ok'=>false,'error'=>'La línea seleccionada no existe o está inactiva en esta planta.'], 400);
    $lineNo = (int)$line['line_no'];
    $lineName = (string)$line['line_name'];

    if ($productId > 0) {
      $st = $pdo->prepare("SELECT id FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
      $st->execute([$productId, $companyId]);
      if (!$st->fetch()) jexit(['ok'=>false,'error'=>'El artículo seleccionado no pertenece a la empresa activa.'], 400);
    } else {
      $productId = null;
    }

    if ($packagingId > 0) {
      $st = $pdo->prepare("SELECT id FROM product_packaging_options WHERE id=? AND company_id=? AND product_id=? AND active=1 LIMIT 1");
      $st->execute([$packagingId, $companyId, $productId ?: 0]);
      if (!$st->fetch()) jexit(['ok'=>false,'error'=>'El embalaje seleccionado no corresponde al artículo.'], 400);
    } else {
      $packagingId = null;
    }

    if ($warehouseId > 0) {
      $st = $pdo->prepare("SELECT id, code FROM warehouses WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
      $st->execute([$warehouseId, $companyId, $locationId]);
      $wh = $st->fetch();
      if (!$wh) jexit(['ok'=>false,'error'=>'El almacén destino no pertenece a la planta activa.'], 400);
      if (preg_match('/^RUTA[-_ ]?[0-9]+$/i', (string)$wh['code'])) jexit(['ok'=>false,'error'=>'Producción no puede configurar TAG directo a una ruta móvil.'], 400);
    } else {
      $warehouseId = null;
    }

    $dupe = $pdo->prepare("SELECT id FROM production_plc_tags WHERE company_id=? AND location_id=? AND UPPER(tag_code)=UPPER(?) AND id<>? LIMIT 1");
    $dupe->execute([$companyId, $locationId, $tagCode, $id]);
    if ($dupe->fetch()) jexit(['ok'=>false,'error'=>'Ya existe un TAG con ese código en esta planta.'], 409);

    $notes = $notes !== '' ? mb_substr($notes, 0, 255) : null;
    $plcName = $plcName !== '' ? mb_substr($plcName, 0, 120) : null;
    $ipAddress = $ipAddress !== '' ? mb_substr($ipAddress, 0, 80) : null;

    if ($id > 0) {
      $st = $pdo->prepare("
        UPDATE production_plc_tags
        SET tag_code=?, tag_name=?, plc_name=?, ip_address=?, line_name=?, production_line_id=?, production_line_no=?,
            product_id=?, packaging_option_id=?, warehouse_id=?, cf_wh_id=?, qty_per_signal=?, pallets_per_signal=?, packages_per_signal=?, pieces_per_signal=?,
            enabled=?, active=?, notes=?, updated_at=NOW()
        WHERE id=? AND company_id=? AND location_id=?
      ");
      $st->execute([$tagCode,$tagName,$plcName,$ipAddress,$lineName,$lineId,$lineNo,$productId,$packagingId,$warehouseId,$warehouseId,$qtyPerSignal,$palletsPerSignal,$packagesPerSignal,$piecesPerSignal,$enabled,$active,$notes,$id,$companyId,$locationId]);
      jexit(['ok'=>true,'id'=>$id]);
    }

    $st = $pdo->prepare("
      INSERT INTO production_plc_tags(company_id, location_id, tag_code, tag_name, plc_name, ip_address, line_name, production_line_id, production_line_no,
        product_id, packaging_option_id, warehouse_id, cf_wh_id, qty_per_signal, pallets_per_signal, packages_per_signal, pieces_per_signal,
        signal_state, status, active, enabled, notes, created_at, updated_at)
      VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,0,'IDLE',?,?,?,NOW(),NOW())
    ");
    $st->execute([$companyId,$locationId,$tagCode,$tagName,$plcName,$ipAddress,$lineName,$lineId,$lineNo,$productId,$packagingId,$warehouseId,$warehouseId,$qtyPerSignal,$palletsPerSignal,$packagesPerSignal,$piecesPerSignal,$active,$enabled,$notes]);
    jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
  }

  /* ===================== Calidad: Conceptos (MERMA/SCRAP) ===================== */
  if ($action === 'quality_concepts_list') {
    $companyId = admin_current_company_id();
    $type = strtoupper(trim((string)($_GET['concept_type'] ?? 'MERMA')));
    if (!in_array($type, ['MERMA','SCRAP'], true)) $type = 'MERMA';

    $st = $pdo->prepare("
      SELECT qc.id, qc.company_id, c.name AS company_name, qc.concept_type, qc.name, qc.sort_order, qc.active
      FROM quality_concepts qc
      JOIN companies c ON c.id=qc.company_id
      WHERE (?=0 OR qc.company_id=?)
        AND qc.concept_type=?
      ORDER BY c.name, qc.sort_order, qc.name
    ");
    $st->execute([$companyId, $companyId, $type]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  if ($action === 'quality_concepts_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $type = strtoupper(trim((string)($d['concept_type'] ?? 'MERMA')));
    $name = trim((string)($d['name'] ?? ''));
    $sort = (int)($d['sort_order'] ?? 0);
    $active = (int)($d['active'] ?? 1) ? 1 : 0;

    if (!in_array($type, ['MERMA','SCRAP'], true)) $type = 'MERMA';
    if ($companyId<=0 || $name==='') {
      jexit(['ok'=>false,'error'=>'company_id y name son requeridos'], 400);
    }

    if ($id>0) {
      $st = $pdo->prepare("UPDATE quality_concepts SET company_id=?, concept_type=?, name=?, sort_order=?, active=? WHERE id=?");
      $st->execute([$companyId, $type, $name, $sort, $active, $id]);
      jexit(['ok'=>true,'id'=>$id]);
    } else {
      $st = $pdo->prepare("INSERT INTO quality_concepts(company_id, concept_type, name, sort_order, active) VALUES(?,?,?,?,?)");
      $st->execute([$companyId, $type, $name, $sort, $active]);
      jexit(['ok'=>true,'id'=>(int)$pdo->lastInsertId()]);
    }
  }



  /* ===================== Inventario: Ajuste Almacenes ===================== */
  if ($action === 'inventory_adjust_auth') {
    $d = read_json();
    $code = (string)($d['code'] ?? '');
    if (!inv_adjust_code_ok($code)) {
      jexit(['ok'=>false,'error'=>'Código incorrecto.'], 403);
    }
    jexit(['ok'=>true]);
  }

  if ($action === 'inventory_adjust_locations') {
    $companyId = admin_current_company_id();
    $st = $pdo->prepare("\n      SELECT l.id, l.company_id, c.name AS company_name, l.name, l.code\n      FROM locations l\n      JOIN companies c ON c.id = l.company_id\n      WHERE l.active = 1\n        AND (? = 0 OR l.company_id = ?)\n      ORDER BY c.name, l.name\n    ");
    $st->execute([$companyId, $companyId]);
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  if ($action === 'inventory_adjust_warehouses') {
    $companyId = admin_current_company_id();
    $locationId = (int)($_GET['location_id'] ?? 0);
    if ($companyId <= 0 || $locationId <= 0) jexit(['ok'=>false,'error'=>'Empresa y planta requeridas.'], 400);

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

  if ($action === 'inventory_adjust_products') {
    $companyId = admin_current_company_id();
    if ($companyId <= 0) jexit(['ok'=>false,'error'=>'Empresa requerida.'], 400);

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

  if ($action === 'inventory_adjust_snapshot') {
    $companyId = admin_current_company_id();
    $locationId = (int)($_GET['location_id'] ?? 0);
    if ($companyId <= 0 || $locationId <= 0) jexit(['ok'=>false,'error'=>'Empresa y planta requeridas.'], 400);

    $st = $pdo->prepare("\n      SELECT\n        p.id AS product_id,\n        p.sku,\n        p.name AS product_name,\n        p.uom,\n        w.id AS warehouse_id,\n        w.code AS warehouse_code,\n        w.name AS warehouse_name,\n        " . inv_balance_sql() . " AS qty\n      FROM products p\n      CROSS JOIN warehouses w\n      LEFT JOIN inventory_moves im\n             ON im.product_id = p.id\n            AND im.company_id = p.company_id\n            AND im.location_id = w.location_id\n            AND im.warehouse_id = w.id\n      WHERE p.company_id = ?\n        AND p.active = 1\n        AND w.company_id = ?\n        AND w.location_id = ?\n        AND w.active = 1\n      GROUP BY p.id, p.sku, p.name, p.uom, w.id, w.code, w.name\n      ORDER BY p.sku, FIELD(w.code,'CF1','CF2','CF3','CUB','SOB-CUB','APROV-MERMA'), w.code\n    ");
    $st->execute([$companyId, $companyId, $locationId]);
    $rows = $st->fetchAll();

    $tot = $pdo->prepare("\n      SELECT\n        p.id AS product_id,\n        p.sku,\n        p.name AS product_name,\n        p.uom,\n        " . inv_balance_sql() . " AS qty_total\n      FROM products p\n      LEFT JOIN inventory_moves im\n             ON im.product_id = p.id\n            AND im.company_id = p.company_id\n            AND im.location_id = ?\n      WHERE p.company_id = ?\n        AND p.active = 1\n      GROUP BY p.id, p.sku, p.name, p.uom\n      ORDER BY p.sku\n    ");
    $tot->execute([$locationId, $companyId]);

    jexit(['ok'=>true,'rows'=>$rows,'totals'=>$tot->fetchAll()]);
  }

  if ($action === 'inventory_adjust_save') {
    $d = read_json();
    $code = (string)($d['code'] ?? '');
    if (!inv_adjust_code_ok($code)) jexit(['ok'=>false,'error'=>'Código incorrecto.'], 403);

    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $locationId = (int)($d['location_id'] ?? 0);
    $warehouseId = (int)($d['warehouse_id'] ?? 0);
    $productId = (int)($d['product_id'] ?? 0);
    $qty = (float)($d['qty'] ?? 0);
    $kind = strtoupper(trim((string)($d['kind'] ?? 'ADD')));
    $notes = trim((string)($d['notes'] ?? ''));

    if ($companyId <= 0 || $locationId <= 0 || $warehouseId <= 0 || $productId <= 0 || $qty <= 0) {
      jexit(['ok'=>false,'error'=>'Empresa, planta, almacén, producto y cantidad son requeridos.'], 400);
    }
    if (!in_array($kind, ['ADD','SUB'], true)) $kind = 'ADD';

    $st = $pdo->prepare("SELECT id, code FROM warehouses WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
    $st->execute([$warehouseId, $companyId, $locationId]);
    $wh = $st->fetch();
    if (!$wh) jexit(['ok'=>false,'error'=>'Almacén inválido para la planta seleccionada.'], 400);

    $st = $pdo->prepare("SELECT id, sku, name FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $st->execute([$productId, $companyId]);
    $prod = $st->fetch();
    if (!$prod) jexit(['ok'=>false,'error'=>'Producto inválido para la empresa seleccionada.'], 400);

    $moveType = $kind === 'SUB' ? 'OUT' : 'ADJ';
    $u = current_user();
    $note = 'Ajuste almacén ' . ($kind === 'SUB' ? 'quita' : 'agrega') . ' | SKU: ' . (string)$prod['sku'] . ' | WH: ' . (string)$wh['code'];
    if ($notes !== '') $note .= ' | ' . $notes;
    $note = mb_substr($note, 0, 255);

    $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        ?, ?, ?, CURDATE(), ?, ?, ?,\n        NULL, 'AJUSTE_ALMACEN', NULL, ?, ?, NOW()\n      )\n    ");
    $st->execute([$companyId, $locationId, $warehouseId, $productId, $qty, $moveType, $note, (int)$u['id']]);

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


  if ($action === 'inventory_cut_save') {
    $d = read_json();
    $code = (string)($d['code'] ?? '');
    if (!inv_adjust_code_ok($code)) jexit(['ok'=>false,'error'=>'Código incorrecto.'], 403);

    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $locationId = (int)($d['location_id'] ?? 0);
    $warehouseId = (int)($d['warehouse_id'] ?? 0);
    $productId = (int)($d['product_id'] ?? 0);
    $targetQty = (float)($d['target_qty'] ?? 0);
    $notes = trim((string)($d['notes'] ?? ''));

    if ($companyId <= 0 || $locationId <= 0 || $warehouseId <= 0 || $productId <= 0 || $targetQty < 0) {
      jexit(['ok'=>false,'error'=>'Empresa, planta, almacén, producto y cantidad física objetivo son requeridos.'], 400);
    }

    $st = $pdo->prepare("SELECT id, code, name FROM warehouses WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
    $st->execute([$warehouseId, $companyId, $locationId]);
    $wh = $st->fetch();
    if (!$wh) jexit(['ok'=>false,'error'=>'Almacén inválido para la planta seleccionada.'], 400);

    $st = $pdo->prepare("SELECT id, sku, name FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $st->execute([$productId, $companyId]);
    $prod = $st->fetch();
    if (!$prod) jexit(['ok'=>false,'error'=>'Producto inválido para la empresa seleccionada.'], 400);

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

    if (abs($delta) <= 0.0001) {
      jexit([
        'ok'=>true,
        'id'=>0,
        'message'=>'Sin diferencia. El almacén ya tiene la cantidad física capturada.',
        'current_qty'=>$currentQty,
        'target_qty'=>$targetQty,
        'delta'=>0
      ]);
    }

    $moveType = $delta > 0 ? 'ADJ' : 'OUT';
    $qty = abs($delta);
    $u = current_user();

    $note = 'Corte físico WH | SKU: ' . (string)$prod['sku'] .
      ' | WH: ' . (string)$wh['code'] .
      ' | Ant: ' . number_format($currentQty, 3, '.', '') .
      ' | Obj: ' . number_format($targetQty, 3, '.', '') .
      ' | Dif: ' . number_format($delta, 3, '.', '');
    if ($notes !== '') $note .= ' | ' . $notes;
    $note = mb_substr($note, 0, 255);

    $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 (
        ?, ?, ?, CURDATE(), ?, ?, ?,
        NULL, 'CORTE_FISICO_WH', NULL, ?, ?, NOW()
      )
    ");
    $st->execute([$companyId, $locationId, $warehouseId, $productId, $qty, $moveType, $note, (int)$u['id']]);

    jexit([
      'ok'=>true,
      'id'=>(int)$pdo->lastInsertId(),
      'current_qty'=>$currentQty,
      'target_qty'=>$targetQty,
      'delta'=>$delta,
      'move_type'=>$moveType,
      'qty'=>$qty
    ]);
  }



  /* ===================== Auditoría: Kardex / Log operativo ===================== */
  if ($action === 'inventory_audit_log') {
    $companyId = admin_current_company_id();
    $locationId = (int)($_GET['location_id'] ?? 0);
    $productId = (int)($_GET['product_id'] ?? 0);
    $warehouseId = (int)($_GET['warehouse_id'] ?? 0);
    $moveType = strtoupper(trim((string)($_GET['move_type'] ?? '')));
    $refType = strtoupper(trim((string)($_GET['ref_type'] ?? '')));
    $userId = (int)($_GET['user_id'] ?? 0);
    $q = trim((string)($_GET['q'] ?? ''));
    $dateFrom = trim((string)($_GET['date_from'] ?? date('Y-m-01')));
    $dateTo = trim((string)($_GET['date_to'] ?? date('Y-m-d')));
    $page = max(1, (int)($_GET['page'] ?? 1));
    $perPage = (int)($_GET['per_page'] ?? 50);
    if (!in_array($perPage, [25,50,100,200], true)) $perPage = 50;
    $offset = ($page - 1) * $perPage;

    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateFrom)) $dateFrom = date('Y-m-01');
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $dateTo)) $dateTo = date('Y-m-d');
    if ($dateFrom > $dateTo) { $tmp = $dateFrom; $dateFrom = $dateTo; $dateTo = $tmp; }

    $where = ["im.work_date BETWEEN ? AND ?"];
    $params = [$dateFrom, $dateTo];

    if ($companyId > 0) { $where[] = "im.company_id = ?"; $params[] = $companyId; }
    if ($locationId > 0) { $where[] = "im.location_id = ?"; $params[] = $locationId; }
    if ($productId > 0) { $where[] = "im.product_id = ?"; $params[] = $productId; }
    if ($warehouseId > 0) { $where[] = "im.warehouse_id = ?"; $params[] = $warehouseId; }
    if (in_array($moveType, ['IN','OUT','ADJ'], true)) { $where[] = "UPPER(im.move_type) = ?"; $params[] = $moveType; }
    if ($refType !== '') { $where[] = "UPPER(im.ref_type) LIKE ?"; $params[] = '%' . $refType . '%'; }
    if ($userId > 0) { $where[] = "im.created_by = ?"; $params[] = $userId; }
    if ($q !== '') {
      $where[] = "(
        p.sku LIKE ? OR p.name LIKE ? OR COALESCE(w.code,'') LIKE ? OR
        COALESCE(im.ref_type,'') LIKE ? OR CAST(COALESCE(im.ref_id,0) AS CHAR) LIKE ? OR
        COALESCE(im.notes,'') LIKE ? OR COALESCE(u.full_name,'') LIKE ? OR COALESCE(u.username,'') LIKE ?
      )";
      $like = '%' . $q . '%';
      array_push($params, $like, $like, $like, $like, $like, $like, $like, $like);
    }

    $whereSql = implode(' AND ', $where);

    $sqlRows = "
      SELECT
        im.id,
        im.created_at,
        im.work_date,
        im.company_id,
        c.name AS company_name,
        im.location_id,
        l.name AS location_name,
        im.warehouse_id,
        COALESCE(w.code, 'SIN-WH') AS warehouse_code,
        COALESCE(w.name, '') AS warehouse_name,
        im.product_id,
        p.sku,
        p.name AS product_name,
        p.uom,
        UPPER(im.move_type) AS move_type,
        im.qty,
        CASE
          WHEN UPPER(im.move_type) IN ('IN','ADJ') THEN im.qty
          WHEN UPPER(im.move_type) = 'OUT' THEN -im.qty
          ELSE 0
        END AS signed_qty,
        im.ref_type,
        im.ref_id,
        im.created_by,
        COALESCE(u.full_name, u.username, CONCAT('Usuario #', im.created_by)) AS user_name,
        im.notes
      FROM inventory_moves im
      JOIN products p ON p.id = im.product_id
      LEFT JOIN companies c ON c.id = im.company_id
      LEFT JOIN locations l ON l.id = im.location_id
      LEFT JOIN warehouses w ON w.id = im.warehouse_id
      LEFT JOIN users u ON u.id = im.created_by
      WHERE $whereSql
      ORDER BY im.created_at DESC, im.id DESC
      LIMIT $perPage OFFSET $offset
    ";
    $st = $pdo->prepare($sqlRows);
    $st->execute($params);
    $rows = $st->fetchAll();

    $sqlSummary = "
      SELECT
        COALESCE(SUM(CASE WHEN UPPER(im.move_type)='IN' THEN im.qty ELSE 0 END),0) AS total_in,
        COALESCE(SUM(CASE WHEN UPPER(im.move_type)='OUT' THEN im.qty ELSE 0 END),0) AS total_out,
        COALESCE(SUM(CASE WHEN UPPER(im.move_type)='ADJ' THEN im.qty ELSE 0 END),0) AS total_adj,
        COALESCE(SUM(CASE
          WHEN UPPER(im.move_type) IN ('IN','ADJ') THEN im.qty
          WHEN UPPER(im.move_type)='OUT' THEN -im.qty
          ELSE 0
        END),0) AS net_qty,
        COUNT(*) AS moves_count
      FROM inventory_moves im
      JOIN products p ON p.id = im.product_id
      LEFT JOIN warehouses w ON w.id = im.warehouse_id
      LEFT JOIN users u ON u.id = im.created_by
      WHERE $whereSql
    ";
    $st = $pdo->prepare($sqlSummary);
    $st->execute($params);
    $summary = $st->fetch() ?: [];

    $sqlRef = "
      SELECT COALESCE(im.ref_type,'SIN_REF') AS ref_type, COUNT(*) AS moves_count,
             COALESCE(SUM(CASE WHEN UPPER(im.move_type) IN ('IN','ADJ') THEN im.qty WHEN UPPER(im.move_type)='OUT' THEN -im.qty ELSE 0 END),0) AS net_qty
      FROM inventory_moves im
      JOIN products p ON p.id = im.product_id
      LEFT JOIN warehouses w ON w.id = im.warehouse_id
      LEFT JOIN users u ON u.id = im.created_by
      WHERE $whereSql
      GROUP BY COALESCE(im.ref_type,'SIN_REF')
      ORDER BY moves_count DESC, ref_type ASC
      LIMIT 20
    ";
    $st = $pdo->prepare($sqlRef);
    $st->execute($params);
    $byRefType = $st->fetchAll();

    $sqlUser = "
      SELECT COALESCE(u.full_name, u.username, CONCAT('Usuario #', im.created_by)) AS user_name, COUNT(*) AS moves_count,
             COALESCE(SUM(CASE WHEN UPPER(im.move_type) IN ('IN','ADJ') THEN im.qty WHEN UPPER(im.move_type)='OUT' THEN -im.qty ELSE 0 END),0) AS net_qty
      FROM inventory_moves im
      JOIN products p ON p.id = im.product_id
      LEFT JOIN warehouses w ON w.id = im.warehouse_id
      LEFT JOIN users u ON u.id = im.created_by
      WHERE $whereSql
      GROUP BY COALESCE(u.full_name, u.username, CONCAT('Usuario #', im.created_by))
      ORDER BY moves_count DESC, user_name ASC
      LIMIT 20
    ";
    $st = $pdo->prepare($sqlUser);
    $st->execute($params);
    $byUser = $st->fetchAll();

    $totalRows = (int)($summary['moves_count'] ?? 0);
    $totalPages = max(1, (int)ceil($totalRows / max(1, $perPage)));

    jexit([
      'ok'=>true,
      'rows'=>$rows,
      'summary'=>$summary,
      'by_ref_type'=>$byRefType,
      'by_user'=>$byUser,
      'pagination'=>[
        'page'=>$page,
        'per_page'=>$perPage,
        'total_rows'=>$totalRows,
        'total_pages'=>$totalPages,
        'has_prev'=>$page > 1,
        'has_next'=>$page < $totalPages
      ]
    ]);
  }



  /* ===================== Inventario: Corrección controlada de movimientos ===================== */
  if ($action === 'inventory_move_get') {
    $id = (int)($_GET['id'] ?? 0);
    if ($id <= 0) jexit(['ok'=>false,'error'=>'ID de movimiento requerido.'], 400);

    $st = $pdo->prepare("
      SELECT
        im.id, im.created_at, im.work_date, im.company_id, c.name AS company_name,
        im.location_id, l.name AS location_name, im.warehouse_id,
        COALESCE(w.code,'SIN-WH') AS warehouse_code, COALESCE(w.name,'') AS warehouse_name,
        im.product_id, p.sku, p.name AS product_name, p.uom,
        UPPER(im.move_type) AS move_type, im.qty, im.ref_type, im.ref_id,
        im.created_by, COALESCE(u.full_name, u.username, CONCAT('Usuario #', im.created_by)) AS user_name,
        im.notes
      FROM inventory_moves im
      JOIN products p ON p.id = im.product_id
      LEFT JOIN companies c ON c.id = im.company_id
      LEFT JOIN locations l ON l.id = im.location_id
      LEFT JOIN warehouses w ON w.id = im.warehouse_id
      LEFT JOIN users u ON u.id = im.created_by
      WHERE im.id = ?
        AND im.company_id = ?
      LIMIT 1
    ");
    $st->execute([$id, admin_current_company_id()]);
    $row = $st->fetch();
    if (!$row) jexit(['ok'=>false,'error'=>'Movimiento no encontrado.'], 404);

    $prod = $pdo->prepare("
      SELECT id, sku, name, uom
      FROM products
      WHERE company_id = ? AND active = 1
      ORDER BY sku
    ");
    $prod->execute([(int)$row['company_id']]);

    $wh = $pdo->prepare("
      SELECT id, code, name
      FROM warehouses
      WHERE company_id = ? AND location_id = ? AND active = 1
      ORDER BY FIELD(code,'CF1','CF2','CF3','CUB','SOB-CUB','MERMA','APROV-MERMA'), code, name
    ");
    $wh->execute([(int)$row['company_id'], (int)$row['location_id']]);

    jexit([
      'ok'=>true,
      'row'=>$row,
      'products'=>$prod->fetchAll(),
      'warehouses'=>$wh->fetchAll()
    ]);
  }

  if ($action === 'inventory_move_update') {
    $d = read_json();
    $code = (string)($d['code'] ?? '');
    if (!inv_adjust_code_ok($code)) jexit(['ok'=>false,'error'=>'Código de autorización incorrecto.'], 403);

    $id = (int)($d['id'] ?? 0);
    $workDate = trim((string)($d['work_date'] ?? ''));
    $createdAt = trim((string)($d['created_at'] ?? ''));
    $productId = (int)($d['product_id'] ?? 0);
    $warehouseIdRaw = $d['warehouse_id'] ?? null;
    $warehouseId = ($warehouseIdRaw === null || $warehouseIdRaw === '' || (int)$warehouseIdRaw <= 0) ? null : (int)$warehouseIdRaw;
    $qty = round((float)($d['qty'] ?? 0), 3);
    $moveType = strtoupper(trim((string)($d['move_type'] ?? '')));
    $notes = trim((string)($d['notes'] ?? ''));
    $reason = trim((string)($d['reason'] ?? ''));

    if ($id <= 0) jexit(['ok'=>false,'error'=>'ID de movimiento requerido.'], 400);
    if (!preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) jexit(['ok'=>false,'error'=>'Fecha operativa inválida.'], 400);
    if ($createdAt !== '' && !preg_match('/^\d{4}-\d{2}-\d{2}T?\s?\d{2}:\d{2}(:\d{2})?$/', str_replace('T',' ', $createdAt))) {
      jexit(['ok'=>false,'error'=>'Fecha/hora inválida. Usa YYYY-MM-DD HH:MM:SS.'], 400);
    }
    $createdAt = str_replace('T', ' ', $createdAt);
    if (preg_match('/^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}$/', $createdAt)) $createdAt .= ':00';
    if ($createdAt === '') $createdAt = $workDate . ' 00:00:00';
    if ($productId <= 0 || $qty <= 0 || !in_array($moveType, ['IN','OUT','ADJ'], true)) {
      jexit(['ok'=>false,'error'=>'Producto, cantidad y tipo de movimiento son requeridos.'], 400);
    }
    if ($reason === '') {
      jexit(['ok'=>false,'error'=>'Indica un motivo de corrección para dejar trazabilidad.'], 400);
    }

    $pdo->beginTransaction();
    try {
      $st = $pdo->prepare("SELECT * FROM inventory_moves WHERE id = ? AND company_id = ? FOR UPDATE");
      $st->execute([$id, admin_current_company_id()]);
      $old = $st->fetch(PDO::FETCH_ASSOC);
      if (!$old) throw new RuntimeException('Movimiento no encontrado.');

      $companyId = (int)$old['company_id'];
      $locationId = (int)$old['location_id'];

      $chk = $pdo->prepare("SELECT id FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
      $chk->execute([$productId, $companyId]);
      if (!$chk->fetch()) throw new RuntimeException('Producto inválido para la empresa del movimiento.');

      if ($warehouseId !== null) {
        $chk = $pdo->prepare("SELECT id FROM warehouses WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
        $chk->execute([$warehouseId, $companyId, $locationId]);
        if (!$chk->fetch()) throw new RuntimeException('Almacén inválido para la planta del movimiento.');
      }

      $notes = $notes !== '' ? mb_substr($notes, 0, 255) : null;

      $upd = $pdo->prepare("
        UPDATE inventory_moves
        SET work_date = ?,
            created_at = ?,
            product_id = ?,
            warehouse_id = ?,
            qty = ?,
            move_type = ?,
            notes = ?
        WHERE id = ?
      ");
      $upd->execute([$workDate, $createdAt, $productId, $warehouseId, $qty, $moveType, $notes, $id]);

      $st = $pdo->prepare("SELECT * FROM inventory_moves WHERE id = ? LIMIT 1");
      $st->execute([$id]);
      $new = $st->fetch(PDO::FETCH_ASSOC) ?: [];

      $log = $pdo->prepare("
        INSERT INTO inventory_move_admin_edits (
          inventory_move_id, company_id, location_id, old_data, new_data, reason, edited_by, edited_at
        ) VALUES (?, ?, ?, ?, ?, ?, ?, NOW())
      ");
      $u = current_user();
      $log->execute([
        $id,
        $companyId,
        $locationId,
        inv_move_snapshot_json($old),
        inv_move_snapshot_json($new),
        mb_substr($reason, 0, 255),
        (int)$u['id']
      ]);

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



  /* ===================== Cubicado: validador de capturas ===================== */
  if ($action === 'cubing_audit_list') {
    if (!admin_table_exists($pdo, 'cubing_entries')) {
      jexit(['ok'=>false,'error'=>'No existe la tabla cubing_entries.'], 404);
    }

    $companyId = admin_current_company_id();
    $locationId = (int)($_GET['location_id'] ?? 0);
    $productId = (int)($_GET['product_id'] ?? 0);
    $shift = (int)($_GET['shift'] ?? 0);
    $q = trim((string)($_GET['q'] ?? ''));
    $date = trim((string)($_GET['date'] ?? date('Y-m-d')));

    if (!admin_date_ok($date)) $date = date('Y-m-d');

    $where = ["ce.work_date = ?"];
    $params = [$date];

    if ($companyId > 0) { $where[] = "ce.company_id = ?"; $params[] = $companyId; }
    if ($locationId > 0) { $where[] = "ce.location_id = ?"; $params[] = $locationId; }
    if (in_array($shift, [1,2,3], true)) { $where[] = "ce.shift = ?"; $params[] = $shift; }
    if ($productId > 0) { $where[] = "ce.output_product_id = ?"; $params[] = $productId; }
    if ($q !== '') {
      $where[] = "(
        p_in.sku LIKE ? OR p_in.name LIKE ? OR
        p_out.sku LIKE ? OR p_out.name LIKE ? OR
        COALESCE(ce.source_mode,'') LIKE ? OR
        CAST(ce.id AS CHAR) LIKE ?
      )";
      $like = '%' . $q . '%';
      array_push($params, $like, $like, $like, $like, $like, $like);
    }

    $whereSql = implode(' AND ', $where);

    $sql = "
      SELECT
        ce.id,
        ce.company_id,
        c.name AS company_name,
        ce.location_id,
        l.name AS location_name,
        ce.work_date,
        ce.shift,
        ce.entry_time,
        ce.input_product_id,
        p_in.sku  AS producto_origen_sku,
        p_in.name AS producto_origen,
        ce.output_product_id,
        p_out.sku AS producto_producido_sku,
        p_out.name AS producto_producido,
        ce.output_qty,
        ce.bars_used,
        ce.bars_taken,
        ce.bars_leftover,
        ce.warehouse_id,
        w_dest.code AS almacen_destino,
        ce.source_warehouse_id,
        w_src.code AS almacen_origen,
        ce.leftover_warehouse_id,
        w_left.code AS almacen_sobrante,
        ce.source_mode,
        ce.created_by,
        COALESCE(u.full_name, u.username, CONCAT('Usuario #', ce.created_by)) AS user_name,

        COUNT(im.id) AS inventory_moves_count,
        COALESCE(SUM(CASE
          WHEN im.move_type = 'OUT' AND im.product_id = ce.input_product_id THEN im.qty
          ELSE 0
        END),0) AS inv_out_bars,
        COALESCE(SUM(CASE
          WHEN im.move_type IN ('IN','ADJ') AND im.product_id = ce.output_product_id THEN im.qty
          ELSE 0
        END),0) AS inv_in_output,
        COALESCE(SUM(CASE
          WHEN im.move_type IN ('IN','ADJ')
           AND im.product_id = ce.input_product_id
           AND im.warehouse_id = ce.leftover_warehouse_id
          THEN im.qty ELSE 0
        END),0) AS inv_in_leftover
      FROM cubing_entries ce
      LEFT JOIN companies c ON c.id = ce.company_id
      LEFT JOIN locations l ON l.id = ce.location_id
      LEFT JOIN products p_in  ON p_in.id = ce.input_product_id
      LEFT JOIN products p_out ON p_out.id = ce.output_product_id
      LEFT JOIN warehouses w_dest ON w_dest.id = ce.warehouse_id
      LEFT JOIN warehouses w_src  ON w_src.id = ce.source_warehouse_id
      LEFT JOIN warehouses w_left ON w_left.id = ce.leftover_warehouse_id
      LEFT JOIN users u ON u.id = ce.created_by
      LEFT JOIN inventory_moves im
             ON im.ref_type = 'CUBING'
            AND im.ref_id = ce.id
            AND im.company_id = ce.company_id
            AND im.location_id = ce.location_id
      WHERE {$whereSql}
      GROUP BY
        ce.id, ce.company_id, c.name, ce.location_id, l.name, ce.work_date, ce.shift, ce.entry_time,
        ce.input_product_id, p_in.sku, p_in.name, ce.output_product_id, p_out.sku, p_out.name,
        ce.output_qty, ce.bars_used, ce.bars_taken, ce.bars_leftover, ce.warehouse_id, w_dest.code,
        ce.source_warehouse_id, w_src.code, ce.leftover_warehouse_id, w_left.code, ce.source_mode,
        ce.created_by, u.full_name, u.username
      ORDER BY ce.entry_time DESC, ce.id DESC
      LIMIT 500
    ";

    $st = $pdo->prepare($sql);
    $st->execute($params);
    $rows = $st->fetchAll(PDO::FETCH_ASSOC);

    $okCount = 0;
    $warnCount = 0;
    $errorCount = 0;
    $totalOutput = 0.0;
    $totalBarsUsed = 0.0;
    $totalLeftover = 0.0;

    foreach ($rows as &$r) {
      $sourceMode = strtoupper(trim((string)($r['source_mode'] ?? 'CUB')));
      $barsUsed = (float)($r['bars_used'] ?? 0);
      $barsTaken = (float)($r['bars_taken'] ?? 0);
      $barsLeftover = (float)($r['bars_leftover'] ?? 0);
      $outputQty = (float)($r['output_qty'] ?? 0);

      $expectedOutBars = ($sourceMode === 'SOB-CUB') ? $barsUsed : (($barsTaken > 0) ? $barsTaken : $barsUsed);

      $issues = [];
      if ((int)($r['inventory_moves_count'] ?? 0) <= 0) {
        $issues[] = 'Sin movimientos de inventario ligados a CUBING.';
      }
      if (abs(((float)($r['inv_out_bars'] ?? 0)) - $expectedOutBars) > 0.0005) {
        $issues[] = 'Diferencia en salida de barra: esperado ' . number_format($expectedOutBars, 3, '.', '') . ', inventario ' . number_format((float)($r['inv_out_bars'] ?? 0), 3, '.', '') . '.';
      }
      if (abs(((float)($r['inv_in_output'] ?? 0)) - $outputQty) > 0.0005) {
        $issues[] = 'Diferencia en entrada de producto terminado: esperado ' . number_format($outputQty, 3, '.', '') . ', inventario ' . number_format((float)($r['inv_in_output'] ?? 0), 3, '.', '') . '.';
      }
      if ($barsLeftover > 0.0005 && abs(((float)($r['inv_in_leftover'] ?? 0)) - $barsLeftover) > 0.0005) {
        $issues[] = 'Diferencia en sobrante generado: esperado ' . number_format($barsLeftover, 3, '.', '') . ', inventario ' . number_format((float)($r['inv_in_leftover'] ?? 0), 3, '.', '') . '.';
      }

      $r['expected_out_bars'] = $expectedOutBars;
      $r['issues'] = $issues;
      $r['status'] = empty($issues) ? 'OK' : 'REVISAR';

      if (empty($issues)) $okCount++;
      else {
        $warnCount++;
        $errorCount++;
      }

      $totalOutput += $outputQty;
      $totalBarsUsed += $barsUsed;
      $totalLeftover += $barsLeftover;
    }
    unset($r);

    jexit([
      'ok'=>true,
      'date'=>$date,
      'rows'=>$rows,
      'summary'=>[
        'entries_count'=>count($rows),
        'ok_count'=>$okCount,
        'review_count'=>$warnCount,
        'error_count'=>$errorCount,
        'output_qty'=>$totalOutput,
        'bars_used'=>$totalBarsUsed,
        'bars_leftover'=>$totalLeftover
      ]
    ]);
  }


  /* ===================== Productos: reglas de almacén por localidad ===================== */
  if ($action === 'product_wh_rules_get') {
    $companyId = admin_current_company_id();
    $productId = (int)($_GET['product_id'] ?? 0);
    if ($companyId <= 0 || $productId <= 0) jexit(['ok'=>false,'error'=>'Empresa y producto requeridos.'], 400);

    $st = $pdo->prepare("SELECT id, sku, name FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $st->execute([$productId, $companyId]);
    $product = $st->fetch();
    if (!$product) jexit(['ok'=>false,'error'=>'Producto inválido para la empresa seleccionada.'], 404);

    $locSt = $pdo->prepare("
      SELECT id, name, code
      FROM locations
      WHERE company_id=? AND active=1
      ORDER BY name
    ");
    $locSt->execute([$companyId]);
    $locations = $locSt->fetchAll();

    $ruleSt = $pdo->prepare("
      SELECT *
      FROM product_warehouse_rules
      WHERE company_id=? AND product_id=? AND location_id=?
      LIMIT 1
    ");
    $whSt = $pdo->prepare("
      SELECT id, code, name
      FROM warehouses
      WHERE company_id=? AND location_id=? AND active=1
      ORDER BY FIELD(code,'CF1','CF2','CF3','CUB','MERMA','APROV-MERMA','SOB-CUB'), code, name
    ");

    $rows = [];
    foreach ($locations as $loc) {
      $lid = (int)$loc['id'];
      $ruleSt->execute([$companyId, $productId, $lid]);
      $rule = $ruleSt->fetch() ?: [];
      $whSt->execute([$companyId, $lid]);
      $rows[] = [
        'location_id' => $lid,
        'location_name' => (string)$loc['name'],
        'location_code' => (string)$loc['code'],
        'warehouses' => $whSt->fetchAll(),
        'rule' => [
          'default_finished_warehouse_id' => (int)($rule['default_finished_warehouse_id'] ?? 0),
          'default_sales_warehouse_id' => (int)($rule['default_sales_warehouse_id'] ?? 0),
          'merma_warehouse_id' => (int)($rule['merma_warehouse_id'] ?? 0),
          'aprovechamiento_warehouse_id' => (int)($rule['aprovechamiento_warehouse_id'] ?? 0),
          'cubicado_warehouse_id' => (int)($rule['cubicado_warehouse_id'] ?? 0),
          'notes' => (string)($rule['notes'] ?? ''),
          'active' => (int)($rule['active'] ?? 1),
        ]
      ];
    }

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

  if ($action === 'product_wh_rules_save') {
    $d = read_json();
    $companyId = (int)($d['company_id'] ?? 0);
    $companyId = admin_validate_write_company($companyId);
    $locationId = (int)($d['location_id'] ?? 0);
    $productId = (int)($d['product_id'] ?? 0);
    $notes = trim((string)($d['notes'] ?? ''));
    $active = (int)($d['active'] ?? 1) ? 1 : 0;

    $fields = [
      'default_finished_warehouse_id',
      'default_sales_warehouse_id',
      'merma_warehouse_id',
      'aprovechamiento_warehouse_id',
      'cubicado_warehouse_id'
    ];
    $vals = [];
    foreach ($fields as $f) $vals[$f] = (int)($d[$f] ?? 0);

    if ($companyId <= 0 || $locationId <= 0 || $productId <= 0) {
      jexit(['ok'=>false,'error'=>'Empresa, localidad y producto requeridos.'], 400);
    }

    $st = $pdo->prepare("SELECT id FROM products WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $st->execute([$productId, $companyId]);
    if (!$st->fetch()) jexit(['ok'=>false,'error'=>'Producto inválido.'], 400);

    $st = $pdo->prepare("SELECT id FROM locations WHERE id=? AND company_id=? AND active=1 LIMIT 1");
    $st->execute([$locationId, $companyId]);
    if (!$st->fetch()) jexit(['ok'=>false,'error'=>'Localidad inválida.'], 400);

    $whCheck = $pdo->prepare("SELECT id FROM warehouses WHERE id=? AND company_id=? AND location_id=? AND active=1 LIMIT 1");
    foreach ($fields as $f) {
      if ($vals[$f] <= 0) { $vals[$f] = null; continue; }
      $whCheck->execute([$vals[$f], $companyId, $locationId]);
      if (!$whCheck->fetch()) jexit(['ok'=>false,'error'=>'Almacén inválido para la localidad seleccionada en campo '.$f], 400);
    }
    if ($notes === '') $notes = null;

    $st = $pdo->prepare("
      INSERT INTO product_warehouse_rules (
        company_id, location_id, product_id,
        default_finished_warehouse_id, default_sales_warehouse_id, merma_warehouse_id,
        aprovechamiento_warehouse_id, cubicado_warehouse_id,
        notes, active, created_at, updated_at
      ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW(), NOW())
      ON DUPLICATE KEY UPDATE
        default_finished_warehouse_id = VALUES(default_finished_warehouse_id),
        default_sales_warehouse_id = VALUES(default_sales_warehouse_id),
        merma_warehouse_id = VALUES(merma_warehouse_id),
        aprovechamiento_warehouse_id = VALUES(aprovechamiento_warehouse_id),
        cubicado_warehouse_id = VALUES(cubicado_warehouse_id),
        notes = VALUES(notes),
        active = VALUES(active),
        updated_at = NOW()
    ");
    $st->execute([
      $companyId, $locationId, $productId,
      $vals['default_finished_warehouse_id'], $vals['default_sales_warehouse_id'], $vals['merma_warehouse_id'],
      $vals['aprovechamiento_warehouse_id'], $vals['cubicado_warehouse_id'],
      $notes, $active
    ]);

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

  /* ===================== Análisis de datos: explorador seguro ===================== */
  function data_safe_identifier(string $name): string {
    $name = trim($name);
    if (!preg_match('/^[A-Za-z0-9_]+$/', $name)) {
      throw new RuntimeException('Identificador inválido: ' . $name);
    }
    return $name;
  }

  function data_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 data_columns(PDO $pdo, string $table): array {
    data_safe_identifier($table);
    $st = $pdo->prepare("\n      SELECT column_name, column_type, data_type, is_nullable, column_key, column_default, extra\n      FROM information_schema.columns\n      WHERE table_schema = DATABASE() AND table_name = ?\n      ORDER BY ordinal_position\n    ");
    $st->execute([$table]);
    return $st->fetchAll();
  }

  function data_fk_relationships(PDO $pdo, array $tables): array {
    $rels = [];
    if (!$tables) return $rels;
    $in = implode(',', array_fill(0, count($tables), '?'));
    $params = array_merge($tables, $tables);
    $st = $pdo->prepare("\n      SELECT\n        kcu.table_name AS left_table,\n        kcu.column_name AS left_column,\n        kcu.referenced_table_name AS right_table,\n        kcu.referenced_column_name AS right_column,\n        kcu.constraint_name\n      FROM information_schema.key_column_usage kcu\n      WHERE kcu.table_schema = DATABASE()\n        AND kcu.referenced_table_name IS NOT NULL\n        AND kcu.table_name IN ($in)\n        AND kcu.referenced_table_name IN ($in)\n    ");
    $st->execute($params);
    foreach ($st->fetchAll() as $r) {
      $rels[] = [
        'left_table'=>(string)$r['left_table'],
        'left_column'=>(string)$r['left_column'],
        'right_table'=>(string)$r['right_table'],
        'right_column'=>(string)$r['right_column'],
        'confidence'=>'alta',
        'reason'=>'Relación por llave foránea declarada: ' . (string)$r['constraint_name']
      ];
    }
    return $rels;
  }

  function data_known_relationships(array $tables): array {
    $known = [
      ['inventory_moves','company_id','companies','id','Inventario pertenece a empresa'],
      ['inventory_moves','location_id','locations','id','Inventario pertenece a localidad'],
      ['inventory_moves','warehouse_id','warehouses','id','Movimiento apunta al almacén real'],
      ['inventory_moves','product_id','products','id','Movimiento apunta al producto'],
      ['inventory_moves','created_by','users','id','Usuario que registró movimiento'],
      ['products','company_id','companies','id','Producto pertenece a empresa'],
      ['warehouses','company_id','companies','id','Almacén pertenece a empresa'],
      ['warehouses','location_id','locations','id','Almacén pertenece a localidad'],
      ['locations','company_id','companies','id','Localidad pertenece a empresa'],
      ['production_entries','company_id','companies','id','Producción pertenece a empresa'],
      ['production_entries','location_id','locations','id','Producción pertenece a localidad'],
      ['production_entries','product_id','products','id','Producción apunta a producto'],
      ['quality_entries','company_id','companies','id','Calidad pertenece a empresa'],
      ['quality_entries','location_id','locations','id','Calidad pertenece a localidad'],
      ['quality_entries','product_id','products','id','Calidad apunta a producto'],
      ['quality_entries','concept_id','quality_concepts','id','Calidad apunta a concepto'],
      ['warehouse_location_requests','company_id','companies','id','Solicitud pertenece a empresa'],
      ['warehouse_location_requests','request_location_id','locations','id','Localidad solicitante'],
      ['warehouse_location_requests','source_location_id','locations','id','Localidad proveedora'],
      ['warehouse_location_requests','requested_by','users','id','Usuario solicitante'],
      ['warehouse_location_request_items','request_id','warehouse_location_requests','id','Detalle pertenece a solicitud'],
      ['warehouse_location_request_items','product_id','products','id','Detalle apunta a producto'],
      ['warehouse_location_request_items','source_warehouse_id','warehouses','id','Almacén origen transferencia'],
      ['warehouse_location_request_items','destination_warehouse_id','warehouses','id','Almacén destino transferencia'],
      ['outbounds','company_id','companies','id','Salida pertenece a empresa'],
      ['outbounds','location_id','locations','id','Salida pertenece a localidad'],
      ['outbound_items','outbound_id','outbounds','id','Detalle pertenece a salida'],
      ['outbound_items','product_id','products','id','Detalle apunta a producto'],
      ['sales_orders','company_id','companies','id','Pedido pertenece a empresa'],
      ['sales_orders','location_id','locations','id','Pedido pertenece a localidad'],
      ['sales_order_items','order_id','sales_orders','id','Detalle pertenece a pedido'],
      ['sales_order_items','product_id','products','id','Detalle apunta a producto'],
      ['product_warehouse_rules','company_id','companies','id','Regla pertenece a empresa'],
      ['product_warehouse_rules','location_id','locations','id','Regla pertenece a localidad'],
      ['product_warehouse_rules','product_id','products','id','Regla apunta a producto'],
      ['product_warehouse_rules','default_finished_warehouse_id','warehouses','id','Almacén entrada / producto terminado configurado'],
      ['product_warehouse_rules','default_sales_warehouse_id','warehouses','id','Almacén salida comercial configurado'],
      ['product_warehouse_rules','merma_warehouse_id','warehouses','id','Almacén merma configurado'],
      ['product_warehouse_rules','aprovechamiento_warehouse_id','warehouses','id','Almacén aprovechamiento configurado'],
      ['product_warehouse_rules','cubicado_warehouse_id','warehouses','id','Almacén cubicado configurado'],
    ];
    $set = array_flip($tables);
    $rels = [];
    foreach ($known as $k) {
      if (isset($set[$k[0]]) && isset($set[$k[2]])) {
        $rels[] = ['left_table'=>$k[0],'left_column'=>$k[1],'right_table'=>$k[2],'right_column'=>$k[3],'confidence'=>'alta','reason'=>$k[4]];
      }
    }
    return $rels;
  }

  function data_heuristic_relationships(PDO $pdo, array $tables): array {
    $cols = [];
    foreach ($tables as $t) {
      $cols[$t] = array_map(fn($c)=>(string)$c['column_name'], data_columns($pdo, $t));
    }
    $rels = [];
    foreach ($tables as $lt) {
      foreach ($cols[$lt] as $lc) {
        if ($lc === 'id' || !str_ends_with($lc, '_id')) continue;
        $base = substr($lc, 0, -3);
        foreach ($tables as $rt) {
          if ($rt === $lt) continue;
          $candidates = [$base, $base . 's', $base . 'es'];
          $singularRt = rtrim($rt, 's');
          if (in_array($rt, $candidates, true) || $singularRt === $base || str_contains($rt, $base)) {
            if (in_array('id', $cols[$rt] ?? [], true)) {
              $rels[] = ['left_table'=>$lt,'left_column'=>$lc,'right_table'=>$rt,'right_column'=>'id','confidence'=>'media','reason'=>'Sugerida por convención de columna *_id'];
            }
          }
        }
      }
    }
    return $rels;
  }

  function data_relationships(PDO $pdo, array $tables): array {
    $tables = array_values(array_unique(array_filter(array_map('trim', $tables))));
    foreach ($tables as $t) { data_safe_identifier($t); if (!data_table_exists($pdo, $t)) throw new RuntimeException('Tabla no existe: ' . $t); }
    $all = array_merge(data_fk_relationships($pdo, $tables), data_known_relationships($tables), data_heuristic_relationships($pdo, $tables));
    $seen = [];
    $out = [];
    foreach ($all as $r) {
      $key = implode('|', [$r['left_table'],$r['left_column'],$r['right_table'],$r['right_column']]);
      if (isset($seen[$key])) continue;
      $seen[$key] = true;
      $out[] = $r;
    }
    return $out;
  }

  function data_build_query(PDO $pdo, array $tables): string {
    $tables = array_values(array_unique(array_filter(array_map('trim', $tables))));
    if (!$tables) throw new RuntimeException('Selecciona al menos una tabla.');
    foreach ($tables as $t) { data_safe_identifier($t); if (!data_table_exists($pdo, $t)) throw new RuntimeException('Tabla no existe: ' . $t); }
    if (count($tables) === 1) return "SELECT t1.*\nFROM `{$tables[0]}` t1\nLIMIT 100";

    $rels = data_relationships($pdo, $tables);
    $aliases = [];
    foreach ($tables as $i=>$t) $aliases[$t] = 't' . ($i+1);
    $joined = [$tables[0]=>true];
    $sql = "SELECT\n  " . implode(",\n  ", array_map(fn($t)=>$aliases[$t] . '.*', $tables)) . "\nFROM `{$tables[0]}` {$aliases[$tables[0]]}";
    $pending = array_slice($tables, 1);
    while ($pending) {
      $progress = false;
      foreach ($pending as $idx=>$t) {
        $relUse = null; $direction = '';
        foreach ($rels as $r) {
          if ($r['left_table'] === $t && isset($joined[$r['right_table']])) { $relUse=$r; $direction='left_to_joined'; break; }
          if ($r['right_table'] === $t && isset($joined[$r['left_table']])) { $relUse=$r; $direction='right_to_joined'; break; }
        }
        if ($relUse) {
          if ($direction === 'left_to_joined') {
            $sql .= "\nLEFT JOIN `{$t}` {$aliases[$t]} ON {$aliases[$t]}.`{$relUse['left_column']}` = {$aliases[$relUse['right_table']]}.`{$relUse['right_column']}`";
          } else {
            $sql .= "\nLEFT JOIN `{$t}` {$aliases[$t]} ON {$aliases[$relUse['left_table']]}.`{$relUse['left_column']}` = {$aliases[$t]}.`{$relUse['right_column']}`";
          }
          $joined[$t] = true; unset($pending[$idx]); $pending = array_values($pending); $progress = true; break;
        }
      }
      if (!$progress) {
        $t = array_shift($pending);
        $sql .= "\n-- Sin relación confiable detectada para {$t}; se agrega separado para revisión.\n-- JOIN `{$t}` {$aliases[$t]} ON /* define condición */";
        $joined[$t] = true;
      }
    }
    return $sql . "\nLIMIT 100";
  }

  function data_prebuilt_queries(): array {
    return [
      'inventario_actual' => [
        'title'=>'Inventario actual por empresa/localidad/producto/almacén',
        'sql'=>"SELECT c.name AS empresa, l.name AS localidad, p.sku, p.name AS producto, w.code AS almacen, w.name AS almacen_nombre, ROUND(SUM(CASE WHEN im.move_type IN ('IN','ADJ') THEN im.qty WHEN im.move_type='OUT' THEN -im.qty ELSE 0 END),3) AS existencia\nFROM inventory_moves im\nJOIN companies c ON c.id = im.company_id\nJOIN locations l ON l.id = im.location_id\nJOIN products p ON p.id = im.product_id\nLEFT JOIN warehouses w ON w.id = im.warehouse_id\nGROUP BY c.name, l.name, p.sku, p.name, w.code, w.name\nHAVING ABS(existencia) > 0.001\nORDER BY c.name, l.name, p.sku, w.code\nLIMIT 500"
      ],
      'movimientos_recientes' => [
        'title'=>'Últimos movimientos de inventario',
        'sql'=>"SELECT im.id, im.created_at, im.work_date, c.name AS empresa, l.name AS localidad, p.sku, p.name AS producto, COALESCE(w.code,'SIN-WH') AS almacen, im.move_type, im.qty, im.ref_type, im.ref_id, COALESCE(u.full_name,u.username) AS usuario, im.notes\nFROM inventory_moves im\nJOIN companies c ON c.id=im.company_id\nJOIN locations l ON l.id=im.location_id\nJOIN products p ON p.id=im.product_id\nLEFT JOIN warehouses w ON w.id=im.warehouse_id\nLEFT JOIN users u ON u.id=im.created_by\nORDER BY im.id DESC\nLIMIT 200"
      ],
      'produccion_mes' => [
        'title'=>'Producción registrada por mes/producto',
        'sql'=>"SELECT c.name AS empresa, l.name AS localidad, DATE_FORMAT(im.work_date,'%Y-%m') AS mes, p.sku, p.name AS producto, SUM(im.qty) AS cantidad\nFROM inventory_moves im\nJOIN companies c ON c.id=im.company_id\nJOIN locations l ON l.id=im.location_id\nJOIN products p ON p.id=im.product_id\nWHERE im.ref_type IN ('PRODUCTION','PRODUCTION_MERMA') AND im.move_type IN ('IN','ADJ')\nGROUP BY c.name, l.name, DATE_FORMAT(im.work_date,'%Y-%m'), p.sku, p.name\nORDER BY mes DESC, empresa, localidad, p.sku\nLIMIT 300"
      ],
      'solicitudes_localidades' => [
        'title'=>'Solicitudes entre localidades y estatus',
        'sql'=>"SELECT r.id, c.name AS empresa, rl.name AS solicita, sl.name AS provee, r.status, r.request_date, r.required_date, COALESCE(u.full_name,u.username) AS solicitado_por, r.driver_name, r.vehicle_unit, r.reject_reason\nFROM warehouse_location_requests r\nJOIN companies c ON c.id=r.company_id\nJOIN locations rl ON rl.id=r.request_location_id\nJOIN locations sl ON sl.id=r.source_location_id\nLEFT JOIN users u ON u.id=r.requested_by\nORDER BY r.id DESC\nLIMIT 200"
      ],
      'reglas_almacen_producto' => [
        'title'=>'Reglas de almacén configuradas por producto/localidad',
        'sql'=>"SELECT c.name AS empresa, l.name AS localidad, p.sku, p.name AS producto, wi.code AS almacen_entrada, wo.code AS almacen_salida, wm.code AS almacen_merma, wa.code AS almacen_aprov, wc.code AS almacen_cubicado\nFROM product_warehouse_rules r\nJOIN companies c ON c.id=r.company_id\nJOIN locations l ON l.id=r.location_id\nJOIN products p ON p.id=r.product_id\nLEFT JOIN warehouses wi ON wi.id=r.default_finished_warehouse_id\nLEFT JOIN warehouses wo ON wo.id=r.default_sales_warehouse_id\nLEFT JOIN warehouses wm ON wm.id=r.merma_warehouse_id\nLEFT JOIN warehouses wa ON wa.id=r.aprovechamiento_warehouse_id\nLEFT JOIN warehouses wc ON wc.id=r.cubicado_warehouse_id\nORDER BY c.name, l.name, p.sku\nLIMIT 500"
      ]
    ];
  }

  if ($action === 'data_tables') {
    $st = $pdo->query("\n      SELECT table_name, table_rows, engine, create_time, update_time\n      FROM information_schema.tables\n      WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE'\n      ORDER BY table_name\n    ");
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  if ($action === 'data_columns') {
    $table = data_safe_identifier((string)($_GET['table'] ?? ''));
    if (!data_table_exists($pdo, $table)) jexit(['ok'=>false,'error'=>'Tabla no existe.'], 404);
    jexit(['ok'=>true,'rows'=>data_columns($pdo, $table)]);
  }

  if ($action === 'data_relationships') {
    $tables = array_filter(array_map('trim', explode(',', (string)($_GET['tables'] ?? ''))));
    try { jexit(['ok'=>true,'rows'=>data_relationships($pdo, $tables)]); }
    catch(Throwable $e){ jexit(['ok'=>false,'error'=>$e->getMessage()], 400); }
  }

  if ($action === 'data_generate_query') {
    $d = read_json();
    $tables = is_array($d['tables'] ?? null) ? $d['tables'] : [];
    try { jexit(['ok'=>true,'sql'=>data_build_query($pdo, $tables),'relationships'=>data_relationships($pdo, $tables)]); }
    catch(Throwable $e){ jexit(['ok'=>false,'error'=>$e->getMessage()], 400); }
  }

  if ($action === 'data_preset_queries') {
    $rows = [];
    foreach (data_prebuilt_queries() as $key=>$q) $rows[] = ['key'=>$key,'title'=>$q['title'],'sql'=>$q['sql']];
    jexit(['ok'=>true,'rows'=>$rows]);
  }

  if ($action === 'data_query_run') {
    $d = read_json();
    $sql = trim((string)($d['sql'] ?? ''));
    if ($sql === '') jexit(['ok'=>false,'error'=>'Consulta requerida.'], 400);
    $clean = rtrim($sql, " \t\n\r\0\x0B;");
    if (preg_match('/;\s*\S/', $sql)) jexit(['ok'=>false,'error'=>'Solo se permite una consulta por ejecución.'], 400);
    if (!preg_match('/^\s*(SELECT|WITH)\b/i', $clean)) jexit(['ok'=>false,'error'=>'Solo se permiten consultas SELECT/WITH de solo lectura.'], 400);
    if (preg_match('/\b(INSERT|UPDATE|DELETE|DROP|ALTER|CREATE|TRUNCATE|REPLACE|GRANT|REVOKE|CALL|LOAD|INTO\s+OUTFILE|LOCK|UNLOCK)\b/i', $clean)) {
      jexit(['ok'=>false,'error'=>'Consulta bloqueada: contiene palabra reservada de escritura o administración.'], 400);
    }
    if (!preg_match('/\bLIMIT\s+\d+/i', $clean)) $clean .= "\nLIMIT 200";
    try {
      $st = $pdo->query($clean);
      $rows = $st ? $st->fetchAll(PDO::FETCH_ASSOC) : [];
      $cols = $rows ? array_keys($rows[0]) : [];
      jexit(['ok'=>true,'sql'=>$clean,'columns'=>$cols,'rows'=>$rows,'count'=>count($rows)]);
    } catch(Throwable $e) {
      jexit(['ok'=>false,'error'=>$e->getMessage()], 400);
    }
  }




  /* ===================== Usuarios + Roles + Accesos ===================== */
  if ($action === 'roles_list') {
    jexit(['ok'=>true,'rows'=>db_roles_list($pdo)]);
  }

  if ($action === 'users_list') {
    $q = trim((string)($_GET['q'] ?? ''));
    if ($q !== '') {
      $st = $pdo->prepare("
        SELECT id, username, full_name, email, phone, active, created_at
        FROM users
        WHERE username LIKE ? OR full_name LIKE ? OR email LIKE ?
        ORDER BY id DESC
        LIMIT 300
      ");
      $like = "%{$q}%";
      $st->execute([$like,$like,$like]);
      jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
    }

    $st = $pdo->query("
      SELECT id, username, full_name, email, phone, active, created_at
      FROM users
      ORDER BY id DESC
      LIMIT 300
    ");
    jexit(['ok'=>true,'rows'=>$st->fetchAll()]);
  }

  if ($action === 'users_get') {
    $id = (int)($_GET['id'] ?? 0);
    if ($id<=0) jexit(['ok'=>false,'error'=>'id requerido'], 400);

    $st = $pdo->prepare("
      SELECT id, username, full_name, email, phone, active
      FROM users
      WHERE id=?
      LIMIT 1
    ");
    $st->execute([$id]);
    $row = $st->fetch();
    if (!$row) jexit(['ok'=>false,'error'=>'Usuario no existe'], 404);

    $roles = db_user_roles($pdo, $id);
    $access = db_user_access($pdo, $id);
    jexit(['ok'=>true,'row'=>$row,'roles'=>$roles,'access'=>$access]);
  }

  if ($action === 'users_save') {
    $d = read_json();
    $id = (int)($d['id'] ?? 0);

    $username = trim((string)($d['username'] ?? ''));
    $fullName = trim((string)($d['full_name'] ?? ''));
    $email    = trim((string)($d['email'] ?? ''));
    $phone    = trim((string)($d['phone'] ?? ''));
    $active   = (int)($d['active'] ?? 1) ? 1 : 0;

    $password = (string)($d['password'] ?? ''); // opcional (solo si se envía)
    $roleCodes   = is_array($d['role_codes'] ?? null) ? (array)$d['role_codes'] : [];
    $companyIds  = is_array($d['company_ids'] ?? null) ? (array)$d['company_ids'] : [];
    $locationIds = is_array($d['location_ids'] ?? null) ? (array)$d['location_ids'] : [];

    if ($username==='' || $fullName==='') {
      jexit(['ok'=>false,'error'=>'username y full_name son requeridos'], 400);
    }

    // username único
    if ($id>0) {
      $st = $pdo->prepare("SELECT id FROM users WHERE username=? AND id<>? LIMIT 1");
      $st->execute([$username, $id]);
      if ($st->fetch()) jexit(['ok'=>false,'error'=>'username ya existe'], 409);
    } else {
      $st = $pdo->prepare("SELECT id FROM users WHERE username=? LIMIT 1");
      $st->execute([$username]);
      if ($st->fetch()) jexit(['ok'=>false,'error'=>'username ya existe'], 409);
    }

    // Insert/Update
    if ($id>0) {
      if ($password !== '') {
        $hash = password_hash($password, PASSWORD_DEFAULT);
        $st = $pdo->prepare("
          UPDATE users
          SET username=?, full_name=?, email=?, phone=?, active=?, pass_hash=?, updated_at=NOW()
          WHERE id=?
        ");
        $st->execute([$username,$fullName,$email?:null,$phone?:null,$active,$hash,$id]);
      } else {
        $st = $pdo->prepare("
          UPDATE users
          SET username=?, full_name=?, email=?, phone=?, active=?, updated_at=NOW()
          WHERE id=?
        ");
        $st->execute([$username,$fullName,$email?:null,$phone?:null,$active,$id]);
      }
      $userId = $id;
    } else {
      if ($password === '') {
        jexit(['ok'=>false,'error'=>'password requerido al crear usuario'], 400);
      }
      $hash = password_hash($password, PASSWORD_DEFAULT);
      $st = $pdo->prepare("
        INSERT INTO users(username, full_name, email, phone, pass_hash, active, created_at)
        VALUES(?,?,?,?,?,?,NOW())
      ");
      $st->execute([$username,$fullName,$email?:null,$phone?:null,$hash,$active]);
      $userId = (int)$pdo->lastInsertId();
    }

    // Roles + accesos
    try {
      db_set_user_roles($pdo, $userId, $roleCodes);
      db_set_user_access($pdo, $userId, $companyIds, $locationIds);
    } catch (Throwable $e) {
      jexit(['ok'=>false,'error'=>'Guardó usuario pero falló roles/accesos: '.$e->getMessage()], 500);
    }

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

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

/* ===================== UI ===================== */
$u = current_user();
$csrf = csrf_token();
?>
<!doctype html>
<html lang="es">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>BReport · Admin Configuración</title>
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
  <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet">
  <style>
    :root{
      --navy:#0f355d;
      --navy-2:#174f86;
      --navy-3:#eaf2fb;
      --blue:#1f6feb;
      --blue-soft:#eff6ff;
      --green:#118454;
      --amber:#bd7600;
      --red:#c73946;
      --ink:#12263f;
      --muted:#65758b;
      --surface:#ffffff;
      --surface-2:#f5f8fc;
      --border:#dbe5f1;
      --shadow:0 18px 44px rgba(14,39,68,.08);
      --shadow-soft:0 5px 16px rgba(14,39,68,.05);
      --radius:20px;
    }

    *{box-sizing:border-box}
    body{
      margin:0;
      background:#f1f5fa;
      color:var(--ink);
      font-family:Inter, "Segoe UI", Arial, sans-serif;
    }
    .mono{font-family:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;}
    .muted{color:var(--muted)!important}
    .btn-pill{border-radius:999px}
    .btn{
      font-weight:600;
      border-radius:11px;
    }
    .btn-sm{border-radius:10px}
    .btn-primary{
      background:var(--blue);
      border-color:var(--blue);
    }
    .form-control,.form-select{
      border-radius:11px;
      border-color:var(--border);
      min-height:40px;
    }
    .form-control:focus,.form-select:focus{
      border-color:#76a8ef;
      box-shadow:0 0 0 .2rem rgba(31,111,235,.12);
    }

    /* Encabezado corporativo */
    .adminTopbar{
      min-height:74px;
      padding:0 24px;
      background:linear-gradient(115deg,var(--navy),var(--navy-2));
      box-shadow:0 3px 14px rgba(11,31,56,.18);
    }
    .adminBrand{
      display:flex;
      align-items:center;
      gap:13px;
      color:#fff;
      text-decoration:none;
    }
    .adminBrandIcon{
      width:46px;height:46px;border-radius:14px;
      display:flex;align-items:center;justify-content:center;
      background:rgba(255,255,255,.13);
      border:1px solid rgba(255,255,255,.18);
      font-size:1.4rem;
    }
    .adminBrandTitle{
      font-weight:800;
      font-size:1.27rem;
      line-height:1.15;
      letter-spacing:-.02em;
    }
    .adminBrandSub{
      font-size:.8rem;
      color:rgba(255,255,255,.75);
    }
    .adminUser{
      color:rgba(255,255,255,.87);
      display:flex;
      align-items:center;
      gap:8px;
      font-size:.88rem;
      font-weight:600;
    }
    .adminUser .avatar{
      width:34px;height:34px;border-radius:50%;
      display:flex;align-items:center;justify-content:center;
      background:rgba(255,255,255,.16);
      color:#fff;
    }
    .adminTopbar .topAction{
      border-radius:999px;
      border:1px solid rgba(255,255,255,.3);
      color:#fff;
      background:rgba(255,255,255,.07);
      font-weight:600;
    }
    .adminTopbar .topAction:hover{
      background:#fff;
      color:var(--navy);
    }
    .adminTopbar .exitAction{
      border-color:rgba(255,151,157,.55);
      color:#ffe4e7;
    }

    /* Workspace: menú tipo aplicación + panel de trabajo */
    .adminShell{
      max-width:1720px;
      padding:22px 22px 28px;
    }
    .stickyTop{
      background:var(--surface);
      border:1px solid var(--border);
      border-radius:var(--radius);
      box-shadow:var(--shadow-soft);
      padding:20px 16px 16px;
      margin-bottom:18px;
    }
    .adminIntro{
      padding:2px 8px 16px;
      border-bottom:1px solid #e7eef7;
      margin-bottom:12px;
    }
    .adminIntroTitle{
      font-size:1.28rem;
      font-weight:800;
      letter-spacing:-.02em;
      margin-bottom:3px;
    }
    .scopeCard{
      background:var(--blue-soft);
      border:1px solid #d5e5ff;
      border-radius:14px;
      padding:12px;
      margin-top:14px;
    }
    .scopeLabel{
      text-transform:uppercase;
      letter-spacing:.06em;
      font-size:.67rem;
      font-weight:800;
      color:#637ca0;
    }
    .scopeCompany{
      font-size:1rem;
      font-weight:750;
      color:var(--navy);
      margin-top:2px;
    }
    .scopeLocation{
      font-size:.82rem;
      color:#5f7390;
    }
    .scopeControls .form-select{
      background:#fff;
      font-size:.88rem;
    }
    .scopeControls .btn{
      width:100%;
      margin-top:9px;
    }

    .adminHub{margin:0;}
    .cardHub{
      cursor:pointer;
      border:1px solid transparent;
      background:transparent;
      border-radius:14px;
      transition:all .16s ease;
      box-shadow:none;
    }
    .cardHub .card-body{
      min-height:64px;
      padding:9px 10px;
      gap:11px!important;
    }
    .cardHub:hover{
      background:#f2f7fd;
      transform:none;
      border-color:#e2ebf6;
      box-shadow:none;
    }
    .cardHub.active{
      background:linear-gradient(90deg,#eaf3ff,#f4f8ff);
      border-color:#c8ddfb;
      box-shadow:inset 3px 0 0 var(--blue);
    }
    .cardHub .fw-semibold{
      color:var(--ink);
      font-size:.92rem;
    }
    .cardHub .small{
      font-size:.74rem;
      line-height:1.25;
      max-width:230px;
    }
    .hubIcon{
      width:39px;height:39px;border-radius:11px;
      display:flex;align-items:center;justify-content:center;
      background:#e7f0ff!important;
      color:var(--blue)!important;
      font-size:1.08rem;
      flex:0 0 auto;
    }
    .cardHub[data-panel="wh"] .hubIcon,
    .cardHub[data-panel="routes"] .hubIcon,
    .cardHub[data-panel="qual"] .hubIcon{background:#e8f6f1!important;color:var(--green)!important;}
    .cardHub[data-panel="invadj"] .hubIcon{background:#fff4df!important;color:var(--amber)!important;}
    .cardHub[data-panel="audit"] .hubIcon{background:#e6f5fb!important;color:#087990!important;}
    .cardHub[data-panel="cubaudit"] .hubIcon{background:#ffeef2!important;color:#c2185b!important;}
    .cardHub[data-panel="users"] .hubIcon,
    .cardHub[data-panel="data"] .hubIcon{background:#f2eefe!important;color:#6741d9!important;}

    .mainPanelCard{
      margin-top:0!important;
      border:1px solid var(--border)!important;
      border-radius:var(--radius)!important;
      box-shadow:var(--shadow)!important;
      background:var(--surface);
      min-height:calc(100vh - 124px);
    }
    .mainPanelCard>.card-body{padding:26px;}
    .panel{display:none}
    .panel.show{display:block;animation:panelEnter .16s ease;}
    @keyframes panelEnter{from{opacity:.45;transform:translateY(3px)}to{opacity:1;transform:none}}
    .panel>.d-flex:first-child{
      padding-bottom:18px;
      border-bottom:1px solid #e9f0f7;
      margin-bottom:18px;
    }
    .panel>.d-flex:first-child .fs-5{
      color:var(--navy);
      font-size:1.28rem!important;
      letter-spacing:-.02em;
    }

    /* Tablas / formularios de nivel operativo */
    .table-responsive{
      background:#fff;
      border:1px solid #e4edf7;
      border-radius:15px;
      overflow:auto;
    }
    .table{margin-bottom:0;}
    .table thead th{
      background:#f5f8fc;
      color:#64748b;
      font-size:.73rem;
      text-transform:uppercase;
      letter-spacing:.035em;
      font-weight:750;
      padding:.78rem .7rem;
      border-bottom:1px solid #dde8f4;
      white-space:nowrap;
    }
    .table tbody td{
      padding:.72rem .7rem;
      border-color:#edf2f8;
      vertical-align:middle;
    }
    .table tbody tr:hover{background:#fbfdff;}
    .badge{border-radius:999px;padding:.38em .7em;font-weight:650}
    .alert{border-radius:13px;border-width:1px}

    .checkgrid{
      max-height:240px;overflow:auto;
      border:1px solid var(--border);
      border-radius:13px;padding:10px;background:#fff;
    }
    .checkgrid .form-check{margin-bottom:6px}
    #panel-data textarea{resize:vertical;}
    .miniHint{font-size:.85rem;color:#6c757d}
    .invAdjustLocked{filter:blur(1px);opacity:.45;pointer-events:none}
    .qty-pos{color:#198754;font-weight:700}
    .qty-neg{color:#dc3545;font-weight:700}
    .qty-zero{color:#6c757d;font-weight:700}
    .whRuleCard{
      border:1px solid var(--border);
      border-radius:16px;background:#fff;padding:14px;margin-bottom:12px;
    }
    .whRuleTitle{font-weight:800;color:#0c2340}
    .whRuleGrid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}



    /* Mejoras visuales dinámicas: alertas, buscadores y paginado */
    .appToastStack{
      position:fixed;right:18px;top:86px;z-index:2090;
      display:flex;flex-direction:column;gap:10px;width:min(420px,calc(100vw - 28px));
      pointer-events:none;
    }
    .appToast{
      pointer-events:auto;background:#fff;border:1px solid var(--border);border-left:5px solid var(--blue);
      box-shadow:0 18px 48px rgba(15,36,61,.18);border-radius:16px;padding:13px 14px;
      display:grid;grid-template-columns:34px 1fr auto;gap:10px;align-items:flex-start;
      animation:toastIn .18s ease both;
    }
    .appToast.success{border-left-color:var(--green)}
    .appToast.warning{border-left-color:var(--amber)}
    .appToast.danger{border-left-color:var(--red)}
    .appToast.info{border-left-color:var(--blue)}
    .appToastIcon{width:34px;height:34px;border-radius:11px;display:flex;align-items:center;justify-content:center;background:#eef5ff;color:var(--blue)}
    .appToast.success .appToastIcon{background:#eaf8f0;color:var(--green)}
    .appToast.warning .appToastIcon{background:#fff6df;color:var(--amber)}
    .appToast.danger .appToastIcon{background:#fff0f2;color:var(--red)}
    .appToastTitle{font-weight:800;color:#0c2340;line-height:1.15}
    .appToastMsg{font-size:.88rem;color:#5f6f86;margin-top:3px;white-space:pre-wrap;word-break:break-word}
    .appToastClose{border:0;background:transparent;color:#7b8797;font-size:1.1rem;line-height:1;padding:2px 4px}
    @keyframes toastIn{from{opacity:0;transform:translateY(-8px) scale(.98)}to{opacity:1;transform:none}}

    .sectionTools{
      margin-top:14px;padding:12px;border:1px solid #e2ebf6;border-radius:15px;
      background:linear-gradient(180deg,#fbfdff,#f7faff);
      display:flex;flex-wrap:wrap;align-items:center;gap:10px;
    }
    .sectionTools .searchWrap{position:relative;flex:1 1 280px;min-width:220px}
    .sectionTools .searchWrap i{position:absolute;left:12px;top:50%;transform:translateY(-50%);color:#7890ad}
    .sectionTools input.tableSearch{padding-left:36px;background:#fff}
    .sectionTools .countPill{border:1px solid #d7e5f6;background:#fff;color:#385575;border-radius:999px;padding:7px 11px;font-size:.82rem;font-weight:750}
    .sectionPager{margin-top:10px;display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:10px}
    .sectionPager .pageInfo{font-size:.84rem;color:#64748b}
    .table-responsive.adminPagedTable{max-height:560px;overflow:auto}
    .table-responsive.adminPagedTable thead th{position:sticky;top:0;z-index:2}
    .emptyFilterRow td{padding:28px!important;color:#64748b;text-align:center;background:#fbfdff!important}

    @media (max-width:767.98px){
      .appToastStack{top:76px;right:10px;width:calc(100vw - 20px)}
      .sectionTools{padding:10px}.sectionTools .searchWrap{flex-basis:100%}
      .sectionPager{justify-content:center}.sectionPager .pageInfo{width:100%;text-align:center}
    }

    .modal-content{
      border:0;
      border-radius:20px;
      overflow:hidden;
      box-shadow:0 25px 70px rgba(15,36,61,.25);
    }
    .modal-header{
      background:#f5f8fc;
      border-bottom:1px solid #e5edf7;
      padding:18px 22px;
    }
    .modal-body{padding:22px;}
    .modal-footer{padding:15px 22px;background:#fbfcfe;border-top:1px solid #e5edf7;}

    @media (min-width:1200px){
      .adminShell{
        display:grid;
        grid-template-columns:324px minmax(0,1fr);
        gap:20px;
        align-items:start;
      }
      .stickyTop{
        position:sticky;
        top:18px;
        margin-bottom:0;
      }
      .stickyTop>.d-flex{
        display:block!important;
      }
      .stickyTop>.d-flex>.ms-auto{
        display:block!important;
        margin-left:0!important;
      }
      .scopeControls{display:block!important;}
      .adminHub{
        display:flex!important;
        flex-direction:column;
        gap:4px!important;
      }
      .adminHub>[class*="col-"]{
        flex:0 0 auto;
        width:100%;
        padding:0!important;
      }
    }

    @media (max-width:1199.98px){
      .stickyTop{padding:17px;}
      .adminHub{margin-top:15px!important;}
      .cardHub{background:#fff;border-color:#e5edf7;}
      .mainPanelCard{min-height:0;}
    }
    @media (max-width:767.98px){
      .adminTopbar{padding:12px 14px;min-height:68px;}
      .adminBrandSub{display:none;}
      .adminUser span:not(.avatar){display:none;}
      .adminShell{padding:12px;}
      .stickyTop>.d-flex>.ms-auto{margin-left:0!important;width:100%;margin-top:14px;}
      .scopeControls{width:100%;flex-wrap:wrap;}
      .scopeControls label{width:100%;}
      .scopeControls .form-select{flex:1;min-width:0!important;}
      .scopeControls .btn{width:auto;margin-top:0;}
      .mainPanelCard>.card-body{padding:15px;}
      .panel>.d-flex:first-child{align-items:flex-start!important;}
    }
  </style>
</head>
<body>
<nav class="navbar adminTopbar">
  <div class="container-fluid px-0">
    <div class="adminBrand">
      <div class="adminBrandIcon"><i class="bi bi-grid-1x2-fill"></i></div>
      <div>
        <div class="adminBrandTitle">BReport · Centro Administrativo</div>
        <div class="adminBrandSub">Configuración operativa, logística y trazabilidad MES</div>
      </div>
    </div>
    <div class="ms-auto d-flex gap-2 align-items-center">
      <div class="adminUser me-2">
        <span class="avatar"><i class="bi bi-person-fill"></i></span>
        <span><?=h($u['full_name'])?></span>
      </div>
      <a class="btn btn-sm topAction" href="dashboard.php"><i class="bi bi-speedometer2 me-1"></i>Dashboard</a>
      <a class="btn btn-sm topAction exitAction" href="logout.php"><i class="bi bi-box-arrow-right me-1"></i>Salir</a>
    </div>
  </div>
</nav>
<div id="appToastStack" class="appToastStack" aria-live="polite" aria-atomic="true"></div>

<div class="container-fluid adminShell">

  <div class="stickyTop">
    <div class="adminIntro">
      <div class="adminIntroTitle">Administración</div>
      <div class="muted small">Módulos de configuración y control operativo.</div>
      <div class="scopeCard">
        <div class="scopeLabel">Empresa / planta activa</div>
        <div class="scopeCompany"><?= htmlspecialchars($adminCompanyName, ENT_QUOTES, 'UTF-8') ?></div>
        <div class="scopeLocation"><i class="bi bi-geo-alt me-1"></i><?= htmlspecialchars($adminLocationName, ENT_QUOTES, 'UTF-8') ?></div>
        <div class="scopeControls mt-3">
          <select id="companyFilter" class="form-select form-select-sm" disabled aria-label="Empresa activa de configuración"></select>
          <button id="btnReload" class="btn btn-outline-primary btn-sm btn-pill">
            <i class="bi bi-arrow-repeat me-1"></i>Actualizar configuración
          </button>
        </div>
      </div>
    </div>

    <div class="scopeLabel px-2 mb-2">Módulos disponibles</div>
    <!-- HUB Cards -->
    <div class="row g-3 mt-1 adminHub">
      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub active" data-panel="loc">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon"><i class="bi bi-geo-alt"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Localidades</div>
              <div class="muted small">Sucursales, plantas y horarios</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="wh">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(32,201,151,.12);color:#087f5b"><i class="bi bi-houses"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Almacenes</div>
              <div class="muted small">Almacenes por localidad</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="prod">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon"><i class="bi bi-box-seam"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Productos</div>
              <div class="muted small">Catálogo e identificadores ERP</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="pack">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(13,110,253,.10);color:#0d6efd"><i class="bi bi-boxes"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Empaque y tarimas</div>
              <div class="muted small">Embalajes y capacidades</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="plcprod">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(255,193,7,.16);color:#b58100"><i class="bi bi-cpu"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">PLC Producción</div>
              <div class="muted small">Líneas y TAGs de conteo</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="routes">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(25,135,84,.10);color:#198754"><i class="bi bi-signpost-split"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Rutas</div>
              <div class="muted small">Rutas de entrega y acceso</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="chan">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon"><i class="bi bi-truck"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Canales</div>
              <div class="muted small">Tipos de salida operativa</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="qual">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(25,135,84,.08);color:#198754"><i class="bi bi-shield-check"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Conceptos Calidad</div>
              <div class="muted small">Merma y desperdicio</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="users">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(111,66,193,.08);color:#6f42c1"><i class="bi bi-people"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Usuarios & Accesos</div>
              <div class="muted small">Permisos y accesos</div>
            </div>
            <i class="bi bi-chevron-right muted"></i>
          </div>
        </div>
      </div>


      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="invadj">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(255,193,7,.12);color:#b58100"><i class="bi bi-sliders"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Ajuste almacenes</div>
              <div class="muted small">Corrección controlada</div>
            </div>
            <i class="bi bi-lock-fill muted"></i>
          </div>
        </div>
      </div>


      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="audit">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(13,202,240,.12);color:#087990"><i class="bi bi-clipboard-data"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Auditoría movimientos</div>
              <div class="muted small">Kardex y trazabilidad</div>
            </div>
            <i class="bi bi-search muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="cubaudit">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(255,99,132,.10);color:#c2185b"><i class="bi bi-boxes"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Validar cubicado</div>
              <div class="muted small">Conciliación de conversiones</div>
            </div>
            <i class="bi bi-clipboard-check muted"></i>
          </div>
        </div>
      </div>

      <div class="col-12 col-md-6 col-xl-4">
        <div class="card cardHub" data-panel="data">
          <div class="card-body d-flex gap-3 align-items-center">
            <div class="hubIcon" style="background:rgba(102,16,242,.10);color:#6610f2"><i class="bi bi-database-check"></i></div>
            <div class="flex-grow-1">
              <div class="fw-semibold">Análisis de datos</div>
              <div class="muted small">Consultas de solo lectura</div>
            </div>
            <i class="bi bi-braces muted"></i>
          </div>
        </div>
      </div>

    </div>
  </div>

  <!-- Panels -->
  <div class="card shadow-sm rounded-4 mt-4 mainPanelCard">
    <div class="card-body">

      <!-- Localidades -->
      <div class="panel show" id="panel-loc">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-geo-alt me-2"></i>Localidades</div>
            <div class="muted small">Administra localidades por empresa</div>
          </div>
          <div class="ms-auto">
            <button class="btn btn-primary btn-sm btn-pill" id="addLoc"><i class="bi bi-plus-lg me-1"></i>Nueva localidad</button>
          </div>
        </div>
        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>ID</th><th>Empresa</th><th>Nombre</th><th class="mono">Code</th><th>Timezone</th><th>Activa</th><th></th>
              </tr>
            </thead>
            <tbody id="locBody"></tbody>
          </table>
        </div>
      </div>

      <!-- Almacenes -->
      <div class="panel" id="panel-wh">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-houses me-2"></i>Almacenes</div>
            <div class="muted small">Administra almacenes por empresa y localidad. Estos almacenes son usados por inventory_moves y reglas de producto.</div>
          </div>
          <div class="ms-auto">
            <button class="btn btn-primary btn-sm btn-pill" id="addWh"><i class="bi bi-plus-lg me-1"></i>Nuevo almacén</button>
          </div>
        </div>
        <div class="alert alert-info mt-3 mb-2 py-2">
          Recomendado: <span class="mono">CF1</span>, <span class="mono">CF2</span>, <span class="mono">CF3</span>, <span class="mono">CUB</span>, <span class="mono">MERMA</span>, <span class="mono">APROV-MERMA</span>, <span class="mono">SOB-CUB</span>.
          Cada localidad debe tener sus propios almacenes activos. No afecta inventario por sí solo.
        </div>
        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>ID</th><th>Empresa</th><th>Localidad</th><th class="mono">Código</th><th>Nombre</th><th>Activo</th><th></th>
              </tr>
            </thead>
            <tbody id="whBody"></tbody>
          </table>
        </div>
      </div>


      <!-- Productos -->
      <div class="panel" id="panel-prod">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-box-seam me-2"></i>Productos</div>
            <div class="muted small">Catálogo por empresa</div>
          </div>
          <div class="ms-auto">
            <button class="btn btn-primary btn-sm btn-pill" id="addProd"><i class="bi bi-plus-lg me-1"></i>Nuevo producto</button>
          </div>
        </div>
        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>ID</th><th>Empresa</th><th class="mono">Artículo</th><th>Descripción</th><th class="mono"># DLAX</th><th>UOM</th><th>Presentación logística</th><th>Activo</th><th>Almacenes</th><th></th>
              </tr>
            </thead>
            <tbody id="prodBody"></tbody>
          </table>
        </div>
      </div>

      <!-- Empaque y tarimas -->
      <div class="panel" id="panel-pack">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-boxes me-2"></i>Empaque y tarimas</div>
            <div class="muted small">Define unidades logísticas por artículo de la empresa seleccionada.</div>
          </div>
        </div>
        <div class="alert alert-info mt-3 mb-2 py-2">
          Un mismo artículo puede tener <b>más de un tipo de embalaje</b>, por ejemplo una presentación de 600 ml
          en tarimas de 1296 o 1080 piezas. Esta configuración no modifica inventario ni movimientos.
        </div>
        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th class="mono">Artículo / # DLAX</th><th>Descripción</th><th>Tipo de embalaje</th>
                <th class="text-end">Pzas / paquete</th><th class="text-end">Paquetes / tarima</th>
                <th class="text-end">Pzas totales</th><th>Estado</th><th></th>
              </tr>
            </thead>
            <tbody id="packBody"></tbody>
          </table>
        </div>
      </div>

      <!-- Producción PLC / Automatización -->
      <div class="panel" id="panel-plcprod">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-cpu me-2"></i>PLC Producción · Botella</div>
            <div class="muted small">Configura líneas y TAGs de conteo para reportaje automático en la planta activa.</div>
          </div>
          <div class="ms-auto d-flex flex-wrap gap-2">
            <button class="btn btn-outline-primary btn-sm btn-pill" id="btnPlcReload"><i class="bi bi-arrow-repeat me-1"></i>Actualizar</button>
            <button class="btn btn-primary btn-sm btn-pill" id="addProdLine"><i class="bi bi-plus-lg me-1"></i>Nueva línea</button>
            <button class="btn btn-warning btn-sm btn-pill" id="addPlcTag"><i class="bi bi-plus-lg me-1"></i>Nuevo TAG</button>
          </div>
        </div>
        <div class="alert alert-info mt-3 mb-3 py-2">
          Para Planta Botella normalmente se configuran <b>Línea 1</b>, <b>Línea 2</b> y <b>Línea 3</b>. Cada TAG queda ligado a una línea, artículo, embalaje y almacén destino. La producción automática tomará estos datos cuando se inicie sesión en modo PLC.
        </div>
        <div class="row g-3">
          <div class="col-12 col-xl-4">
            <div class="border rounded-4 p-3 h-100 bg-light">
              <div class="fw-bold mb-2"><i class="bi bi-list-ol me-1"></i>Líneas configuradas</div>
              <div id="prodLineCards" class="d-grid gap-2"></div>
            </div>
          </div>
          <div class="col-12 col-xl-8">
            <div class="table-responsive">
              <table class="table table-sm align-middle">
                <thead>
                  <tr>
                    <th>Línea</th><th class="mono">TAG</th><th>PLC / IP</th><th>Artículo</th><th>Destino</th><th>Señal</th><th>Estado</th><th></th>
                  </tr>
                </thead>
                <tbody id="plcTagBody"></tbody>
              </table>
            </div>
          </div>
        </div>
      </div>

      <!-- Rutas comerciales -->
      <div class="panel" id="panel-routes">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-signpost-split me-2"></i>Rutas</div>
            <div class="muted small">Catálogo único de rutas por empresa y localidad, compatible con accesos, Ventas y Almacén.</div>
          </div>
          <div class="ms-auto">
            <button class="btn btn-success btn-sm btn-pill" id="addRoute"><i class="bi bi-plus-lg me-1"></i>Nueva ruta</button>
          </div>
        </div>
        <div class="alert alert-info mt-3 mb-2 py-2">
          Se utiliza el catálogo existente <span class="mono">route_access_codes</span> y el campo <span class="mono">route_no</span> de órdenes de venta.
          Este catálogo no genera salidas ni movimientos por sí solo.
        </div>
        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>Localidad</th><th class="text-end">Ruta</th><th class="mono">Código</th><th>Nombre</th>
                <th>Operador / Unidad</th><th>Días</th><th>Activo</th><th></th>
              </tr>
            </thead>
            <tbody id="routeBody"></tbody>
          </table>
        </div>
      </div>

      <!-- Canales -->
      <div class="panel" id="panel-chan">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-truck me-2"></i>Canales</div>
            <div class="muted small">Canales de salida por empresa</div>
          </div>
          <div class="ms-auto">
            <button class="btn btn-primary btn-sm btn-pill" id="addChan"><i class="bi bi-plus-lg me-1"></i>Nuevo canal</button>
          </div>
        </div>
        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>ID</th><th>Empresa</th><th>Nombre</th><th>Tipo</th><th>Orden</th><th>Activo</th><th></th>
              </tr>
            </thead>
            <tbody id="chanBody"></tbody>
          </table>
        </div>
      </div>

      <!-- Calidad -->
      <div class="panel" id="panel-qual">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-shield-check me-2"></i>Conceptos Calidad</div>
            <div class="muted small">Configura conceptos por empresa: Merma / Scrap</div>
          </div>
          <div class="ms-auto d-flex gap-2 align-items-center">
            <select id="qcType" class="form-select form-select-sm" style="min-width:160px">
              <option value="MERMA">MERMA</option>
              <option value="SCRAP">SCRAP</option>
            </select>
            <button class="btn btn-success btn-sm btn-pill" id="addQc"><i class="bi bi-plus-lg me-1"></i>Nuevo concepto</button>
          </div>
        </div>
        <div class="miniHint mt-2">Tip: puedes ordenar (sort_order) para controlar el orden en captura.</div>

        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>ID</th><th>Empresa</th><th>Tipo</th><th>Concepto</th><th>Orden</th><th>Activo</th><th></th>
              </tr>
            </thead>
            <tbody id="qcBody"></tbody>
          </table>
        </div>
      </div>

      <!-- Usuarios -->
      <div class="panel" id="panel-users">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-people me-2"></i>Usuarios & Accesos</div>
            <div class="muted small">Alta/edición + roles + empresas/localidades permitidas</div>
          </div>
          <div class="ms-auto d-flex gap-2 align-items-center">
            <input id="uSearch" class="form-control form-control-sm" placeholder="Buscar usuario..." style="min-width:240px">
            <button class="btn btn-outline-secondary btn-sm btn-pill" id="btnUSearch"><i class="bi bi-search me-1"></i>Buscar</button>
            <button class="btn btn-primary btn-sm btn-pill" id="addUser"><i class="bi bi-person-plus me-1"></i>Nuevo usuario</button>
          </div>
        </div>

        <div class="table-responsive mt-3">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>ID</th><th>Username</th><th>Nombre</th><th>Email</th><th>Tel</th><th>Activo</th><th></th>
              </tr>
            </thead>
            <tbody id="userBody"></tbody>
          </table>
        </div>
      </div>



      <!-- Ajuste Almacenes -->
      <div class="panel" id="panel-invadj">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-sliders me-2"></i>Ajuste de almacenes</div>
            <div class="muted small">Consulta la existencia oficial desde inventory_moves y registra entradas/salidas de ajuste.</div>
          </div>
          <div class="ms-auto d-flex gap-2 align-items-center">
            <button class="btn btn-warning btn-sm btn-pill" id="btnInvUnlock"><i class="bi bi-lock me-1"></i>Validar código</button>
            <button class="btn btn-outline-primary btn-sm btn-pill" id="btnInvReload"><i class="bi bi-arrow-repeat me-1"></i>Actualizar</button>
          </div>
        </div>

        <div class="alert alert-warning mt-3 mb-3 py-2">
          <strong>Control:</strong> para modificar inventario se requiere el código autorizado. Todo cambio se registra en <span class="mono">inventory_moves</span>.
          <br><strong>Ajuste manual:</strong> usa <span class="mono">ref_type = AJUSTE_ALMACEN</span> para agregar/quitar una cantidad directa.
          <br><strong>Corte físico:</strong> usa <span class="mono">ref_type = CORTE_FISICO_WH</span>; capturas la cantidad física objetivo y el sistema calcula automáticamente la diferencia por producto + almacén.
        </div>

        <div id="invAdjustArea" class="invAdjustLocked">
          <div class="row g-3 align-items-end">
            <div class="col-12 col-md-4">
              <label class="form-label">Empresa</label>
              <select id="invCompany" class="form-select"></select>
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Planta / localidad</label>
              <select id="invLocation" class="form-select"></select>
            </div>
            <div class="col-12 col-md-4">
              <label class="form-label">Almacén</label>
              <select id="invWarehouse" class="form-select"></select>
            </div>

            <div class="col-12 col-md-5">
              <label class="form-label">Producto</label>
              <select id="invProduct" class="form-select"></select>
            </div>
            <div class="col-12 col-md-2">
              <label class="form-label">Movimiento</label>
              <select id="invKind" class="form-select">
                <option value="ADD">Agregar</option>
                <option value="SUB">Quitar</option>
              </select>
            </div>
            <div class="col-12 col-md-2">
              <label class="form-label">Cantidad</label>
              <input id="invQty" class="form-control mono" type="number" min="0" step="0.001" value="0">
            </div>
            <div class="col-12 col-md-3">
              <label class="form-label">Nota</label>
              <input id="invNotes" class="form-control" placeholder="Motivo del ajuste">
            </div>
            <div class="col-12 d-flex justify-content-end">
              <button class="btn btn-primary btn-pill" id="btnInvSave"><i class="bi bi-save me-1"></i>Guardar ajuste manual</button>
            </div>
          </div>

          <div class="border rounded-4 p-3 mt-3 bg-light">
            <div class="d-flex flex-wrap gap-2 align-items-center mb-2">
              <div>
                <div class="fw-bold"><i class="bi bi-clipboard-check me-1"></i>Corte físico por almacén</div>
                <div class="muted small">Selecciona producto + almacén, captura la cantidad física real y el sistema agregará o quitará solo la diferencia necesaria.</div>
              </div>
              <div class="ms-auto">
                <span class="badge text-bg-info">ref_type: CORTE_FISICO_WH</span>
              </div>
            </div>
            <div class="row g-3 align-items-end">
              <div class="col-12 col-md-3">
                <label class="form-label">Cantidad física objetivo</label>
                <input id="invTargetQty" class="form-control mono" type="number" min="0" step="0.001" value="0">
              </div>
              <div class="col-12 col-md-6">
                <label class="form-label">Nota del corte</label>
                <input id="invCutNotes" class="form-control" placeholder="Ej. Corte físico 3 pm / conteo planta">
              </div>
              <div class="col-12 col-md-3 d-grid">
                <button class="btn btn-success btn-pill" id="btnInvCutSave"><i class="bi bi-check2-circle me-1"></i>Aplicar corte</button>
              </div>
            </div>
            <div class="small text-muted mt-2">
              Ejemplo: si CF3 tiene 500 y el conteo físico dice 638, se agrega +138. Si tiene 700, se quitan -62. No borra histórico.
            </div>
          </div>

          <hr>

          <div class="row g-3">
            <div class="col-12 col-xl-5">
              <div class="fw-semibold mb-2">Totales por producto</div>
              <div class="table-responsive" style="max-height:420px;overflow:auto">
                <table class="table table-sm align-middle">
                  <thead><tr><th>SKU</th><th>Producto</th><th class="text-end">Total</th></tr></thead>
                  <tbody id="invTotalsBody"></tbody>
                </table>
              </div>
            </div>
            <div class="col-12 col-xl-7">
              <div class="fw-semibold mb-2">Detalle por almacén</div>
              <div class="table-responsive" style="max-height:420px;overflow:auto">
                <table class="table table-sm align-middle">
                  <thead><tr><th>SKU</th><th>Almacén</th><th class="text-end">Existencia</th></tr></thead>
                  <tbody id="invDetailBody"></tbody>
                </table>
              </div>
            </div>
          </div>
        </div>
      </div>



      <!-- Auditoría de movimientos -->
      <div class="panel" id="panel-audit">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-clipboard-data me-2"></i>Auditoría de movimientos / Kardex operativo</div>
            <div class="muted small">Consulta movimientos reales de <span class="mono">inventory_moves</span> y corrige casos autorizados con trazabilidad.</div>
          </div>
          <div class="ms-auto d-flex gap-2 align-items-center">
            <button class="btn btn-outline-primary btn-sm btn-pill" id="btnAuditReload"><i class="bi bi-arrow-repeat me-1"></i>Consultar</button>
          </div>
        </div>

        <div class="alert alert-info mt-3 mb-3 py-2">
          Esta sección permite rastrear entradas, salidas, ajustes, producción, calidad, ventas, VentasHielo, transferencias y cortes físicos. Las correcciones requieren código de autorización y quedan registradas en bitácora.
        </div>

        <div class="row g-3 align-items-end">
          <div class="col-12 col-md-3"><label class="form-label">Empresa</label><select id="audCompany" class="form-select"></select></div>
          <div class="col-12 col-md-3"><label class="form-label">Planta</label><select id="audLocation" class="form-select"></select></div>
          <div class="col-6 col-md-2"><label class="form-label">Desde</label><input id="audDateFrom" class="form-control" type="date"></div>
          <div class="col-6 col-md-2"><label class="form-label">Hasta</label><input id="audDateTo" class="form-control" type="date"></div>
          <div class="col-12 col-md-2"><label class="form-label">Movimiento</label><select id="audMoveType" class="form-select"><option value="">Todos</option><option value="IN">IN</option><option value="OUT">OUT</option><option value="ADJ">ADJ</option></select></div>
          <div class="col-12 col-md-3"><label class="form-label">Producto</label><select id="audProduct" class="form-select"><option value="0">Todos</option></select></div>
          <div class="col-12 col-md-3"><label class="form-label">Almacén</label><select id="audWarehouse" class="form-select"><option value="0">Todos</option></select></div>
          <div class="col-12 col-md-2"><label class="form-label">ref_type</label><input id="audRefType" class="form-control mono" placeholder="Ej. PRODUCTION"></div>
          <div class="col-12 col-md-4"><label class="form-label">Búsqueda libre</label><input id="audSearch" class="form-control" placeholder="SKU, nota, ref_id, usuario, almacén..."></div>
          <div class="col-12 col-md-2"><label class="form-label">Filas por página</label><select id="audPerPage" class="form-select"><option value="25">25</option><option value="50" selected>50</option><option value="100">100</option><option value="200">200</option></select></div>
        </div>

        <div class="row g-3 mt-3">
          <div class="col-6 col-xl-2"><div class="card border-0 bg-light rounded-4"><div class="card-body"><div class="muted small">Movimientos</div><div class="fw-bold fs-4 mono" id="audMovesCount">0</div></div></div></div>
          <div class="col-6 col-xl-2"><div class="card border-0 bg-light rounded-4"><div class="card-body"><div class="muted small">Total IN</div><div class="fw-bold fs-4 mono qty-pos" id="audTotalIn">0.000</div></div></div></div>
          <div class="col-6 col-xl-2"><div class="card border-0 bg-light rounded-4"><div class="card-body"><div class="muted small">Total OUT</div><div class="fw-bold fs-4 mono qty-neg" id="audTotalOut">0.000</div></div></div></div>
          <div class="col-6 col-xl-2"><div class="card border-0 bg-light rounded-4"><div class="card-body"><div class="muted small">Total ADJ</div><div class="fw-bold fs-4 mono" id="audTotalAdj">0.000</div></div></div></div>
          <div class="col-12 col-xl-4"><div class="card border-0 bg-light rounded-4"><div class="card-body"><div class="muted small">Saldo neto</div><div class="fw-bold fs-4 mono" id="audNetQty">0.000</div></div></div></div>
        </div>

        <div class="row g-3 mt-3">
          <div class="col-12 col-xl-8">
            <div class="d-flex flex-wrap gap-2 justify-content-between align-items-center mb-2">
              <div>
                <div class="fw-semibold">Movimientos encontrados</div>
                <div class="small text-muted" id="audPageInfo">Página 1 de 1 · 0 registros</div>
              </div>
              <div class="btn-group btn-group-sm" role="group" aria-label="Paginación auditoría">
                <button type="button" class="btn btn-outline-secondary" id="audFirstPage"><i class="bi bi-chevron-double-left"></i></button>
                <button type="button" class="btn btn-outline-secondary" id="audPrevPage"><i class="bi bi-chevron-left"></i> Anterior</button>
                <button type="button" class="btn btn-outline-secondary" id="audNextPage">Siguiente <i class="bi bi-chevron-right"></i></button>
                <button type="button" class="btn btn-outline-secondary" id="audLastPage"><i class="bi bi-chevron-double-right"></i></button>
              </div>
            </div>
            <div class="table-responsive" style="max-height:520px;overflow:auto">
              <table class="table table-sm align-middle">
                <thead><tr><th>Fecha/hora</th><th>Operativa</th><th>Producto</th><th>Almacén</th><th>Mov.</th><th class="text-end">Cant.</th><th>ref_type</th><th>ref_id</th><th>Usuario</th><th>Notas</th><th class="text-end">Acción</th></tr></thead>
                <tbody id="audBody"></tbody>
              </table>
            </div>
          </div>
          <div class="col-12 col-xl-4">
            <div class="fw-semibold mb-2">Resumen por ref_type</div>
            <div class="table-responsive mb-3" style="max-height:245px;overflow:auto">
              <table class="table table-sm align-middle"><thead><tr><th>ref_type</th><th class="text-end">Movs</th><th class="text-end">Neto</th></tr></thead><tbody id="audRefBody"></tbody></table>
            </div>
            <div class="fw-semibold mb-2">Resumen por usuario</div>
            <div class="table-responsive" style="max-height:245px;overflow:auto">
              <table class="table table-sm align-middle"><thead><tr><th>Usuario</th><th class="text-end">Movs</th><th class="text-end">Neto</th></tr></thead><tbody id="audUserBody"></tbody></table>
            </div>
          </div>
        </div>
      </div>



      <!-- Validador cubicado -->
      <div class="panel" id="panel-cubaudit">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-boxes me-2"></i>Validación de producción cubicado</div>
            <div class="muted small">Consulta capturas de <span class="mono">cubing_entries</span> por fecha y turno, y valida su afectación contra <span class="mono">inventory_moves</span>.</div>
          </div>
          <div class="ms-auto">
            <button class="btn btn-primary btn-sm btn-pill" id="btnCubAuditOpen"><i class="bi bi-search me-1"></i>Abrir validador</button>
          </div>
        </div>
        <div class="alert alert-warning mt-3 mb-0 py-2">
          Esta vista es de revisión. No modifica inventario; solo ayuda a detectar errores de captura o diferencias entre la bitácora de cubicado y los movimientos reales de inventario.
        </div>
      </div>



      <!-- Análisis de datos -->
      <div class="panel" id="panel-data">
        <div class="d-flex flex-wrap gap-2 align-items-center">
          <div>
            <div class="fw-bold fs-5"><i class="bi bi-database-check me-2"></i>Análisis de datos</div>
            <div class="muted small">Explorador de tablas, relaciones sugeridas y ejecución segura de consultas SELECT. Solo administradores.</div>
          </div>
          <div class="ms-auto d-flex gap-2 align-items-center">
            <button class="btn btn-outline-primary btn-sm btn-pill" id="btnDataReload"><i class="bi bi-arrow-repeat me-1"></i>Recargar tablas</button>
            <button class="btn btn-success btn-sm btn-pill" id="btnDataRun"><i class="bi bi-play-fill me-1"></i>Ejecutar consulta</button>
          </div>
        </div>

        <div class="alert alert-warning mt-3 mb-3 py-2">
          Esta sección es de <strong>solo lectura</strong>. El servidor bloquea INSERT, UPDATE, DELETE, DROP, ALTER, CREATE, TRUNCATE y otras operaciones de escritura.
        </div>

        <div class="row g-3">
          <div class="col-12 col-xl-3">
            <div class="fw-semibold mb-2">Tablas de la base</div>
            <input id="dataTableSearch" class="form-control form-control-sm mb-2" placeholder="Buscar tabla...">
            <div class="checkgrid" style="max-height:520px" id="dataTablesBox"></div>
          </div>
          <div class="col-12 col-xl-5">
            <div class="d-flex flex-wrap gap-2 align-items-center mb-2">
              <div class="fw-semibold">Consulta generada / editable</div>
              <div class="ms-auto d-flex gap-2">
                <button class="btn btn-outline-secondary btn-sm" id="btnDataDescribe"><i class="bi bi-columns-gap me-1"></i>Ver columnas</button>
                <button class="btn btn-outline-primary btn-sm" id="btnDataGenerate"><i class="bi bi-magic me-1"></i>Generar SELECT</button>
              </div>
            </div>
            <textarea id="dataSql" class="form-control mono" rows="15" placeholder="Selecciona una o más tablas para generar una consulta SELECT..."></textarea>
            <div class="small text-muted mt-2" id="dataQueryInfo">Sin consulta ejecutada.</div>
          </div>
          <div class="col-12 col-xl-4">
            <div class="fw-semibold mb-2">Relaciones sugeridas</div>
            <div class="table-responsive" style="max-height:245px;overflow:auto">
              <table class="table table-sm align-middle">
                <thead><tr><th>Relación</th><th>Conf.</th></tr></thead>
                <tbody id="dataRelBody"></tbody>
              </table>
            </div>
            <hr>
            <div class="fw-semibold mb-2">Consultas relacionadas listas</div>
            <div class="list-group small" id="dataPresetBox" style="max-height:245px;overflow:auto"></div>
          </div>
        </div>

        <hr>
        <div class="d-flex flex-wrap gap-2 align-items-center mb-2">
          <div>
            <div class="fw-semibold">Resultado</div>
            <div class="small text-muted" id="dataResultInfo">0 registros</div>
          </div>
        </div>
        <div class="table-responsive" style="max-height:520px;overflow:auto">
          <table class="table table-sm table-striped align-middle">
            <thead id="dataResultHead"></thead>
            <tbody id="dataResultBody"></tbody>
          </table>
        </div>
      </div>

      <div class="text-muted small mt-3">
        CSRF activo · si algo falla, revisa consola y permisos MySQL.
      </div>
    </div>
  </div>
</div>

<!-- Modal editor (reutilizado para TODOS los módulos) -->
<div class="modal fade" id="editModal" tabindex="-1">
  <div class="modal-dialog modal-dialog-centered modal-lg">
    <form class="modal-content" id="editForm">
      <div class="modal-header">
        <div class="modal-title fw-bold" id="mTitle">Editar</div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body" id="mBody"></div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-primary" id="mSave">Guardar</button>
      </div>
    </form>
  </div>

</div>


<!-- Modal corrección controlada de movimiento -->
<div class="modal fade" id="moveEditModal" tabindex="-1">
  <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
    <form class="modal-content" id="moveEditForm">
      <div class="modal-header">
        <div>
          <div class="modal-title fw-bold"><i class="bi bi-pencil-square me-2"></i>Corregir movimiento de inventario</div>
          <div class="text-muted small" id="moveEditSub">Edición controlada sobre inventory_moves</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <input type="hidden" id="moveEditId">
        <div class="alert alert-warning py-2">
          Esta acción modifica el kardex oficial. Úsala solo para corregir errores de captura: fecha, artículo, almacén, cantidad, tipo de movimiento o comentario. El cambio queda registrado en <span class="mono">inventory_move_admin_edits</span>.
        </div>
        <div class="row g-3">
          <div class="col-12 col-md-3">
            <label class="form-label">Fecha operativa</label>
            <input id="moveEditWorkDate" type="date" class="form-control" required>
          </div>
          <div class="col-12 col-md-3">
            <label class="form-label">Fecha/hora visible</label>
            <input id="moveEditCreatedAt" type="datetime-local" class="form-control" required>
          </div>
          <div class="col-12 col-md-3">
            <label class="form-label">Movimiento</label>
            <select id="moveEditMoveType" class="form-select" required>
              <option value="IN">IN · Entrada</option>
              <option value="OUT">OUT · Salida</option>
              <option value="ADJ">ADJ · Ajuste positivo</option>
            </select>
          </div>
          <div class="col-12 col-md-3">
            <label class="form-label">Cantidad</label>
            <input id="moveEditQty" type="number" step="0.001" min="0.001" class="form-control mono" required>
          </div>

          <div class="col-12 col-lg-6">
            <label class="form-label">Artículo</label>
            <select id="moveEditProduct" class="form-select" required></select>
          </div>
          <div class="col-12 col-lg-6">
            <label class="form-label">Almacén</label>
            <select id="moveEditWarehouse" class="form-select"></select>
          </div>

          <div class="col-12">
            <label class="form-label">Comentario / notas</label>
            <textarea id="moveEditNotes" class="form-control" rows="3" maxlength="255"></textarea>
          </div>
          <div class="col-12 col-md-6">
            <label class="form-label">Motivo de corrección</label>
            <input id="moveEditReason" class="form-control" maxlength="255" placeholder="Ej. Fecha incorrecta por desfase horario" required>
          </div>
          <div class="col-12 col-md-6">
            <label class="form-label">Código de autorización</label>
            <input id="moveEditCode" type="password" class="form-control mono" autocomplete="off" required>
          </div>

          <div class="col-12">
            <div class="small text-muted" id="moveEditMeta"></div>
          </div>
        </div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancelar</button>
        <button class="btn btn-danger" id="moveEditSave"><i class="bi bi-save me-1"></i>Guardar corrección</button>
      </div>
    </form>
  </div>
</div>

<!-- Modal reglas de almacén por producto/localidad -->
<div class="modal fade" id="whRulesModal" tabindex="-1">
  <div class="modal-dialog modal-xl modal-dialog-centered modal-dialog-scrollable">
    <div class="modal-content">
      <div class="modal-header">
        <div>
          <div class="modal-title fw-bold" id="whRulesTitle">Almacenes por producto</div>
          <div class="text-muted small" id="whRulesSub">Configura reglas por localidad.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="alert alert-info py-2">
          Esta configuración define la regla operativa esperada por producto y localidad. No mueve inventario; los movimientos reales seguirán generándose en <span class="mono">inventory_moves</span> desde Producción, Almacén, Calidad, Ventas o Transferencias.
        </div>
        <div id="whRulesBody"></div>
      </div>
      <div class="modal-footer">
        <button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>


<div class="modal fade" id="cubAuditModal" tabindex="-1" aria-hidden="true">
  <div class="modal-dialog modal-fullscreen-xl-down modal-xl modal-dialog-scrollable">
    <div class="modal-content rounded-4">
      <div class="modal-header">
        <div>
          <h5 class="modal-title"><i class="bi bi-clipboard-check me-2"></i>Validador de producción cubicado</h5>
          <div class="small text-muted">Filtra por fecha y turno. Se compara <span class="mono">cubing_entries</span> contra <span class="mono">inventory_moves.ref_type = CUBING</span>.</div>
        </div>
        <button type="button" class="btn-close" data-bs-dismiss="modal"></button>
      </div>
      <div class="modal-body">
        <div class="row g-2 align-items-end">
          <div class="col-12 col-md-3">
            <label class="form-label">Empresa</label>
            <select class="form-select" id="cubAudCompany"></select>
          </div>
          <div class="col-12 col-md-3">
            <label class="form-label">Planta</label>
            <select class="form-select" id="cubAudLocation"></select>
          </div>
          <div class="col-6 col-md-2">
            <label class="form-label">Fecha</label>
            <input type="date" class="form-control" id="cubAudDate">
          </div>
          <div class="col-6 col-md-2">
            <label class="form-label">Turno</label>
            <select class="form-select" id="cubAudShift">
              <option value="0">Todos</option>
              <option value="1">Turno 1</option>
              <option value="2">Turno 2</option>
              <option value="3">Turno 3</option>
            </select>
          </div>
          <div class="col-12 col-md-2">
            <label class="form-label">Producto</label>
            <select class="form-select" id="cubAudProduct"><option value="0">Todos</option></select>
          </div>
          <div class="col-12 col-md-8">
            <label class="form-label">Búsqueda</label>
            <input type="text" class="form-control" id="cubAudSearch" placeholder="SKU, producto, origen, ID de captura...">
          </div>
          <div class="col-12 col-md-4 d-grid">
            <button class="btn btn-primary" id="btnCubAuditRun"><i class="bi bi-arrow-repeat me-1"></i>Consultar</button>
          </div>
        </div>

        <div class="row g-3 mt-3">
          <div class="col-6 col-md-2"><div class="card border-0 bg-light rounded-4"><div class="card-body py-3"><div class="muted small">Capturas</div><div class="fw-bold fs-4 mono" id="cubAudCount">0</div></div></div></div>
          <div class="col-6 col-md-2"><div class="card border-0 bg-light rounded-4"><div class="card-body py-3"><div class="muted small">OK</div><div class="fw-bold fs-4 mono text-success" id="cubAudOk">0</div></div></div></div>
          <div class="col-6 col-md-2"><div class="card border-0 bg-light rounded-4"><div class="card-body py-3"><div class="muted small">Revisar</div><div class="fw-bold fs-4 mono text-danger" id="cubAudReview">0</div></div></div></div>
          <div class="col-6 col-md-2"><div class="card border-0 bg-light rounded-4"><div class="card-body py-3"><div class="muted small">Piezas</div><div class="fw-bold fs-4 mono" id="cubAudOutput">0</div></div></div></div>
          <div class="col-6 col-md-2"><div class="card border-0 bg-light rounded-4"><div class="card-body py-3"><div class="muted small">Barras usadas</div><div class="fw-bold fs-4 mono" id="cubAudBars">0.000</div></div></div></div>
          <div class="col-6 col-md-2"><div class="card border-0 bg-light rounded-4"><div class="card-body py-3"><div class="muted small">Sobrante</div><div class="fw-bold fs-4 mono" id="cubAudLeft">0.000</div></div></div></div>
        </div>

        <div class="table-responsive mt-3" style="max-height:560px;overflow:auto">
          <table class="table table-sm align-middle">
            <thead>
              <tr>
                <th>Estado</th>
                <th>ID</th>
                <th>Fecha / turno</th>
                <th>Producto producido</th>
                <th>Origen</th>
                <th class="text-end">Piezas</th>
                <th class="text-end">Barras usadas</th>
                <th class="text-end">Tomadas</th>
                <th class="text-end">Sobrante</th>
                <th class="text-end">Movs inv.</th>
                <th>Validación</th>
              </tr>
            </thead>
            <tbody id="cubAudBody"></tbody>
          </table>
        </div>
      </div>
      <div class="modal-footer">
        <div class="me-auto small text-muted">Para corregir inventario, usa “Auditoría movimientos” y busca <span class="mono">ref_type CUBING</span> con el ID de captura.</div>
        <button class="btn btn-outline-secondary" data-bs-dismiss="modal">Cerrar</button>
      </div>
    </div>
  </div>
</div>

<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const CSRF = <?=json_encode($csrf)?>;
const ADMIN_SCOPE_COMPANY_ID = <?= json_encode($adminCompanyId) ?>;
const ADMIN_SCOPE_COMPANY_NAME = <?= json_encode($adminCompanyName) ?>;
const ADMIN_SCOPE_LOCATION_ID = <?= json_encode($adminLocationId) ?>;
const ADMIN_SCOPE_LOCATION_NAME = <?= json_encode($adminLocationName) ?>;

async function apiReadJsonResponse(r){
  const txt = await r.text();
  try{
    const data = JSON.parse(txt || '{}');
    if(data && typeof data === 'object' && !('http_status' in data)) data.http_status = r.status;
    return data;
  }catch(e){
    console.error('Respuesta no JSON', txt);
    return {
      ok:false,
      http_status:r.status,
      error: r.status === 403
        ? 'Acceso denegado o sesión expirada. Vuelve a iniciar sesión.'
        : (r.status === 404 ? 'Endpoint no encontrado.' : 'Respuesta no válida del servidor. Revisa el error_log si persiste.'),
      raw_response: txt
    };
  }
}
async function apiGet(action, params={}){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);
  for(const [k,v] of Object.entries(params)) u.searchParams.set(k, v);
  const r = await fetch(u.toString(), {credentials:'same-origin'});
  return await apiReadJsonResponse(r);
}
async function apiPost(action, payload){
  const u = new URL(location.href);
  u.search = '';
  u.searchParams.set('action', action);
  const r = await fetch(u.toString(), {
    method:'POST',
    credentials:'same-origin',
    headers:{'Content-Type':'application/json','X-CSRF':CSRF},
    body: JSON.stringify(payload||{})
  });
  return await apiReadJsonResponse(r);
}

function h(s){return String(s??'').replace(/[&<>"]/g,m=>({ '&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;' }[m]));}
const $ = (id)=>document.getElementById(id);


function notify(type='info', title='Aviso', message='', timeout=5200){
  const stack = $('appToastStack');
  if(!stack){ console[type==='danger'?'error':'log'](title, message); return; }
  const safeType = ['success','warning','danger','info'].includes(type) ? type : 'info';
  const icon = safeType === 'success' ? 'bi-check-circle-fill' : (safeType === 'warning' ? 'bi-exclamation-triangle-fill' : (safeType === 'danger' ? 'bi-x-octagon-fill' : 'bi-info-circle-fill'));
  const el = document.createElement('div');
  el.className = `appToast ${safeType}`;
  el.innerHTML = `
    <div class="appToastIcon"><i class="bi ${icon}"></i></div>
    <div><div class="appToastTitle">${h(title)}</div><div class="appToastMsg">${h(message || '')}</div></div>
    <button class="appToastClose" type="button" aria-label="Cerrar"><i class="bi bi-x-lg"></i></button>`;
  el.querySelector('.appToastClose')?.addEventListener('click', ()=>el.remove());
  stack.appendChild(el);
  if(timeout > 0) setTimeout(()=>{ if(el.isConnected) el.remove(); }, timeout);
}
function notifyError(message, title='No se pudo completar la acción'){
  notify('danger', title, message || 'Revisa la información capturada e intenta nuevamente.', 7800);
}
function notifySuccess(message, title='Listo'){
  notify('success', title, message || 'Operación realizada correctamente.', 4200);
}
window.alert = (message)=>notify('warning', 'Atención', String(message || ''), 7000);

const tableUiState = {};
const tableUiConfig = {
  locBody:{label:'localidades', placeholder:'Buscar por localidad, código, empresa o zona horaria...', perPage:12},
  whBody:{label:'almacenes', placeholder:'Buscar por localidad, código o nombre de almacén...', perPage:12},
  prodBody:{label:'productos', placeholder:'Buscar por artículo, descripción, DLAX o UOM...', perPage:12},
  packBody:{label:'embalajes', placeholder:'Buscar por artículo, descripción, tipo o capacidad...', perPage:12},
  plcTagBody:{label:'tags PLC', placeholder:'Buscar por línea, tag, PLC, artículo o almacén...', perPage:12},
  routeBody:{label:'rutas', placeholder:'Buscar por ruta, código, operador, unidad o días...', perPage:12},
  chanBody:{label:'canales', placeholder:'Buscar por canal, tipo o empresa...', perPage:12},
  qcBody:{label:'conceptos', placeholder:'Buscar por tipo, concepto o empresa...', perPage:12},
  userBody:{label:'usuarios', placeholder:'Buscar por usuario, nombre, correo o teléfono...', perPage:12},
  invTotalsBody:{label:'totales', placeholder:'Buscar producto...', perPage:10},
  invDetailBody:{label:'detalle de almacén', placeholder:'Buscar SKU o almacén...', perPage:10}
};
function tableColspan(tb){
  const table = tb?.closest('table');
  return Math.max(1, table?.querySelectorAll('thead th')?.length || 1);
}
function ensureTableTools(){
  for(const [bodyId,cfg] of Object.entries(tableUiConfig)){
    const tb = $(bodyId); if(!tb) continue;
    const wrap = tb.closest('.table-responsive'); if(!wrap) continue;
    wrap.classList.add('adminPagedTable');
    if(!tableUiState[bodyId]) tableUiState[bodyId] = {page:1, q:''};
    if(!document.querySelector(`[data-table-tools-for="${bodyId}"]`)){
      const tools = document.createElement('div');
      tools.className = 'sectionTools';
      tools.dataset.tableToolsFor = bodyId;
      tools.innerHTML = `
        <div class="searchWrap"><i class="bi bi-search"></i><input class="form-control form-control-sm tableSearch" data-table-search="${bodyId}" placeholder="${h(cfg.placeholder)}"></div>
        <span class="countPill" data-table-count="${bodyId}">0 ${h(cfg.label)}</span>
        <select class="form-select form-select-sm" style="width:auto" data-table-page-size="${bodyId}">
          <option value="8">8 por página</option><option value="12" selected>12 por página</option><option value="20">20 por página</option><option value="50">50 por página</option>
        </select>`;
      wrap.parentNode.insertBefore(tools, wrap);
      tools.querySelector('[data-table-search]')?.addEventListener('input', e=>{ tableUiState[bodyId].q = e.target.value.toLowerCase().trim(); tableUiState[bodyId].page = 1; applyTableView(bodyId); });
      tools.querySelector('[data-table-page-size]')?.addEventListener('change', e=>{ tableUiConfig[bodyId].perPage = parseInt(e.target.value||'12',10); tableUiState[bodyId].page = 1; applyTableView(bodyId); });
    }
    if(!document.querySelector(`[data-table-pager-for="${bodyId}"]`)){
      const pager = document.createElement('div');
      pager.className = 'sectionPager';
      pager.dataset.tablePagerFor = bodyId;
      pager.innerHTML = `
        <div class="pageInfo" data-page-info="${bodyId}">Página 1 de 1</div>
        <div class="btn-group btn-group-sm">
          <button type="button" class="btn btn-outline-secondary" data-page-first="${bodyId}"><i class="bi bi-chevron-double-left"></i></button>
          <button type="button" class="btn btn-outline-secondary" data-page-prev="${bodyId}"><i class="bi bi-chevron-left"></i> Anterior</button>
          <button type="button" class="btn btn-outline-secondary" data-page-next="${bodyId}">Siguiente <i class="bi bi-chevron-right"></i></button>
          <button type="button" class="btn btn-outline-secondary" data-page-last="${bodyId}"><i class="bi bi-chevron-double-right"></i></button>
        </div>`;
      wrap.parentNode.insertBefore(pager, wrap.nextSibling);
      pager.querySelector('[data-page-first]')?.addEventListener('click',()=>{tableUiState[bodyId].page=1;applyTableView(bodyId);});
      pager.querySelector('[data-page-prev]')?.addEventListener('click',()=>{tableUiState[bodyId].page=Math.max(1,(tableUiState[bodyId].page||1)-1);applyTableView(bodyId);});
      pager.querySelector('[data-page-next]')?.addEventListener('click',()=>{tableUiState[bodyId].page=(tableUiState[bodyId].page||1)+1;applyTableView(bodyId);});
      pager.querySelector('[data-page-last]')?.addEventListener('click',()=>{tableUiState[bodyId].page=999999;applyTableView(bodyId);});
    }
  }
}
function applyTableView(bodyId){
  const tb = $(bodyId); if(!tb) return;
  const cfg = tableUiConfig[bodyId] || {label:'registros', perPage:12};
  const state = tableUiState[bodyId] || (tableUiState[bodyId] = {page:1,q:''});
  const allRows = Array.from(tb.querySelectorAll('tr')).filter(tr=>!tr.classList.contains('emptyFilterRow'));
  tb.querySelectorAll('.emptyFilterRow').forEach(x=>x.remove());
  const q = String(state.q || '').toLowerCase().trim();
  const dataRows = allRows.filter(tr=>tr.textContent.trim() !== '');
  const filtered = q ? dataRows.filter(tr=>tr.textContent.toLowerCase().includes(q)) : dataRows;
  const per = Math.max(1, parseInt(cfg.perPage||12,10));
  const pages = Math.max(1, Math.ceil(filtered.length / per));
  state.page = Math.min(Math.max(1, parseInt(state.page||1,10)), pages);
  const start = (state.page - 1) * per;
  const shownSet = new Set(filtered.slice(start, start + per));
  dataRows.forEach(tr=>{ tr.style.display = shownSet.has(tr) ? '' : 'none'; });
  if(filtered.length === 0 && dataRows.length > 0){
    const tr = document.createElement('tr');
    tr.className = 'emptyFilterRow';
    tr.innerHTML = `<td colspan="${tableColspan(tb)}"><i class="bi bi-search me-1"></i>No hay coincidencias para la búsqueda.</td>`;
    tb.appendChild(tr);
  }
  const count = document.querySelector(`[data-table-count="${bodyId}"]`);
  if(count) count.textContent = `${filtered.length} de ${dataRows.length} ${cfg.label}`;
  const info = document.querySelector(`[data-page-info="${bodyId}"]`);
  if(info) info.textContent = `Página ${state.page} de ${pages} · ${filtered.length} registro(s)`;
  const pager = document.querySelector(`[data-table-pager-for="${bodyId}"]`);
  if(pager){
    pager.querySelector('[data-page-first]').disabled = state.page <= 1;
    pager.querySelector('[data-page-prev]').disabled = state.page <= 1;
    pager.querySelector('[data-page-next]').disabled = state.page >= pages;
    pager.querySelector('[data-page-last]').disabled = state.page >= pages;
  }
}
function refreshTableViews(){
  ensureTableTools();
  for(const bodyId of Object.keys(tableUiConfig)) applyTableView(bodyId);
}

let companies = [];
let packagingRows = [];
let productionLines = [];
let plcTags = [];
let plcRefs = {lines:[], products:[], packaging:[], warehouses:[]};
let roles = [];
let auditPage = 1;
let auditTotalPages = 1;
let invUnlocked = false;
let invCode = '';
let modal, whRulesModal, moveEditModal, cubAuditModal, mode='', editing=null;

function companyFilterId(){
  return parseInt(ADMIN_SCOPE_COMPANY_ID || 0, 10);
}
function boolBadge(v){
  return v ? '<span class="badge text-bg-success">Sí</span>' : '<span class="badge text-bg-secondary">No</span>';
}

/* ===== Loaders ===== */
async function loadCompanies(){
  const j = await apiGet('companies');
  if(!j.ok) throw new Error(j.error||'No se pudo consultar la empresa activa.');
  companies = (j.rows || []).filter(c => parseInt(c.active ?? 1, 10) === 1);
  const sel = $('companyFilter');
  const current = companies.find(c => parseInt(c.id, 10) === companyFilterId());
  sel.innerHTML = `<option value="${companyFilterId()}" selected>${h(current?.name || ADMIN_SCOPE_COMPANY_NAME)}</option>`;
  sel.value = String(companyFilterId());
  sel.disabled = true;
}
async function loadRoles(){
  const j = await apiGet('roles_list');
  roles = j.ok ? (j.rows||[]) : [];
}

/* ===== Tables ===== */
async function loadLocations(){
  const j = await apiGet('locations_list', {company_id: companyFilterId()});
  if(!j.ok) return;
  const tb = $('locBody');
  tb.innerHTML = '';
  for(const r of (j.rows||[])){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="text-muted">${r.id}</td>
        <td>${h(r.company_name)}</td>
        <td>${h(r.name)}</td>
        <td class="mono">${h(r.code)}</td>
        <td class="mono">${h(r.timezone)}</td>
        <td>${boolBadge(parseInt(r.active,10)===1)}</td>
        <td class="text-end">
          <button class="btn btn-outline-primary btn-sm" data-edit="loc" data-id="${r.id}">
            <i class="bi bi-pencil"></i>
          </button>
        </td>
      </tr>
    `);
  }
  applyTableView('locBody');
}

async function loadWarehouses(){
  const j = await apiGet('warehouses_list', {company_id: companyFilterId()});
  if(!j.ok) return;
  const tb = $('whBody');
  if(!tb) return;
  tb.innerHTML = '';
  for(const r of (j.rows||[])){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="text-muted">${r.id}</td>
        <td>${h(r.company_name)}</td>
        <td>${h(r.location_name)} <span class="text-muted small mono">${h(r.location_code||'')}</span></td>
        <td class="mono fw-semibold">${h(r.code)}</td>
        <td>${h(r.name)}</td>
        <td>${boolBadge(parseInt(r.active,10)===1)}</td>
        <td class="text-end">
          <button class="btn btn-outline-primary btn-sm" data-edit="wh" data-id="${r.id}">
            <i class="bi bi-pencil"></i>
          </button>
        </td>
      </tr>
    `);
  }
  applyTableView('whBody');
}


async function loadProducts(){
  const j = await apiGet('products_list', {company_id: companyFilterId()});
  if(!j.ok) return;
  const tb = $('prodBody');
  tb.innerHTML = '';
  for(const r of (j.rows||[])){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="text-muted">${r.id}</td>
        <td>${h(r.company_name)}</td>
        <td class="mono">${h(r.sku)}</td>
        <td>${h(r.name)}</td>
        <td class="mono">${h(r.erp_id ?? '')}</td>
        <td class="mono">${h(r.uom)}</td>
        <td class="small">
          ${r.pieces_per_package && r.packages_per_pallet
            ? `<span class="badge text-bg-light border">${h(r.pieces_per_package)} pzas/paq · ${h(r.packages_per_pallet)} paq/tar · ${h(r.pieces_per_pallet)} pzas/tar</span>`
            : '<span class="text-muted">Sin configurar</span>'}
        </td>
        <td>${boolBadge(parseInt(r.active,10)===1)}</td>
        <td>
          <button class="btn btn-outline-info btn-sm btn-pill" data-whrules="1" data-id="${r.id}" data-company-id="${r.company_id}" data-sku="${h(r.sku)}" data-name="${h(r.name)}">
            <i class="bi bi-houses me-1"></i>Ver / configurar
          </button>
        </td>
        <td class="text-end">
          <button class="btn btn-outline-primary btn-sm" data-edit="prod" data-id="${r.id}">
            <i class="bi bi-pencil"></i>
          </button>
        </td>
      </tr>
    `);
  }
  applyTableView('prodBody');
}

async function loadPackaging(){
  const tb = $('packBody');
  if(!tb) return;

  try {
    const j = await apiGet('packaging_options_list', {company_id: companyFilterId()});
    if(!j.ok) {
      packagingRows = [];
      tb.innerHTML = `
        <tr>
          <td colspan="8" class="py-4">
            <div class="alert alert-warning mb-0">
              <i class="bi bi-exclamation-triangle me-2"></i>${h(j.error || 'No fue posible consultar empaques.')}
            </div>
          </td>
        </tr>`;
      return;
    }
    packagingRows = j.rows || [];
  } catch (err) {
    packagingRows = [];
    tb.innerHTML = `
      <tr>
        <td colspan="8" class="py-4">
          <div class="alert alert-danger mb-0">
            <i class="bi bi-exclamation-circle me-2"></i>
            Error al consultar empaques. Verifica que el script SQL de instalación haya sido ejecutado.
          </div>
        </td>
      </tr>`;
    console.error('loadPackaging:', err);
    return;
  }

  tb.innerHTML = '';

  const products = new Map();
  for(const r of packagingRows){
    const pid = parseInt(r.product_id || 0, 10);
    if(!products.has(pid)) products.set(pid, []);
    products.get(pid).push(r);
  }

  for(const [productId, rows] of products.entries()){
    const first = rows[0];
    const options = rows.filter(x => parseInt(x.packaging_id || 0, 10) > 0);
    if(!options.length){
      tb.insertAdjacentHTML('beforeend', `
        <tr>
          <td class="mono fw-semibold">${h(first.sku)}<div class="small text-muted">${h(first.erp_id || '')}</div></td>
          <td>${h(first.product_name)}</td>
          <td colspan="5"><span class="text-muted">Sin embalajes configurados.</span></td>
          <td class="text-end">
            <button class="btn btn-outline-success btn-sm btn-pill" data-pack-additional="1" data-product-id="${productId}">
              <i class="bi bi-plus-lg me-1"></i>Configurar
            </button>
          </td>
        </tr>`);
      continue;
    }

    options.forEach((r, idx) => {
      const principal = parseInt(r.is_default || 0, 10) === 1;
      const activo = parseInt(r.packaging_active || 0, 10) === 1;
      tb.insertAdjacentHTML('beforeend', `
        <tr class="${idx > 0 ? 'table-light' : ''}">
          <td class="mono fw-semibold">
            ${idx === 0 ? h(r.sku) : '<span class="text-muted">↳ adicional</span>'}
            ${idx === 0 ? `<div class="small text-muted">${h(r.erp_id || '')}</div>` : ''}
          </td>
          <td>${idx === 0 ? h(r.product_name) : ''}</td>
          <td><div class="fw-semibold">${h(r.packaging_name || r.packaging_type || '')}</div><div class="small text-muted mono">${h(r.packaging_type || '')}</div></td>
          <td class="text-end mono">${h(r.pieces_per_package || '')}</td>
          <td class="text-end mono">${h(r.packages_per_pallet || '')}</td>
          <td class="text-end mono fw-bold">${h(r.pieces_per_pallet || '')}</td>
          <td>
            ${principal ? '<span class="badge text-bg-primary">Principal</span>' : '<span class="badge text-bg-light border">Adicional</span>'}
            ${activo ? '' : '<span class="badge text-bg-secondary ms-1">Inactivo</span>'}
          </td>
          <td class="text-end text-nowrap">
            <button class="btn btn-outline-primary btn-sm" data-pack-edit="1" data-packaging-id="${r.packaging_id}" title="Editar"><i class="bi bi-pencil"></i></button>
            ${idx === 0 ? `<button class="btn btn-outline-success btn-sm btn-pill ms-1" data-pack-additional="1" data-product-id="${productId}"><i class="bi bi-plus-lg me-1"></i>Adicional</button>` : ''}
          </td>
        </tr>`);
    });
  }
  if(!packagingRows.length){
    tb.innerHTML = '<tr><td colspan="8" class="text-center text-muted py-4">Sin artículos para la empresa seleccionada.</td></tr>';
  }
  applyTableView('packBody');
}

async function loadRoutes(){
  const companyId = companyFilterId();
  const tb = $('routeBody');
  if(!tb || companyId <= 0) return;
  const j = await apiGet('routes_list', {company_id: companyId});
  if(!j.ok){ tb.innerHTML = `<tr><td colspan="8" class="text-danger">${h(j.error||'No se pudieron cargar rutas.')}</td></tr>`; return; }
  tb.innerHTML = '';
  for(const r of (j.rows||[])){
    const operator = [r.driver_name || '', r.vehicle_unit || ''].filter(Boolean).join(' · ');
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td>${h(r.location_name)} <span class="text-muted mono small">${h(r.location_code||'')}</span></td>
        <td class="text-end mono fw-semibold">${h(r.route_no)}</td>
        <td class="mono">${h(r.code)}</td>
        <td><div class="fw-semibold">${h(r.name)}</div><div class="small text-muted">${h(r.description||'')}</div></td>
        <td>${h(operator || 'Sin asignar')}</td>
        <td class="small">${h(r.delivery_days || '')}</td>
        <td>${boolBadge(parseInt(r.active,10)===1)}</td>
        <td class="text-end">
          <button class="btn btn-outline-primary btn-sm" data-edit="route" data-id="${r.id}">
            <i class="bi bi-pencil"></i>
          </button>
        </td>
      </tr>
    `);
  }
  if(!(j.rows||[]).length){
    tb.innerHTML = '<tr><td colspan="8" class="text-center text-muted py-4">No existen rutas configuradas para la empresa seleccionada.</td></tr>';
  }
  applyTableView('routeBody');
}

async function loadChannels(){
  const j = await apiGet('channels_list', {company_id: companyFilterId()});
  if(!j.ok) return;
  const tb = $('chanBody');
  tb.innerHTML = '';
  for(const r of (j.rows||[])){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="text-muted">${r.id}</td>
        <td>${h(r.company_name)}</td>
        <td>${h(r.name)}</td>
        <td class="mono">${h(r.channel_type)}</td>
        <td class="mono">${h(r.sort_order)}</td>
        <td>${boolBadge(parseInt(r.active,10)===1)}</td>
        <td class="text-end">
          <button class="btn btn-outline-primary btn-sm" data-edit="chan" data-id="${r.id}">
            <i class="bi bi-pencil"></i>
          </button>
        </td>
      </tr>
    `);
  }
  applyTableView('chanBody');
}

async function loadQualityConcepts(){
  const type = $('qcType').value || 'MERMA';
  const j = await apiGet('quality_concepts_list', {company_id: companyFilterId(), concept_type: type});
  if(!j.ok) return;
  const tb = $('qcBody');
  tb.innerHTML = '';
  for(const r of (j.rows||[])){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="text-muted">${r.id}</td>
        <td>${h(r.company_name)}</td>
        <td class="mono">${h(r.concept_type)}</td>
        <td>${h(r.name)}</td>
        <td class="mono">${h(r.sort_order)}</td>
        <td>${boolBadge(parseInt(r.active,10)===1)}</td>
        <td class="text-end">
          <button class="btn btn-outline-primary btn-sm" data-edit="qc" data-id="${r.id}">
            <i class="bi bi-pencil"></i>
          </button>
        </td>
      </tr>
    `);
  }
  applyTableView('qcBody');
}

async function loadUsers(q=''){
  const j = await apiGet('users_list', q ? {q} : {});
  if(!j.ok) return;
  const tb = $('userBody');
  tb.innerHTML = '';
  for(const r of (j.rows||[])){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td class="text-muted">${r.id}</td>
        <td class="mono">${h(r.username)}</td>
        <td>${h(r.full_name)}</td>
        <td>${h(r.email ?? '')}</td>
        <td class="mono">${h(r.phone ?? '')}</td>
        <td>${boolBadge(parseInt(r.active,10)===1)}</td>
        <td class="text-end">
          <button class="btn btn-outline-primary btn-sm" data-edit="user" data-id="${r.id}">
            <i class="bi bi-pencil"></i>
          </button>
        </td>
      </tr>
    `);
  }
  applyTableView('userBody');
}


async function loadPlcRefs(){
  const j = await apiGet('plc_config_refs');
  if(!j.ok){ notifyError(j.error || 'No se pudieron cargar referencias PLC'); return; }
  plcRefs = {
    lines: j.lines || [],
    products: j.products || [],
    packaging: j.packaging || [],
    warehouses: j.warehouses || []
  };
}

async function loadProductionLines(){
  const j = await apiGet('production_lines_list');
  if(!j.ok){ notifyError(j.error || 'No se pudieron cargar líneas de producción'); return; }
  productionLines = j.rows || [];
  const box = $('prodLineCards');
  if(!box) return;
  if(!productionLines.length){
    box.innerHTML = '<div class="text-muted small p-2">Sin líneas configuradas. Agrega Línea 1, Línea 2 y Línea 3 para Botella.</div>';
    return;
  }
  box.innerHTML = productionLines.map(r=>`
    <div class="card border-0 shadow-sm rounded-4">
      <div class="card-body py-3">
        <div class="d-flex justify-content-between gap-2 align-items-start">
          <div>
            <div class="fw-bold">${h(r.line_name)}</div>
            <div class="small muted mono">${h(r.line_code || ('L'+r.line_no))} · Línea ${h(r.line_no)}</div>
          </div>
          <span class="badge ${String(r.active)==='1'?'text-bg-success':'text-bg-secondary'}">${String(r.active)==='1'?'Activa':'Inactiva'}</span>
        </div>
        <button class="btn btn-outline-primary btn-sm btn-pill mt-2" data-edit="pline" data-id="${h(r.id)}"><i class="bi bi-pencil me-1"></i>Editar</button>
      </div>
    </div>
  `).join('');
}

async function loadPlcTags(){
  const j = await apiGet('plc_tags_list');
  if(!j.ok){ notifyError(j.error || 'No se pudieron cargar TAGs PLC'); return; }
  plcTags = j.rows || [];
  const body = $('plcTagBody');
  if(!body) return;
  body.innerHTML = '';
  for(const r of plcTags){
    const enabled = String(r.enabled ?? 1) === '1';
    const active = String(r.active ?? 1) === '1';
    const signal = `${Number(r.pallets_per_signal||0).toLocaleString('es-MX')} tar · ${Number(r.packages_per_signal||0).toLocaleString('es-MX')} paq · ${Number(r.pieces_per_signal||0).toLocaleString('es-MX')} pz`;
    body.insertAdjacentHTML('beforeend', `
      <tr>
        <td><b>${h(r.configured_line_name || r.line_name || '')}</b><br><span class="small muted mono">L${h(r.production_line_no || '')}</span></td>
        <td><b class="mono">${h(r.tag_code)}</b><br><span class="small muted">${h(r.tag_name || '')}</span></td>
        <td>${h(r.plc_name || '—')}<br><span class="small muted mono">${h(r.ip_address || '')}</span></td>
        <td>${r.sku ? `<span class="mono">${h(r.sku)}</span><br><span class="small">${h(r.product_name||'')}</span>` : '<span class="muted">Sin artículo</span>'}</td>
        <td>${r.warehouse_code ? `<span class="mono">${h(r.warehouse_code)}</span><br><span class="small muted">${h(r.warehouse_name||'')}</span>` : '<span class="muted">Sin almacén</span>'}</td>
        <td class="small mono">${h(signal)}</td>
        <td><span class="badge ${active && enabled ? 'text-bg-success' : 'text-bg-secondary'}">${active && enabled ? 'Habilitado' : 'Inactivo'}</span><br><span class="small muted mono">${h(r.status || 'IDLE')}</span></td>
        <td class="text-end"><button class="btn btn-outline-primary btn-sm" data-edit="ptag" data-id="${h(r.id)}"><i class="bi bi-pencil"></i></button></td>
      </tr>
    `);
  }
  applyTableView('plcTagBody');
}

function productOptionsHtml(selected=0){
  const selId = parseInt(selected || 0, 10);
  return '<option value="0">Selecciona artículo</option>' + (plcRefs.products||[]).map(p=>`<option value="${p.id}" ${parseInt(p.id,10)===selId?'selected':''}>${h(p.sku)} · ${h(p.name)}</option>`).join('');
}
function warehouseOptionsHtml(selected=0){
  const selId = parseInt(selected || 0, 10);
  return '<option value="0">Selecciona almacén</option>' + (plcRefs.warehouses||[]).map(w=>`<option value="${w.id}" ${parseInt(w.id,10)===selId?'selected':''}>${h(w.code)} · ${h(w.name)}</option>`).join('');
}
function lineOptionsHtml(selected=0){
  const selId = parseInt(selected || 0, 10);
  return (plcRefs.lines||[]).map(l=>`<option value="${l.id}" ${parseInt(l.id,10)===selId?'selected':''}>L${h(l.line_no)} · ${h(l.line_name)}</option>`).join('');
}
function packagingOptionsHtml(productId=0, selected=0){
  const pid = parseInt(productId || 0, 10);
  const selId = parseInt(selected || 0, 10);
  const opts = (plcRefs.packaging||[]).filter(x=>parseInt(x.product_id||0,10)===pid);
  return '<option value="0">Embalaje principal del producto</option>' + opts.map(o=>{
    const label = `${o.packaging_name || 'Embalaje'} · ${o.pieces_per_package || 0} pz/paq · ${o.packages_per_pallet || 0} paq/tar${String(o.is_default)==='1'?' · principal':''}`;
    return `<option value="${o.id}" ${parseInt(o.id,10)===selId?'selected':''}>${h(label)}</option>`;
  }).join('');
}

function openProductionLineModal(row=null){
  mode = 'pline';
  editing = row || {};
  $('mTitle').textContent = row ? 'Editar línea de producción' : 'Nueva línea de producción';
  $('editForm').dataset.id = String(editing.id || 0);
  $('mBody').innerHTML = `
    <input type="hidden" name="company_id" value="${companyFilterId()}">
    <input type="hidden" name="location_id" value="${ADMIN_SCOPE_LOCATION_ID}">
    <div class="row g-3">
      <div class="col-12 col-md-4">
        <label class="form-label">Número de línea</label>
        <input class="form-control mono" type="number" min="1" max="255" step="1" name="line_no" required value="${h(editing.line_no || '')}" placeholder="1">
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Código</label>
        <input class="form-control mono" name="line_code" value="${h(editing.line_code || '')}" placeholder="L1">
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Orden visual</label>
        <input class="form-control mono" type="number" name="sort_order" value="${h(editing.sort_order ?? editing.line_no ?? 0)}">
      </div>
      <div class="col-12">
        <label class="form-label">Nombre de línea</label>
        <input class="form-control" name="line_name" required value="${h(editing.line_name || '')}" placeholder="Línea 1">
      </div>
      <div class="col-12">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="active" ${String(editing.active ?? 1)==='1'?'checked':''}>
          <label class="form-check-label">Línea activa</label>
        </div>
      </div>
      <div class="col-12"><div class="alert alert-info py-2 mb-0">Esta línea será visible en Producción Botella para iniciar turno manual o automático.</div></div>
    </div>`;
  modal.show();
}

async function openPlcTagModal(row=null){
  mode = 'ptag';
  editing = row || {};
  if((plcRefs.lines||[]).length === 0) await loadPlcRefs();
  $('mTitle').textContent = row ? 'Editar TAG PLC de producción' : 'Nuevo TAG PLC de producción';
  $('editForm').dataset.id = String(editing.id || 0);
  const selectedProduct = parseInt(editing.product_id || 0, 10);
  $('mBody').innerHTML = `
    <input type="hidden" name="company_id" value="${companyFilterId()}">
    <input type="hidden" name="location_id" value="${ADMIN_SCOPE_LOCATION_ID}">
    <div class="row g-3">
      <div class="col-12 col-md-6">
        <label class="form-label">Línea de producción</label>
        <select class="form-select" name="production_line_id" required>${lineOptionsHtml(editing.production_line_id || 0)}</select>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Código TAG</label>
        <input class="form-control mono" name="tag_code" required value="${h(editing.tag_code || '')}" placeholder="Tag_Prod_Bot_L1">
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Nombre TAG</label>
        <input class="form-control" name="tag_name" required value="${h(editing.tag_name || '')}" placeholder="Producción Línea 1">
      </div>
      <div class="col-12 col-md-3">
        <label class="form-label">PLC</label>
        <input class="form-control" name="plc_name" value="${h(editing.plc_name || '')}" placeholder="Micro820 L1">
      </div>
      <div class="col-12 col-md-3">
        <label class="form-label">IP / referencia</label>
        <input class="form-control mono" name="ip_address" value="${h(editing.ip_address || '')}" placeholder="192.168.x.x">
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Artículo reportado</label>
        <select class="form-select" name="product_id" id="plcProductSelect">${productOptionsHtml(selectedProduct)}</select>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Embalaje</label>
        <select class="form-select" name="packaging_option_id" id="plcPackagingSelect">${packagingOptionsHtml(selectedProduct, editing.packaging_option_id || 0)}</select>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Almacén destino producción</label>
        <select class="form-select" name="warehouse_id">${warehouseOptionsHtml(editing.warehouse_id || editing.cf_wh_id || 0)}</select>
        <div class="form-text">Producción Botella debe entrar a ALM01 o cuarentena, no a rutas.</div>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Notas</label>
        <input class="form-control" name="notes" maxlength="255" value="${h(editing.notes || '')}" placeholder="Opcional">
      </div>
      <div class="col-6 col-md-3">
        <label class="form-label">Qty/señal</label>
        <input class="form-control mono" type="number" min="0" step="0.001" name="qty_per_signal" value="${h(editing.qty_per_signal ?? 1)}">
      </div>
      <div class="col-6 col-md-3">
        <label class="form-label">Tarimas/señal</label>
        <input class="form-control mono" type="number" min="0" step="0.001" name="pallets_per_signal" value="${h(editing.pallets_per_signal ?? 1)}">
      </div>
      <div class="col-6 col-md-3">
        <label class="form-label">Paquetes/señal</label>
        <input class="form-control mono" type="number" min="0" step="0.001" name="packages_per_signal" value="${h(editing.packages_per_signal ?? 0)}">
      </div>
      <div class="col-6 col-md-3">
        <label class="form-label">Piezas/señal</label>
        <input class="form-control mono" type="number" min="0" step="0.001" name="pieces_per_signal" value="${h(editing.pieces_per_signal ?? 0)}">
      </div>
      <div class="col-12 col-md-6">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="enabled" ${String(editing.enabled ?? 1)==='1'?'checked':''}>
          <label class="form-check-label">Habilitado para automático</label>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="active" ${String(editing.active ?? 1)==='1'?'checked':''}>
          <label class="form-check-label">Activo</label>
        </div>
      </div>
      <div class="col-12"><div class="alert alert-warning py-2 mb-0">Si una línea tiene más de un TAG activo, Producción Botella mostrará el listado para que el operador seleccione cuál usará al iniciar turno automático.</div></div>
    </div>`;
  $('plcProductSelect')?.addEventListener('change', ()=>{
    const pid = parseInt($('plcProductSelect').value || '0', 10);
    const pack = $('plcPackagingSelect');
    if(pack) pack.innerHTML = packagingOptionsHtml(pid, 0);
  });
  modal.show();
}

async function reloadAll(){
  await loadLocations();
  await loadWarehouses();
  await loadProducts();
  await loadPackaging();
  await loadPlcRefs();
  await loadProductionLines();
  await loadPlcTags();
  await loadRoutes();
  await loadChannels();
  await loadQualityConcepts();
  await loadUsers();
  refreshTableViews();
}

/* ===== Modal Builders ===== */
function companySelectHtml(selected){
  const companyId = parseInt(selected || companyFilterId() || companies[0]?.id || 0, 10);
  const company = companies.find(c => parseInt(c.id,10) === companyId);
  return `
    <input type="hidden" name="company_id" value="${companyId}">
    <input class="form-control" value="${h(company?.name || 'Empresa seleccionada')}" readonly>
    <div class="form-text">La configuración se guarda únicamente para la empresa seleccionada en la parte superior.</div>
  `;
}

function companySelectHtmlWithId(selected, idAttr='') {
  let html = `<select class="form-select" name="company_id" ${idAttr ? `id="${idAttr}"` : ''} required>`;
  for(const c of companies){
    const sel = parseInt(c.id,10)===parseInt(selected||0,10) ? 'selected' : '';
    html += `<option value="${c.id}" ${sel}>${h(c.name)}</option>`;
  }
  html += `</select>`;
  return html;
}

function locationOptionsHtml(locations, companyId, selectedId){
  companyId = parseInt(companyId||0,10);
  selectedId = parseInt(selectedId||0,10);
  return (locations||[])
    .filter(l => companyId <= 0 || parseInt(l.company_id||0,10) === companyId)
    .map(l => `<option value="${l.id}" ${parseInt(l.id,10)===selectedId?'selected':''}>${h(l.name)} (${h(l.code||'')})</option>`)
    .join('');
}

async function openWarehouseModal(row=null){
  mode = 'wh';
  editing = row || {};
  const isNew = !row;
  $('mTitle').textContent = (isNew ? 'Nuevo ' : 'Editar ') + 'Almacén';
  $('mBody').innerHTML = `<div class="text-muted">Cargando localidades...</div>`;
  $('editForm').dataset.id = String(editing.id||0);
  modal.show();

  const defaultCompany = editing.company_id || companyFilterId() || companies[0]?.id || 0;
  const jl = await apiGet('locations_list', {company_id: defaultCompany});
  const locs = jl.ok ? (jl.rows||[]) : [];
  const defaultLocation = editing.location_id || 0;

  $('mBody').innerHTML = `
    <div class="row g-3">
      <div class="col-12 col-md-6">
        <label class="form-label">Empresa</label>
        ${companySelectHtml(defaultCompany)}
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Localidad</label>
        <select class="form-select" name="location_id" id="whLocationSelect" required>
          ${locationOptionsHtml(locs, defaultCompany, defaultLocation)}
        </select>
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Código</label>
        <input class="form-control mono" name="code" required value="${h(editing.code||'')}" placeholder="CF1, CF2, CF3, MERMA...">
        <div class="form-text">Usa códigos cortos y estables. Ej. CF1, CF2, CF3, CUB, MERMA.</div>
      </div>
      <div class="col-12 col-md-8">
        <label class="form-label">Nombre</label>
        <input class="form-control" name="name" required value="${h(editing.name||'')}" placeholder="Cuarto frío 1, Merma pendiente...">
      </div>
      <div class="col-12">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="active" ${String(editing.active??1)==='1'?'checked':''}>
          <label class="form-check-label">Activo</label>
        </div>
      </div>
      <div class="col-12">
        <div class="alert alert-warning py-2 mb-0">
          Crear o editar almacenes no mueve inventario. Para mover existencia usa los módulos operativos o Ajuste almacenes.
        </div>
      </div>
    </div>
  `;

  const locationSel = $('whLocationSelect');
  if (locationSel && !locationSel.value && locationSel.options.length) {
    locationSel.selectedIndex = 0;
  }
}


function openPackagingModal(product, option=null){
  mode = 'packOption';
  editing = option || {};
  const isNew = !option;
  const productId = parseInt(product.product_id || product.id || 0, 10);
  const isAdditional = isNew && Boolean(product.has_options);

  $('mTitle').textContent = isNew
    ? (isAdditional ? 'Agregar embalaje adicional' : 'Configurar embalaje principal')
    : 'Editar embalaje';
  $('editForm').dataset.id = String(option?.packaging_id || 0);

  const pieces = parseInt(option?.pieces_per_package || 0, 10);
  const packs = parseInt(option?.packages_per_pallet || 0, 10);
  const pallet = pieces > 0 && packs > 0 ? pieces * packs : 0;
  const defaultChecked = isNew ? !isAdditional : parseInt(option?.is_default || 0, 10) === 1;

  $('mBody').innerHTML = `
    <input type="hidden" name="company_id" value="${companyFilterId()}">
    <input type="hidden" name="product_id" value="${productId}">
    <input type="hidden" name="packaging_id" value="${option?.packaging_id || 0}">
    <div class="mb-3">
      <div class="small text-muted">Artículo</div>
      <div class="fw-bold mono">${h(product.sku || '')}${product.erp_id ? ` · DLAX ${h(product.erp_id)}` : ''}</div>
      <div>${h(product.product_name || product.name || '')}</div>
    </div>
    <div class="row g-3">
      <div class="col-12 col-md-4">
        <label class="form-label">Tipo de embalaje</label>
        <select class="form-select" name="packaging_type">
          ${['TARIMA','RACK','CAJA','CONTENEDOR','OTRO'].map(t => `<option value="${t}" ${String(option?.packaging_type || 'TARIMA')===t ? 'selected' : ''}>${t}</option>`).join('')}
        </select>
      </div>
      <div class="col-12 col-md-8">
        <label class="form-label">Nombre / identificación</label>
        <input class="form-control" name="packaging_name" maxlength="100" value="${h(option?.packaging_name || '')}" placeholder="Ej. Tarima 600 ml · 108 paquetes">
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Piezas por paquete / caja</label>
        <input class="form-control mono" type="number" min="1" step="1" name="pieces_per_package" value="${pieces > 0 ? pieces : ''}" placeholder="Ej. 12" required>
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Paquetes / cajas por tarima</label>
        <input class="form-control mono" type="number" min="1" step="1" name="packages_per_pallet" value="${packs > 0 ? packs : ''}" placeholder="Ej. 108" required>
      </div>
      <div class="col-12">
        <label class="form-label">Notas</label>
        <input class="form-control" name="notes" maxlength="255" value="${h(option?.notes || '')}" placeholder="Opcional: uso especial o referencia operativa.">
      </div>
      <div class="col-12 col-md-6">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="is_default" ${defaultChecked ? 'checked' : ''}>
          <label class="form-check-label">Embalaje principal</label>
          <div class="form-text">El principal mantiene compatibilidad con la configuración actual del artículo.</div>
        </div>
      </div>
      <div class="col-12 col-md-6">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="active" ${String(option?.packaging_active ?? 1)==='1' ? 'checked' : ''}>
          <label class="form-check-label">Embalaje activo</label>
        </div>
      </div>
      <div class="col-12">
        <div class="alert alert-info mb-0">
          Piezas totales: <strong class="mono">${pallet > 0 ? pallet : 'se calculará al guardar'}</strong>. Esta configuración no mueve inventario.
        </div>
      </div>
    </div>`;
  modal.show();
}

async function openRouteModal(row=null){
  mode = 'route';
  editing = row || {};
  const isNew = !row;
  const companyId = editing.company_id || companyFilterId() || companies[0]?.id || 0;
  $('mTitle').textContent = (isNew ? 'Nueva ' : 'Editar ') + 'Ruta';
  $('mBody').innerHTML = '<div class="text-muted">Cargando localidades...</div>';
  $('editForm').dataset.id = String(editing.id || 0);
  modal.show();

  const j = await apiGet('locations_list', {company_id: companyId});
  const locs = j.ok ? (j.rows || []) : [];
  const locationId = editing.location_id || locs[0]?.id || 0;

  $('mBody').innerHTML = `
    <div class="row g-3">
      <div class="col-12 col-md-6">
        <label class="form-label">Empresa</label>
        ${companySelectHtml(companyId)}
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Localidad / planta</label>
        <select class="form-select" name="location_id" required>
          ${locationOptionsHtml(locs, companyId, locationId)}
        </select>
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Número de ruta</label>
        <input class="form-control mono" type="number" min="1" step="1" name="route_no" required value="${h(editing.route_no || '')}" placeholder="1">
        <div class="form-text">Compatible con route_no de pedidos.</div>
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Código / clave de ruta</label>
        <input class="form-control mono" name="code" required value="${h(editing.code || '')}" placeholder="RUTA-01">
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Orden visual</label>
        <input class="form-control mono" type="number" name="sort_order" value="${h(editing.sort_order ?? 0)}">
      </div>
      <div class="col-12">
        <label class="form-label">Nombre de ruta</label>
        <input class="form-control" name="name" required value="${h(editing.name || '')}" placeholder="Ruta Centro / Ruta Reynosa Norte">
      </div>
      <div class="col-12">
        <label class="form-label">Descripción / cobertura</label>
        <input class="form-control" name="description" maxlength="255" value="${h(editing.description || '')}" placeholder="Colonias o clientes atendidos">
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Código de acceso repartidor ${isNew ? '' : '(opcional)'}</label>
        <input class="form-control mono" type="password" name="access_code" value="" ${isNew ? 'required' : ''} placeholder="${isNew ? 'Requerido nueva ruta' : 'Vacío conserva actual'}">
        <div class="form-text">${isNew ? 'Se guardará protegido por SHA-256.' : 'Captura solo si deseas cambiarlo.'}</div>
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Chofer predeterminado</label>
        <input class="form-control" name="driver_name" value="${h(editing.driver_name || '')}">
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Unidad predeterminada</label>
        <input class="form-control mono" name="vehicle_unit" value="${h(editing.vehicle_unit || '')}">
      </div>
      <div class="col-12 col-md-4">
        <label class="form-label">Días de entrega</label>
        <input class="form-control" name="delivery_days" value="${h(editing.delivery_days || '')}" placeholder="Lun, Mié, Vie">
      </div>
      <div class="col-12">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="active" ${String(editing.active ?? 1)==='1' ? 'checked' : ''}>
          <label class="form-check-label">Ruta activa</label>
        </div>
      </div>
    </div>
  `;
}

function openModal(kind, row=null){
  mode = kind;
  editing = row || {};
  const isNew = !row;

  const titleMap = {loc:'Localidad', wh:'Almacén', prod:'Producto', chan:'Canal', qc:'Concepto Calidad', user:'Usuario'};
  $('mTitle').textContent = (isNew ? 'Nuevo ' : 'Editar ') + (titleMap[kind] || 'Registro');

  let body = '';

  if(kind==='loc'){
    body = `
      <div class="row g-3">
        <div class="col-12 col-md-6">
          <label class="form-label">Empresa</label>
          ${companySelectHtml(editing.company_id || companyFilterId() || companies[0]?.id)}
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Timezone</label>
          <input class="form-control mono" name="timezone" value="${h(editing.timezone||'America/Monterrey')}">
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Nombre</label>
          <input class="form-control" name="name" required value="${h(editing.name||'')}">
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Code</label>
          <input class="form-control mono" name="code" required value="${h(editing.code||'')}">
        </div>
        <div class="col-12">
          <div class="form-check">
            <input class="form-check-input" type="checkbox" name="active" ${String(editing.active??1)==='1'?'checked':''}>
            <label class="form-check-label">Activa</label>
          </div>
        </div>
      </div>
    `;
  }

  if(kind==='prod'){
    body = `
      <div class="row g-3">
        <div class="col-12 col-md-6">
          <label class="form-label">Empresa</label>
          ${companySelectHtml(editing.company_id || companyFilterId() || companies[0]?.id)}
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">UOM</label>
          <input class="form-control mono" name="uom" value="${h(editing.uom||'pz')}">
        </div>

        <div class="col-12 col-md-6">
          <label class="form-label">Artículo</label>
          <input class="form-control mono" name="sku" required value="${h(editing.sku||'')}">
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label"># DLAX (ID ERP)</label>
          <input class="form-control mono" name="erp_id" value="${h(editing.erp_id||'')}">
          <div class="form-text">Opcional. ID del artículo en ERP (NetSuite, etc.).</div>
        </div>

        <div class="col-12">
          <label class="form-label">Descripción</label>
          <input class="form-control" name="name" required value="${h(editing.name||'')}">
        </div>

        <div class="col-12"><hr class="my-1"><div class="fw-semibold">Presentación logística</div><div class="small text-muted">Útil para Hielo Bolsa, Botella, Garrafón y surtido por tarima.</div></div>
        <div class="col-12 col-md-6">
          <label class="form-label">Piezas por paquete / caja</label>
          <input class="form-control mono" type="number" min="1" step="1" name="pieces_per_package" value="${h(editing.pieces_per_package||'')}">
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Paquetes / cajas por tarima</label>
          <input class="form-control mono" type="number" min="1" step="1" name="packages_per_pallet" value="${h(editing.packages_per_pallet||'')}">
          <div class="form-text">Piezas por tarima se calcula automáticamente.</div>
        </div>

        <div class="col-12">
          <div class="form-check">
            <input class="form-check-input" type="checkbox" name="active" ${String(editing.active??1)==='1'?'checked':''}>
            <label class="form-check-label">Activo</label>
          </div>
        </div>
        <div class="col-12">
          <div class="alert alert-info py-2 mb-0">
            Guarda el artículo y después usa el botón <strong>Ver / configurar almacenes</strong> para indicar por localidad dónde entra producto terminado, merma, aprovechamiento y cubicado.
          </div>
        </div>
      </div>
    `;
  }

  if(kind==='chan'){
    body = `
      <div class="row g-3">
        <div class="col-12 col-md-6">
          <label class="form-label">Empresa</label>
          ${companySelectHtml(editing.company_id || companyFilterId() || companies[0]?.id)}
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Orden</label>
          <input class="form-control mono" name="sort_order" value="${h(editing.sort_order??0)}">
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Nombre</label>
          <input class="form-control" name="name" required value="${h(editing.name||'')}">
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Tipo</label>
          <select class="form-select mono" name="channel_type">
            ${['SALE','TRANSFER','INTERNAL'].map(t=>{
              const sel = (editing.channel_type||'SALE')===t?'selected':'';
              return `<option ${sel} value="${t}">${t}</option>`;
            }).join('')}
          </select>
          <div class="form-text">SALE=venta · TRANSFER=transferencia · INTERNAL=uso interno</div>
        </div>
        <div class="col-12">
          <div class="form-check">
            <input class="form-check-input" type="checkbox" name="active" ${String(editing.active??1)==='1'?'checked':''}>
            <label class="form-check-label">Activo</label>
          </div>
        </div>
      </div>
    `;
  }

  if(kind==='qc'){
    const t = editing.concept_type || $('qcType').value || 'MERMA';
    body = `
      <div class="row g-3">
        <div class="col-12 col-md-6">
          <label class="form-label">Empresa</label>
          ${companySelectHtml(editing.company_id || companyFilterId() || companies[0]?.id)}
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Tipo</label>
          <select class="form-select mono" name="concept_type">
            ${['MERMA','SCRAP'].map(x=>{
              const sel = String(t)===x?'selected':'';
              return `<option ${sel} value="${x}">${x}</option>`;
            }).join('')}
          </select>
        </div>
        <div class="col-12">
          <label class="form-label">Concepto</label>
          <input class="form-control" name="name" required value="${h(editing.name||'')}">
        </div>
        <div class="col-12 col-md-6">
          <label class="form-label">Orden</label>
          <input class="form-control mono" name="sort_order" value="${h(editing.sort_order??0)}">
        </div>
        <div class="col-12 col-md-6 d-flex align-items-end">
          <div class="form-check">
            <input class="form-check-input" type="checkbox" name="active" ${String(editing.active??1)==='1'?'checked':''}>
            <label class="form-check-label">Activo</label>
          </div>
        </div>
      </div>
    `;
  }

  if(kind==='user'){
    // este modal lo rellenamos completo con fetch de users_get para traer roles/accesos
    body = `<div class="text-muted">Cargando...</div>`;
  }

  $('mBody').innerHTML = body;
  $('editForm').dataset.id = String(editing.id||0);
  modal.show();
}

function formToObj(){
  const fd = new FormData($('editForm'));
  const o = {};
  for(const [k,v] of fd.entries()) o[k]=v;

  // comunes
  o.active = $('editForm').querySelector('[name="active"]')?.checked ? 1 : 0;
  if ($('editForm').querySelector('[name="enabled"]')) o.enabled = $('editForm').querySelector('[name="enabled"]').checked ? 1 : 0;
  if ($('editForm').querySelector('[name="is_default"]')) {
    o.is_default = $('editForm').querySelector('[name="is_default"]').checked ? 1 : 0;
  }
  if (o.sort_order !== undefined) o.sort_order = parseInt(o.sort_order||'0',10);
  if (o.company_id !== undefined) o.company_id = parseInt(o.company_id||'0',10);
  if (o.location_id !== undefined) o.location_id = parseInt(o.location_id||'0',10);
  if (o.pieces_per_package !== undefined) o.pieces_per_package = parseInt(o.pieces_per_package||'0',10);
  if (o.packages_per_pallet !== undefined) o.packages_per_pallet = parseInt(o.packages_per_pallet||'0',10);
  if (o.route_no !== undefined) o.route_no = parseInt(o.route_no||'0',10);
  if (o.line_no !== undefined) o.line_no = parseInt(o.line_no||'0',10);
  if (o.production_line_id !== undefined) o.production_line_id = parseInt(o.production_line_id||'0',10);
  if (o.warehouse_id !== undefined) o.warehouse_id = parseInt(o.warehouse_id||'0',10);
  if (o.qty_per_signal !== undefined) o.qty_per_signal = parseFloat(o.qty_per_signal||'0');
  if (o.pallets_per_signal !== undefined) o.pallets_per_signal = parseFloat(o.pallets_per_signal||'0');
  if (o.packages_per_signal !== undefined) o.packages_per_signal = parseFloat(o.packages_per_signal||'0');
  if (o.pieces_per_signal !== undefined) o.pieces_per_signal = parseFloat(o.pieces_per_signal||'0');
  if (o.product_id !== undefined) o.product_id = parseInt(o.product_id||'0',10);
  if (o.packaging_id !== undefined) o.packaging_id = parseInt(o.packaging_id||'0',10);
  if (o.packaging_option_id !== undefined) o.packaging_option_id = parseInt(o.packaging_option_id||'0',10);

  return o;
}

/* ===== Guardar modal según modo ===== */
async function saveModal(){
  const id = parseInt($('editForm').dataset.id||'0',10);

  if (mode === 'user') {
    // Captura usuario + roles + accesos
    const payload = {};
    payload.id = id>0 ? id : 0;
    payload.username  = $('editForm').querySelector('[name="username"]').value.trim();
    payload.full_name = $('editForm').querySelector('[name="full_name"]').value.trim();
    payload.email     = $('editForm').querySelector('[name="email"]').value.trim();
    payload.phone     = $('editForm').querySelector('[name="phone"]').value.trim();
    payload.active    = $('editForm').querySelector('[name="active"]').checked ? 1 : 0;

    const pwd = $('editForm').querySelector('[name="password"]').value;
    if (pwd && pwd.trim() !== '') payload.password = pwd;

    payload.role_codes = Array.from($('editForm').querySelectorAll('input[name="role_codes[]"]:checked')).map(x=>x.value);
    payload.company_ids = Array.from($('editForm').querySelectorAll('input[name="company_ids[]"]:checked')).map(x=>parseInt(x.value,10));
    payload.location_ids = Array.from($('editForm').querySelectorAll('input[name="location_ids[]"]:checked')).map(x=>parseInt(x.value,10));

    const j = await apiPost('users_save', payload);
    if(!j.ok){ notifyError(j.error||'Error'); return; }

    modal.hide();
    notifySuccess('Usuario guardado correctamente.');
    await loadUsers();
    return;
  }

  const payload = formToObj();
  if(id>0) payload.id=id;

  let action = '';
  if(mode==='loc') action = 'locations_save';
  if(mode==='wh') action = 'warehouses_save';
  if(mode==='prod') action = 'products_save';
  if(mode==='packOption') action = 'packaging_option_save';
  if(mode==='pline') action = 'production_lines_save';
  if(mode==='ptag') action = 'plc_tags_save';
  if(mode==='route') action = 'routes_save';
  if(mode==='chan') action = 'channels_save';
  if(mode==='qc') action = 'quality_concepts_save';

  const j = await apiPost(action, payload);
  if(!j.ok){ notifyError(j.error || 'Error'); return; }

  modal.hide();
  notifySuccess('Registro guardado correctamente.');
  await reloadAll();
  refreshTableViews();
  if($('dataTablesBox')) { dataLoadTables(); dataLoadPresets(); }
}

async function findRow(kind, id){
  const company_id = companyFilterId();
  let action = '';
  if(kind==='loc') action='locations_list';
  if(kind==='wh') action='warehouses_list';
  if(kind==='prod') action='products_list';
  if(kind==='route') action='routes_list';
  if(kind==='pline') action='production_lines_list';
  if(kind==='ptag') action='plc_tags_list';
  if(kind==='chan') action='channels_list';
  if(kind==='qc') action='quality_concepts_list';

  // qc depende del tipo seleccionado
  let params = {company_id};
  if (kind==='qc') params.concept_type = $('qcType').value || 'MERMA';

  const j = await apiGet(action, params);
  const row = (j.rows||[]).find(x=>parseInt(x.id,10)===parseInt(id,10));
  return row || null;
}

/* ===== HUB navigation ===== */
function showPanel(name){
  const panels = {
    loc: $('panel-loc'),
    wh: $('panel-wh'),
    prod: $('panel-prod'),
    pack: $('panel-pack'),
    plcprod: $('panel-plcprod'),
    routes: $('panel-routes'),
    chan: $('panel-chan'),
    qual: $('panel-qual'),
    users: $('panel-users'),
    invadj: $('panel-invadj'),
    audit: $('panel-audit'),
    cubaudit: $('panel-cubaudit'),
    data: $('panel-data'),
  };
  for(const k of Object.keys(panels)){
    panels[k].classList.toggle('show', k===name);
  }
  document.querySelectorAll('.cardHub').forEach(c=>{
    c.classList.toggle('active', c.dataset.panel===name);
  });
}

/* ===== User modal builder ===== */
function checkListHtml(items, checkedSet, nameAttr, labelFn){
  return items.map(it=>{
    const val = String(it.value);
    const checked = checkedSet.has(val) ? 'checked' : '';
    return `
      <div class="form-check">
        <input class="form-check-input" type="checkbox" name="${nameAttr}" value="${h(val)}" id="${nameAttr}_${h(val)}" ${checked}>
        <label class="form-check-label" for="${nameAttr}_${h(val)}">${labelFn(it)}</label>
      </div>
    `;
  }).join('');
}

async function openUserModal(userId){
  mode = 'user';
  const isNew = !userId;

  $('mTitle').textContent = isNew ? 'Nuevo usuario' : 'Editar usuario';
  $('mBody').innerHTML = `<div class="text-muted">Cargando...</div>`;
  $('editForm').dataset.id = String(userId||0);
  modal.show();

  // base data
  if (!roles.length) await loadRoles();
  if (!companies.length) await loadCompanies();

  let user = {username:'',full_name:'',email:'',phone:'',active:1};
  let userRoleCodes = [];
  let access = {company_ids:[], location_ids:[]};

  if (!isNew) {
    const j = await apiGet('users_get', {id:userId});
    if (!j.ok) { $('mBody').innerHTML = `<div class="alert alert-danger">${h(j.error||'Error')}</div>`; return; }
    user = j.row;
    userRoleCodes = j.roles || [];
    access = j.access || access;
  }

  // locations ALL (para asignación multi-empresa)
  const jl = await apiGet('locations_list', {company_id: 0});
  const allLocs = (jl.ok ? (jl.rows||[]) : []).map(x=>({
    id:x.id, name:x.name, company_name:x.company_name, company_id:x.company_id
  }));

  const roleSet = new Set((userRoleCodes||[]).map(String));
  const compSet = new Set((access.company_ids||[]).map(x=>String(x)));
  const locSet  = new Set((access.location_ids||[]).map(x=>String(x)));

  const roleItems = roles.map(r=>({value:r.code, label:`${r.name} (${r.code})`}));
  const compItems = companies.map(c=>({value:c.id, label:`${c.name}`}));
  const locItems  = allLocs.map(l=>({value:l.id, label:`${l.company_name} · ${l.name}`}));

  const body = `
    <div class="row g-3">
      <div class="col-12 col-md-6">
        <label class="form-label">Username</label>
        <input class="form-control mono" name="username" required value="${h(user.username||'')}">
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Nombre</label>
        <input class="form-control" name="full_name" required value="${h(user.full_name||'')}">
      </div>

      <div class="col-12 col-md-6">
        <label class="form-label">Email</label>
        <input class="form-control" name="email" value="${h(user.email||'')}">
      </div>
      <div class="col-12 col-md-6">
        <label class="form-label">Teléfono</label>
        <input class="form-control mono" name="phone" value="${h(user.phone||'')}">
      </div>

      <div class="col-12 col-md-6">
        <label class="form-label">${isNew ? 'Password' : 'Password (opcional)'} </label>
        <input class="form-control mono" name="password" type="password" autocomplete="new-password" placeholder="${isNew ? 'Requerido' : 'Deja vacío para no cambiar'}">
        <div class="form-text">${isNew ? 'Se guardará con password_hash().' : 'Si lo llenas, se actualizará.'}</div>
      </div>

      <div class="col-12 col-md-6 d-flex align-items-end">
        <div class="form-check">
          <input class="form-check-input" type="checkbox" name="active" ${String(user.active??1)==='1'?'checked':''}>
          <label class="form-check-label">Activo</label>
        </div>
      </div>

      <hr class="my-2">

      <div class="col-12 col-lg-4">
        <div class="fw-semibold mb-1"><i class="bi bi-person-badge me-1"></i>Roles</div>
        <div class="checkgrid">
          ${checkListHtml(
            roleItems,
            roleSet,
            'role_codes[]',
            (it)=>h(it.label)
          )}
        </div>
        <div class="miniHint mt-1">Ej: admin, corp, production, quality…</div>
      </div>

      <div class="col-12 col-lg-4">
        <div class="fw-semibold mb-1"><i class="bi bi-building me-1"></i>Empresas permitidas</div>
        <div class="checkgrid">
          ${checkListHtml(
            compItems,
            compSet,
            'company_ids[]',
            (it)=>h(it.label)
          )}
        </div>
        <div class="miniHint mt-1">Controla lo que verá en Select Scope.</div>
      </div>

      <div class="col-12 col-lg-4">
        <div class="fw-semibold mb-1"><i class="bi bi-geo-alt me-1"></i>Localidades permitidas</div>
        <div class="checkgrid">
          ${checkListHtml(
            locItems,
            locSet,
            'location_ids[]',
            (it)=>h(it.label)
          )}
        </div>
        <div class="miniHint mt-1">Puedes asignar varias localidades.</div>
      </div>
    </div>
  `;

  $('mBody').innerHTML = body;
}



/* ===== Ajuste almacenes ===== */
function qtyClass(v){ v=parseFloat(v||0); if(v>0) return 'qty-pos'; if(v<0) return 'qty-neg'; return 'qty-zero'; }
function fmt3(v){ const n=parseFloat(v||0); return (isNaN(n)?0:n).toFixed(3); }

async function invLoadCompanies(){
  const sel = $('invCompany');
  if(!sel) return;
  sel.innerHTML = companies.map(c=>`<option value="${c.id}">${h(c.name)}</option>`).join('');
  if(companyFilterId()>0) sel.value = String(companyFilterId());
  await invLoadLocations();
}
async function invLoadLocations(){
  const companyId = parseInt($('invCompany')?.value||'0',10);
  if(!companyId) return;
  const j = await apiGet('inventory_adjust_locations', {company_id:companyId});
  const rows = j.ok ? (j.rows||[]) : [];
  $('invLocation').innerHTML = rows.map(l=>`<option value="${l.id}">${h(l.name)} (${h(l.code||'')})</option>`).join('');
  await invLoadWarehousesProducts();
}
async function invLoadWarehousesProducts(){
  const companyId = parseInt($('invCompany')?.value||'0',10);
  const locationId = parseInt($('invLocation')?.value||'0',10);
  if(!companyId || !locationId) return;
  const [w,p] = await Promise.all([
    apiGet('inventory_adjust_warehouses', {company_id:companyId, location_id:locationId}),
    apiGet('inventory_adjust_products', {company_id:companyId})
  ]);
  $('invWarehouse').innerHTML = (w.ok?w.rows:[]).map(x=>`<option value="${x.id}">${h(x.code)} · ${h(x.name)}</option>`).join('');
  $('invProduct').innerHTML = (p.ok?p.rows:[]).map(x=>`<option value="${x.id}">${h(x.sku)} · ${h(x.name)}</option>`).join('');
  await invLoadSnapshot();
}
async function invLoadSnapshot(){
  const companyId = parseInt($('invCompany')?.value||'0',10);
  const locationId = parseInt($('invLocation')?.value||'0',10);
  if(!companyId || !locationId) return;
  const j = await apiGet('inventory_adjust_snapshot', {company_id:companyId, location_id:locationId});
  if(!j.ok){ alert(j.error||'Error al cargar inventario'); return; }
  $('invTotalsBody').innerHTML = (j.totals||[]).map(r=>`
    <tr>
      <td class="mono">${h(r.sku)}</td>
      <td>${h(r.product_name)}</td>
      <td class="text-end mono ${qtyClass(r.qty_total)}">${fmt3(r.qty_total)}</td>
    </tr>
  `).join('');
  $('invDetailBody').innerHTML = (j.rows||[]).map(r=>`
    <tr>
      <td class="mono">${h(r.sku)}</td>
      <td><span class="mono">${h(r.warehouse_code)}</span> · ${h(r.warehouse_name)}</td>
      <td class="text-end mono ${qtyClass(r.qty)}">${fmt3(r.qty)}</td>
    </tr>
  `).join('');
}
async function invUnlock(){
  const code = prompt('Código de autorización para ajuste de almacenes:');
  if(code === null) return;
  const j = await apiPost('inventory_adjust_auth', {code});
  if(!j.ok){ alert(j.error||'Código incorrecto'); return; }
  invUnlocked = true;
  invCode = code;
  $('invAdjustArea')?.classList.remove('invAdjustLocked');
  $('btnInvUnlock').innerHTML = '<i class="bi bi-unlock me-1"></i>Código validado';
  $('btnInvUnlock').classList.remove('btn-warning');
  $('btnInvUnlock').classList.add('btn-success');
}
async function invSaveAdjust(){
  if(!invUnlocked){ await invUnlock(); if(!invUnlocked) return; }
  const payload = {
    code: invCode,
    company_id: parseInt($('invCompany').value||'0',10),
    location_id: parseInt($('invLocation').value||'0',10),
    warehouse_id: parseInt($('invWarehouse').value||'0',10),
    product_id: parseInt($('invProduct').value||'0',10),
    kind: $('invKind').value,
    qty: parseFloat(String($('invQty').value||'0').replace(',','.'))||0,
    notes: $('invNotes').value.trim()
  };
  if(payload.qty <= 0){ alert('Captura una cantidad mayor a 0.'); return; }
  const txt = payload.kind === 'SUB' ? 'quitar' : 'agregar';
  if(!confirm(`¿Confirmas ${txt} ${fmt3(payload.qty)} al producto seleccionado?`)) return;
  const j = await apiPost('inventory_adjust_save', payload);
  if(!j.ok){ alert(j.error||'Error al guardar ajuste'); return; }
  $('invQty').value = '0';
  $('invNotes').value = '';
  await invLoadSnapshot();
  alert('Ajuste guardado correctamente.');
}

async function invSaveCut(){
  if(!invUnlocked){ await invUnlock(); if(!invUnlocked) return; }
  const payload = {
    code: invCode,
    company_id: parseInt($('invCompany').value||'0',10),
    location_id: parseInt($('invLocation').value||'0',10),
    warehouse_id: parseInt($('invWarehouse').value||'0',10),
    product_id: parseInt($('invProduct').value||'0',10),
    target_qty: parseFloat(String($('invTargetQty').value||'0').replace(',','.'))||0,
    notes: $('invCutNotes').value.trim()
  };
  if(payload.target_qty < 0){ alert('La cantidad física no puede ser negativa.'); return; }

  const prodText = $('invProduct').options[$('invProduct').selectedIndex]?.text || 'producto seleccionado';
  const whText = $('invWarehouse').options[$('invWarehouse').selectedIndex]?.text || 'almacén seleccionado';
  const msg = `¿Confirmas dejar ${prodText} en ${whText} con existencia física ${fmt3(payload.target_qty)}?\n\nEl sistema calculará y registrará solo la diferencia en inventory_moves.`;
  if(!confirm(msg)) return;

  const j = await apiPost('inventory_cut_save', payload);
  if(!j.ok){ alert(j.error||'Error al aplicar corte físico'); return; }

  $('invCutNotes').value = '';
  await invLoadSnapshot();

  const delta = parseFloat(j.delta||0);
  if(Math.abs(delta) <= 0.0001){
    alert(j.message || 'Sin diferencia.');
  } else {
    alert(`Corte aplicado. Diferencia: ${fmt3(delta)} | Movimiento: ${j.move_type} ${fmt3(j.qty)}`);
  }
}



/* ===== Auditoría movimientos ===== */
function todayIso(){ return new Date().toISOString().slice(0,10); }
function monthStartIso(){ const d=new Date(); d.setDate(1); return d.toISOString().slice(0,10); }
async function auditInit(){
  const c=$('audCompany'); if(!c) return;
  c.innerHTML = '<option value="0">Todas las empresas</option>' + companies.map(x=>`<option value="${x.id}">${h(x.name)}</option>`).join('');
  if(companyFilterId()>0) c.value = String(companyFilterId());
  if(!$('audDateFrom').value) $('audDateFrom').value = monthStartIso();
  if(!$('audDateTo').value) $('audDateTo').value = todayIso();
  auditPage = 1;
  await auditLoadLocationsProductsWarehouses();
  await auditLoad();
}
async function auditLoadLocationsProductsWarehouses(){
  const companyId=parseInt($('audCompany')?.value||'0',10);
  const loc=await apiGet('inventory_adjust_locations',{company_id:companyId});
  $('audLocation').innerHTML = '<option value="0">Todas</option>' + (loc.ok?loc.rows:[]).map(x=>`<option value="${x.id}">${h(x.company_name)} · ${h(x.name)} (${h(x.code||'')})</option>`).join('');

  if(companyId>0){
    const pr=await apiGet('inventory_adjust_products',{company_id:companyId});
    $('audProduct').innerHTML = '<option value="0">Todos</option>' + (pr.ok?pr.rows:[]).map(x=>`<option value="${x.id}">${h(x.sku)} · ${h(x.name)}</option>`).join('');
  } else {
    $('audProduct').innerHTML = '<option value="0">Todos</option>';
  }
  await auditLoadWarehouses();
}
async function auditLoadWarehouses(){
  const companyId=parseInt($('audCompany')?.value||'0',10);
  const locationId=parseInt($('audLocation')?.value||'0',10);
  if(companyId>0 && locationId>0){
    const w=await apiGet('inventory_adjust_warehouses',{company_id:companyId,location_id:locationId});
    $('audWarehouse').innerHTML = '<option value="0">Todos</option>' + (w.ok?w.rows:[]).map(x=>`<option value="${x.id}">${h(x.code)} · ${h(x.name)}</option>`).join('');
  } else {
    $('audWarehouse').innerHTML = '<option value="0">Todos</option>';
  }
}
async function auditLoad(){
  if(!$('audBody')) return;
  const payload={
    company_id:$('audCompany').value||'0',
    location_id:$('audLocation').value||'0',
    product_id:$('audProduct').value||'0',
    warehouse_id:$('audWarehouse').value||'0',
    move_type:$('audMoveType').value||'',
    ref_type:$('audRefType').value.trim(),
    q:$('audSearch').value.trim(),
    date_from:$('audDateFrom').value||monthStartIso(),
    date_to:$('audDateTo').value||todayIso(),
    page:auditPage,
    per_page:$('audPerPage')?.value||'50'
  };
  const j=await apiGet('inventory_audit_log',payload);
  if(!j.ok){ alert(j.error||'Error al consultar auditoría'); return; }
  const s=j.summary||{};
  $('audMovesCount').textContent = String(parseInt(s.moves_count||0,10));
  $('audTotalIn').textContent = fmt3(s.total_in||0);
  $('audTotalOut').textContent = fmt3(s.total_out||0);
  $('audTotalAdj').textContent = fmt3(s.total_adj||0);
  $('audNetQty').textContent = fmt3(s.net_qty||0);
  $('audNetQty').className = 'fw-bold fs-4 mono ' + qtyClass(s.net_qty||0);

  const pg = j.pagination || {page:1,total_pages:1,total_rows:(j.rows||[]).length,per_page:50,has_prev:false,has_next:false};
  auditPage = parseInt(pg.page||1,10);
  auditTotalPages = Math.max(1, parseInt(pg.total_pages||1,10));
  if($('audPageInfo')) $('audPageInfo').textContent = `Página ${auditPage} de ${auditTotalPages} · ${parseInt(pg.total_rows||0,10)} registros · ${parseInt(pg.per_page||50,10)} por página`;
  if($('audFirstPage')) $('audFirstPage').disabled = auditPage <= 1;
  if($('audPrevPage')) $('audPrevPage').disabled = auditPage <= 1;
  if($('audNextPage')) $('audNextPage').disabled = auditPage >= auditTotalPages;
  if($('audLastPage')) $('audLastPage').disabled = auditPage >= auditTotalPages;

  $('audBody').innerHTML = (j.rows||[]).map(r=>`
    <tr>
      <td class="mono small">${h(r.created_at||'')}</td>
      <td class="mono small">${h(r.work_date||'')}</td>
      <td><div class="mono fw-semibold">${h(r.sku||'')}</div><div class="small text-muted">${h(r.product_name||'')}</div></td>
      <td><span class="mono">${h(r.warehouse_code||'SIN-WH')}</span></td>
      <td><span class="badge ${r.move_type==='OUT'?'text-bg-danger':(r.move_type==='ADJ'?'text-bg-warning':'text-bg-success')}">${h(r.move_type)}</span></td>
      <td class="text-end mono ${qtyClass(r.signed_qty)}">${fmt3(r.qty)}</td>
      <td class="mono small">${h(r.ref_type||'')}</td>
      <td class="mono small">${h(r.ref_id??'')}</td>
      <td>${h(r.user_name||'')}</td>
      <td class="small">${h(r.notes||'')}</td>
      <td class="text-end">
        <button class="btn btn-outline-primary btn-sm btn-pill" data-edit-move="${h(r.id)}" title="Corregir movimiento">
          <i class="bi bi-pencil-square"></i>
        </button>
      </td>
    </tr>
  `).join('') || '<tr><td colspan="11" class="text-center text-muted py-4">Sin movimientos con los filtros seleccionados.</td></tr>';

  $('audRefBody').innerHTML = (j.by_ref_type||[]).map(r=>`<tr><td class="mono">${h(r.ref_type)}</td><td class="text-end mono">${h(r.moves_count)}</td><td class="text-end mono ${qtyClass(r.net_qty)}">${fmt3(r.net_qty)}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center text-muted">Sin datos</td></tr>';
  $('audUserBody').innerHTML = (j.by_user||[]).map(r=>`<tr><td>${h(r.user_name)}</td><td class="text-end mono">${h(r.moves_count)}</td><td class="text-end mono ${qtyClass(r.net_qty)}">${fmt3(r.net_qty)}</td></tr>`).join('') || '<tr><td colspan="3" class="text-center text-muted">Sin datos</td></tr>';
}



function toDatetimeLocalValue(s){
  s = String(s || '').trim();
  if(!s) return '';
  return s.replace(' ', 'T').slice(0,16);
}

async function openMoveEditModal(moveId){
  moveId = parseInt(moveId || 0, 10);
  if(moveId <= 0) return;

  $('moveEditSub').textContent = `Movimiento #${moveId}`;
  $('moveEditMeta').innerHTML = '<span class="text-muted">Cargando movimiento...</span>';
  $('moveEditForm').reset();
  $('moveEditId').value = String(moveId);
  moveEditModal.show();

  const j = await apiGet('inventory_move_get', {id: moveId});
  if(!j.ok){
    $('moveEditMeta').innerHTML = `<span class="text-danger">${h(j.error || 'No se pudo cargar el movimiento.')}</span>`;
    return;
  }

  const r = j.row || {};
  $('moveEditWorkDate').value = String(r.work_date || '');
  $('moveEditCreatedAt').value = toDatetimeLocalValue(r.created_at || '');
  $('moveEditMoveType').value = String(r.move_type || 'OUT').toUpperCase();
  $('moveEditQty').value = String(r.qty || '');
  $('moveEditNotes').value = String(r.notes || '');
  $('moveEditReason').value = '';

  $('moveEditProduct').innerHTML = (j.products || []).map(p => {
    const sel = parseInt(p.id,10) === parseInt(r.product_id,10) ? ' selected' : '';
    return `<option value="${p.id}"${sel}>${h(p.sku)} · ${h(p.name)}</option>`;
  }).join('');

  $('moveEditWarehouse').innerHTML = '<option value="0">SIN-WH / sin almacén</option>' + (j.warehouses || []).map(w => {
    const sel = parseInt(w.id,10) === parseInt(r.warehouse_id || 0,10) ? ' selected' : '';
    return `<option value="${w.id}"${sel}>${h(w.code)} · ${h(w.name)}</option>`;
  }).join('');

  $('moveEditMeta').innerHTML = `
    <div><b>Empresa:</b> ${h(r.company_name || '')} · <b>Planta:</b> ${h(r.location_name || '')}</div>
    <div><b>Origen:</b> <span class="mono">${h(r.ref_type || '')} #${h(r.ref_id ?? '')}</span> · <b>Usuario:</b> ${h(r.user_name || '')}</div>
    <div class="text-muted">No se cambia el origen del movimiento; solo los datos operativos corregibles.</div>
  `;
}

async function saveMoveEdit(ev){
  ev.preventDefault();
  const id = parseInt($('moveEditId').value || '0', 10);
  if(id <= 0) return;

  const ok = confirm('Vas a modificar un movimiento real de inventario. ¿Confirmas guardar la corrección?');
  if(!ok) return;

  const payload = {
    id,
    work_date: $('moveEditWorkDate').value,
    created_at: $('moveEditCreatedAt').value,
    move_type: $('moveEditMoveType').value,
    qty: $('moveEditQty').value,
    product_id: $('moveEditProduct').value,
    warehouse_id: $('moveEditWarehouse').value,
    notes: $('moveEditNotes').value,
    reason: $('moveEditReason').value,
    code: $('moveEditCode').value
  };

  const j = await apiPost('inventory_move_update', payload);
  if(!j.ok){
    alert(j.error || 'No se pudo guardar la corrección.');
    return;
  }

  moveEditModal.hide();
  await auditLoad();
}


/* ===== Validador producción cubicado ===== */
async function cubAuditInit(){
  if(!$('cubAudCompany')) return;

  $('cubAudCompany').innerHTML = '<option value="0">Todas las empresas</option>' + companies.map(x=>`<option value="${x.id}">${h(x.name)}</option>`).join('');
  if(companyFilterId()>0) $('cubAudCompany').value = String(companyFilterId());
  if(!$('cubAudDate').value) $('cubAudDate').value = todayIso();

  await cubAuditLoadLocationsProducts();
  await cubAuditLoad();
}

async function cubAuditLoadLocationsProducts(){
  const companyId = parseInt($('cubAudCompany')?.value || '0', 10);
  const loc = await apiGet('inventory_adjust_locations', {company_id: companyId});
  $('cubAudLocation').innerHTML = '<option value="0">Todas</option>' + (loc.ok ? loc.rows : []).map(x => `<option value="${x.id}">${h(x.company_name)} · ${h(x.name)} (${h(x.code||'')})</option>`).join('');

  if(companyId > 0){
    const pr = await apiGet('inventory_adjust_products', {company_id: companyId});
    $('cubAudProduct').innerHTML = '<option value="0">Todos</option>' + (pr.ok ? pr.rows : []).map(x => `<option value="${x.id}">${h(x.sku)} · ${h(x.name)}</option>`).join('');
  } else {
    $('cubAudProduct').innerHTML = '<option value="0">Todos</option>';
  }
}

function cubIssueHtml(issues){
  if(!issues || issues.length === 0) return '<span class="text-success">Sin diferencias detectadas</span>';
  return `<ul class="mb-0 ps-3">${issues.map(x=>`<li>${h(x)}</li>`).join('')}</ul>`;
}

async function cubAuditLoad(){
  if(!$('cubAudBody')) return;

  const payload = {
    company_id: $('cubAudCompany').value || '0',
    location_id: $('cubAudLocation').value || '0',
    product_id: $('cubAudProduct').value || '0',
    shift: $('cubAudShift').value || '0',
    date: $('cubAudDate').value || todayIso(),
    q: $('cubAudSearch').value.trim()
  };

  const j = await apiGet('cubing_audit_list', payload);
  if(!j.ok){
    alert(j.error || 'No se pudo consultar cubicado.');
    return;
  }

  const s = j.summary || {};
  $('cubAudCount').textContent = String(s.entries_count || 0);
  $('cubAudOk').textContent = String(s.ok_count || 0);
  $('cubAudReview').textContent = String(s.review_count || 0);
  $('cubAudOutput').textContent = fmt3(s.output_qty || 0).replace(/\.000$/, '');
  $('cubAudBars').textContent = fmt3(s.bars_used || 0);
  $('cubAudLeft').textContent = fmt3(s.bars_leftover || 0);

  const rows = j.rows || [];
  $('cubAudBody').innerHTML = rows.map(r => {
    const isOk = String(r.status || '') === 'OK';
    return `
      <tr class="${isOk ? '' : 'table-warning'}">
        <td><span class="badge ${isOk ? 'text-bg-success' : 'text-bg-danger'}">${h(r.status || '')}</span></td>
        <td class="mono">#${h(r.id)}</td>
        <td>
          <div class="mono small">${h(r.work_date || '')}</div>
          <div><span class="badge text-bg-secondary">T${h(r.shift || '')}</span> <span class="mono small">${h(r.entry_time || '')}</span></div>
        </td>
        <td>
          <div class="fw-semibold">${h(r.producto_producido || '')}</div>
          <div class="mono small text-muted">${h(r.producto_producido_sku || '')}</div>
        </td>
        <td>
          <span class="badge text-bg-info">${h(r.source_mode || '')}</span>
          <div class="mono small text-muted">${h(r.almacen_origen || '')} → ${h(r.almacen_destino || '')}</div>
        </td>
        <td class="text-end mono">${fmt3(r.output_qty).replace(/\.000$/, '')}</td>
        <td class="text-end mono">${fmt3(r.bars_used)}</td>
        <td class="text-end mono">${fmt3(r.bars_taken)}</td>
        <td class="text-end mono">${fmt3(r.bars_leftover)}</td>
        <td class="text-end mono">${h(r.inventory_moves_count || 0)}</td>
        <td class="small">${cubIssueHtml(r.issues || [])}</td>
      </tr>
    `;
  }).join('') || '<tr><td colspan="11" class="text-center text-muted py-4">Sin capturas de cubicado con los filtros seleccionados.</td></tr>';
}


/* ===== Reglas de almacén por producto/localidad ===== */
function whRuleSelectHtml(fieldName, warehouses, selectedId){
  selectedId = parseInt(selectedId || 0, 10);
  let html = `<select class="form-select form-select-sm" data-rule-field="${h(fieldName)}">`;
  html += `<option value="0">-- No configurado --</option>`;
  for(const w of (warehouses || [])){
    const wid = parseInt(w.id || 0, 10);
    const sel = wid === selectedId ? ' selected' : '';
    html += `<option value="${wid}"${sel}>${h(w.code || '')} · ${h(w.name || '')}</option>`;
  }
  html += `</select>`;
  return html;
}

async function openWhRulesModal(productId, companyId, sku, name){
  productId = parseInt(productId || 0, 10);
  companyId = parseInt(companyId || 0, 10);
  if(productId <= 0 || companyId <= 0){
    alert('Producto o empresa inválida para configurar almacenes.');
    return;
  }

  $('whRulesTitle').textContent = 'Almacenes por producto';
  $('whRulesSub').textContent = `${sku || ''} · ${name || ''}`;
  $('whRulesBody').innerHTML = '<div class="text-center text-muted py-4"><div class="spinner-border spinner-border-sm me-2"></div>Cargando reglas...</div>';
  whRulesModal.show();

  const j = await apiGet('product_wh_rules_get', {company_id: companyId, product_id: productId});
  if(!j.ok){
    $('whRulesBody').innerHTML = `<div class="alert alert-danger">${h(j.error || 'No se pudieron cargar las reglas de almacén.')}</div>`;
    return;
  }

  const rows = j.rows || [];
  if(rows.length === 0){
    $('whRulesBody').innerHTML = '<div class="alert alert-warning mb-0">No hay localidades activas para esta empresa.</div>';
    return;
  }

  $('whRulesBody').innerHTML = rows.map(r => {
    const rule = r.rule || {};
    const whs = r.warehouses || [];
    return `
      <div class="card rounded-4 shadow-sm mb-3" data-rule-location-id="${parseInt(r.location_id || 0, 10)}" data-rule-company-id="${companyId}" data-rule-product-id="${productId}">
        <div class="card-body">
          <div class="d-flex flex-wrap gap-2 align-items-center mb-3">
            <div>
              <div class="fw-bold"><i class="bi bi-geo-alt me-1"></i>${h(r.location_name || '')}</div>
              <div class="small text-muted mono">${h(r.location_code || '')}</div>
            </div>
            <div class="ms-auto form-check form-switch">
              <input class="form-check-input" type="checkbox" data-rule-active ${parseInt(rule.active || 1, 10) === 1 ? 'checked' : ''}>
              <label class="form-check-label small text-muted">Regla activa</label>
            </div>
          </div>

          <div class="row g-3">
            <div class="col-12 col-md-6 col-xl-4">
              <label class="form-label small fw-semibold">Producto terminado / entrada</label>
              ${whRuleSelectHtml('default_finished_warehouse_id', whs, rule.default_finished_warehouse_id)}
            </div>
            <div class="col-12 col-md-6 col-xl-4">
              <label class="form-label small fw-semibold">Salida comercial / venta</label>
              ${whRuleSelectHtml('default_sales_warehouse_id', whs, rule.default_sales_warehouse_id)}
            </div>
            <div class="col-12 col-md-6 col-xl-4">
              <label class="form-label small fw-semibold">Merma pendiente</label>
              ${whRuleSelectHtml('merma_warehouse_id', whs, rule.merma_warehouse_id)}
            </div>
            <div class="col-12 col-md-6 col-xl-4">
              <label class="form-label small fw-semibold">Aprovechamiento merma</label>
              ${whRuleSelectHtml('aprovechamiento_warehouse_id', whs, rule.aprovechamiento_warehouse_id)}
            </div>
            <div class="col-12 col-md-6 col-xl-4">
              <label class="form-label small fw-semibold">Cubicado / proceso</label>
              ${whRuleSelectHtml('cubicado_warehouse_id', whs, rule.cubicado_warehouse_id)}
            </div>
            <div class="col-12 col-xl-4">
              <label class="form-label small fw-semibold">Notas</label>
              <input class="form-control form-control-sm" data-rule-notes value="${h(rule.notes || '')}" placeholder="Regla operativa / comentario">
            </div>
          </div>

          <div class="d-flex justify-content-end mt-3">
            <button type="button" class="btn btn-primary btn-sm btn-pill" data-save-whrule="1">
              <i class="bi bi-save me-1"></i>Guardar regla de esta localidad
            </button>
          </div>
        </div>
      </div>
    `;
  }).join('');
}

async function saveWhRule(card){
  const companyId = parseInt(card.dataset.ruleCompanyId || '0', 10);
  const locationId = parseInt(card.dataset.ruleLocationId || '0', 10);
  const productId = parseInt(card.dataset.ruleProductId || '0', 10);
  if(companyId <= 0 || locationId <= 0 || productId <= 0){
    alert('Datos incompletos para guardar la regla.');
    return;
  }

  const payload = {
    company_id: companyId,
    location_id: locationId,
    product_id: productId,
    active: card.querySelector('[data-rule-active]')?.checked ? 1 : 0,
    notes: card.querySelector('[data-rule-notes]')?.value || ''
  };

  card.querySelectorAll('[data-rule-field]').forEach(sel => {
    payload[sel.dataset.ruleField] = parseInt(sel.value || '0', 10);
  });

  const btn = card.querySelector('[data-save-whrule]');
  const oldHtml = btn ? btn.innerHTML : '';
  if(btn){ btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Guardando...'; }

  const j = await apiPost('product_wh_rules_save', payload);

  if(btn){ btn.disabled = false; btn.innerHTML = oldHtml; }

  if(!j.ok){
    alert(j.error || 'No se pudo guardar la regla.');
    return;
  }

  const toast = document.createElement('div');
  toast.className = 'alert alert-success py-2 mt-3 mb-0';
  toast.innerHTML = '<i class="bi bi-check-circle me-1"></i>Regla guardada correctamente.';
  card.querySelector('.card-body')?.appendChild(toast);
  setTimeout(() => toast.remove(), 2200);
}

/* ===== Events ===== */
document.addEventListener('click', async (e)=>{
  // Hub click
  const hub = e.target.closest('.cardHub');
  if (hub) { showPanel(hub.dataset.panel); if(hub.dataset.panel==='plcprod') { await loadPlcRefs(); await loadProductionLines(); await loadPlcTags(); } if(hub.dataset.panel==='invadj') { await invLoadCompanies(); } if(hub.dataset.panel==='audit') { await auditInit(); } if(hub.dataset.panel==='cubaudit') { await cubAuditInit(); cubAuditModal.show(); } if(hub.dataset.panel==='data') { await dataInit(); } return; }

  // configuración logística: editar un embalaje existente
  const packEditBtn = e.target.closest('[data-pack-edit]');
  if (packEditBtn) {
    const packagingId = parseInt(packEditBtn.dataset.packagingId || '0', 10);
    const option = packagingRows.find(r => parseInt(r.packaging_id || '0', 10) === packagingId);
    if (option) openPackagingModal(option, option);
    return;
  }

  // configuración logística: agregar un embalaje adicional al mismo artículo
  const packAddBtn = e.target.closest('[data-pack-additional]');
  if (packAddBtn) {
    const productId = parseInt(packAddBtn.dataset.productId || '0', 10);
    const rows = packagingRows.filter(r => parseInt(r.product_id || '0', 10) === productId);
    const product = rows[0];
    if (product) {
      product.has_options = rows.some(r => parseInt(r.packaging_id || '0', 10) > 0);
      openPackagingModal(product, null);
    }
    return;
  }

  // product warehouse rules
  const whBtn = e.target.closest('[data-whrules]');
  if (whBtn) {
    await openWhRulesModal(parseInt(whBtn.dataset.id||'0',10), parseInt(whBtn.dataset.companyId||'0',10), whBtn.dataset.sku||'', whBtn.dataset.name||'');
    return;
  }

  const saveRuleBtn = e.target.closest('[data-save-whrule]');
  if (saveRuleBtn) {
    const card = saveRuleBtn.closest('[data-rule-location-id]');
    if(card) await saveWhRule(card);
    return;
  }

  const presetBtn = e.target.closest('[data-preset-key]');
  if (presetBtn) {
    dataUsePreset(presetBtn.dataset.presetKey || '');
    return;
  }

  // controlled inventory movement correction
  const moveBtn = e.target.closest('[data-edit-move]');
  if (moveBtn) {
    await openMoveEditModal(parseInt(moveBtn.dataset.editMove || '0', 10));
    return;
  }

  // edit buttons
  const btn = e.target.closest('[data-edit]');
  if(!btn) return;

  const kind = btn.dataset.edit;
  const id = parseInt(btn.dataset.id||'0',10);

  if (kind === 'user') {
    await openUserModal(id);
    return;
  }

  if (kind === 'wh') {
    const row = await findRow(kind, id);
    await openWarehouseModal(row);
    return;
  }

  if (kind === 'route') {
    const row = await findRow(kind, id);
    await openRouteModal(row);
    return;
  }

  if (kind === 'pline') {
    const row = productionLines.find(x=>parseInt(x.id,10)===id) || null;
    openProductionLineModal(row);
    return;
  }

  if (kind === 'ptag') {
    const row = plcTags.find(x=>parseInt(x.id,10)===id) || null;
    await openPlcTagModal(row);
    return;
  }

  const row = await findRow(kind, id);
  openModal(kind, row);
});

/* ===== Análisis de datos ===== */
let dataTables = [];
let dataPresets = [];

function dataSelectedTables(){
  return Array.from(document.querySelectorAll('[data-data-table]:checked')).map(x=>x.value);
}

function dataRenderTables(filter=''){
  const box = $('dataTablesBox');
  if(!box) return;
  const f = String(filter||'').toLowerCase();
  box.innerHTML = '';
  for(const t of dataTables){
    if(f && !String(t.table_name).toLowerCase().includes(f)) continue;
    box.insertAdjacentHTML('beforeend', `
      <label class="form-check d-flex gap-2 align-items-start border-bottom pb-1 mb-1">
        <input class="form-check-input mt-1" type="checkbox" data-data-table value="${h(t.table_name)}">
        <span class="flex-grow-1">
          <span class="mono fw-semibold">${h(t.table_name)}</span><br>
          <span class="text-muted small">Filas aprox: ${h(t.table_rows ?? 0)} · ${h(t.engine||'')}</span>
        </span>
      </label>
    `);
  }
}

async function dataInit(){
  if(dataTables.length === 0) await dataLoadTables();
  if(dataPresets.length === 0) await dataLoadPresets();
}

async function dataLoadTables(){
  const box = $('dataTablesBox');
  if(box) box.innerHTML = '<div class="text-muted small p-2">Cargando tablas...</div>';
  const j = await apiGet('data_tables');
  if(!j.ok){ if(box) box.innerHTML = `<div class="alert alert-danger py-2">${h(j.error||'No se pudieron cargar tablas')}</div>`; return; }
  dataTables = j.rows || [];
  dataRenderTables(($('dataTableSearch')?.value||''));
}

async function dataLoadPresets(){
  const box = $('dataPresetBox');
  if(box) box.innerHTML = '<div class="text-muted small p-2">Cargando consultas...</div>';
  const j = await apiGet('data_preset_queries');
  if(!j.ok){ if(box) box.innerHTML = `<div class="alert alert-danger py-2">${h(j.error||'No se pudieron cargar consultas')}</div>`; return; }
  dataPresets = j.rows || [];
  if(!box) return;
  box.innerHTML = '';
  for(const q of dataPresets){
    box.insertAdjacentHTML('beforeend', `
      <button type="button" class="list-group-item list-group-item-action" data-preset-key="${h(q.key)}">
        <div class="fw-semibold">${h(q.title)}</div>
        <div class="text-muted mono small">${h(q.key)}</div>
      </button>
    `);
  }
}

function dataRenderRelationships(rows){
  const tb = $('dataRelBody');
  if(!tb) return;
  tb.innerHTML = '';
  if(!rows || rows.length === 0){
    tb.innerHTML = '<tr><td colspan="2" class="text-muted">Sin relación detectada.</td></tr>';
    return;
  }
  for(const r of rows){
    tb.insertAdjacentHTML('beforeend', `
      <tr>
        <td>
          <div class="mono small">${h(r.left_table)}.${h(r.left_column)}</div>
          <div class="small text-muted">→ ${h(r.right_table)}.${h(r.right_column)}</div>
          <div class="small">${h(r.reason||'')}</div>
        </td>
        <td><span class="badge ${r.confidence==='alta'?'text-bg-success':'text-bg-warning'}">${h(r.confidence||'')}</span></td>
      </tr>
    `);
  }
}

async function dataRefreshRelationships(){
  const tables = dataSelectedTables();
  if(tables.length === 0){ dataRenderRelationships([]); return; }
  const j = await apiGet('data_relationships', {tables: tables.join(',')});
  dataRenderRelationships(j.ok ? (j.rows||[]) : []);
}

async function dataGenerateQuery(){
  const tables = dataSelectedTables();
  if(tables.length === 0){ alert('Selecciona al menos una tabla.'); return; }
  const j = await apiPost('data_generate_query', {tables});
  if(!j.ok){ alert(j.error || 'No se pudo generar la consulta.'); return; }
  $('dataSql').value = j.sql || '';
  dataRenderRelationships(j.relationships || []);
  $('dataQueryInfo').textContent = 'Consulta generada. Puedes editarla antes de ejecutar.';
}

async function dataDescribeSelected(){
  const tables = dataSelectedTables();
  if(tables.length !== 1){ alert('Selecciona una sola tabla para ver sus columnas.'); return; }
  const j = await apiGet('data_columns', {table: tables[0]});
  if(!j.ok){ alert(j.error || 'No se pudieron cargar columnas.'); return; }
  const lines = (j.rows||[]).map(c => `-- ${c.column_name} ${c.column_type} ${c.is_nullable==='NO'?'NOT NULL':'NULL'} ${c.column_key?('KEY '+c.column_key):''}`);
  $('dataSql').value = `${lines.join('\n')}\n\nSELECT *\nFROM \`${tables[0]}\`\nLIMIT 100`;
  $('dataQueryInfo').textContent = 'Columnas cargadas como comentarios.';
}

function dataRenderResults(columns, rows){
  const th = $('dataResultHead');
  const tb = $('dataResultBody');
  if(!th || !tb) return;
  th.innerHTML = '';
  tb.innerHTML = '';
  if(!columns || columns.length === 0){
    th.innerHTML = '<tr><th>Sin columnas</th></tr>';
    return;
  }
  th.innerHTML = `<tr>${columns.map(c=>`<th class="text-nowrap">${h(c)}</th>`).join('')}</tr>`;
  for(const r of rows || []){
    tb.insertAdjacentHTML('beforeend', `<tr>${columns.map(c=>`<td class="small">${h(r[c])}</td>`).join('')}</tr>`);
  }
}

async function dataRunQuery(){
  const sql = ($('dataSql')?.value || '').trim();
  if(sql === ''){ alert('Escribe o genera una consulta SELECT.'); return; }
  const btn = $('btnDataRun');
  const old = btn ? btn.innerHTML : '';
  if(btn){ btn.disabled = true; btn.innerHTML = '<span class="spinner-border spinner-border-sm me-1"></span>Ejecutando...'; }
  const j = await apiPost('data_query_run', {sql});
  if(btn){ btn.disabled = false; btn.innerHTML = old; }
  if(!j.ok){
    $('dataQueryInfo').innerHTML = `<span class="text-danger">${h(j.error||'Error ejecutando consulta')}</span>`;
    return;
  }
  $('dataSql').value = j.sql || sql;
  $('dataResultInfo').textContent = `${j.count || 0} registros devueltos`;
  $('dataQueryInfo').innerHTML = '<span class="text-success">Consulta ejecutada correctamente.</span>';
  dataRenderResults(j.columns || [], j.rows || []);
}

function dataUsePreset(key){
  const q = dataPresets.find(x => x.key === key);
  if(!q) return;
  $('dataSql').value = q.sql || '';
  $('dataQueryInfo').textContent = 'Consulta relacionada cargada. Puedes ejecutarla o editarla.';
}


document.addEventListener('DOMContentLoaded', async ()=>{
  modal = bootstrap.Modal.getOrCreateInstance(document.getElementById('editModal'));
  whRulesModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('whRulesModal'));
  moveEditModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('moveEditModal'));
  cubAuditModal = bootstrap.Modal.getOrCreateInstance(document.getElementById('cubAuditModal'));

  await loadCompanies();
  await loadRoles();
  await reloadAll();

  $('btnReload').addEventListener('click', reloadAll);

  $('addLoc').addEventListener('click', ()=>{ mode='loc'; openModal('loc', null); });
  if($('addWh')) $('addWh').addEventListener('click', ()=>openWarehouseModal(null));
  $('addProd').addEventListener('click', ()=>{ mode='prod'; openModal('prod', null); });
  if($('addRoute')) $('addRoute').addEventListener('click', ()=>openRouteModal(null));
  if($('addProdLine')) $('addProdLine').addEventListener('click', ()=>openProductionLineModal(null));
  if($('addPlcTag')) $('addPlcTag').addEventListener('click', ()=>openPlcTagModal(null));
  if($('btnPlcReload')) $('btnPlcReload').addEventListener('click', async ()=>{ await loadPlcRefs(); await loadProductionLines(); await loadPlcTags(); });
  $('addChan').addEventListener('click', ()=>{ mode='chan'; openModal('chan', null); });

  $('qcType').addEventListener('change', loadQualityConcepts);
  $('addQc').addEventListener('click', ()=>{ mode='qc'; openModal('qc', null); });

  $('addUser').addEventListener('click', ()=>openUserModal(0));
  $('btnUSearch').addEventListener('click', ()=>loadUsers(($('uSearch').value||'').trim()));
  $('uSearch').addEventListener('keydown', (ev)=>{
    if (ev.key === 'Enter') { ev.preventDefault(); loadUsers(($('uSearch').value||'').trim()); }
  });

  $('editForm').addEventListener('submit', (e)=>{e.preventDefault(); saveModal();});
  if($('moveEditForm')) $('moveEditForm').addEventListener('submit', saveMoveEdit);


  if($('btnInvUnlock')) $('btnInvUnlock').addEventListener('click', invUnlock);
  if($('btnInvReload')) $('btnInvReload').addEventListener('click', invLoadSnapshot);
  if($('btnInvSave')) $('btnInvSave').addEventListener('click', invSaveAdjust);
  if($('btnInvCutSave')) $('btnInvCutSave').addEventListener('click', invSaveCut);
  if($('invCompany')) $('invCompany').addEventListener('change', invLoadLocations);
  if($('invLocation')) $('invLocation').addEventListener('change', invLoadWarehousesProducts);

  if($('btnAuditReload')) $('btnAuditReload').addEventListener('click', ()=>{ auditPage=1; auditLoad(); });
  if($('audCompany')) $('audCompany').addEventListener('change', async ()=>{ auditPage=1; await auditLoadLocationsProductsWarehouses(); await auditLoad(); });
  if($('audLocation')) $('audLocation').addEventListener('change', async ()=>{ auditPage=1; await auditLoadWarehouses(); await auditLoad(); });
  if($('audSearch')) $('audSearch').addEventListener('keydown', (ev)=>{ if(ev.key==='Enter'){ ev.preventDefault(); auditPage=1; auditLoad(); } });
  ['audDateFrom','audDateTo','audMoveType','audProduct','audWarehouse','audRefType','audPerPage'].forEach(id=>{
    if($(id)) $(id).addEventListener('change', ()=>{ auditPage=1; auditLoad(); });
  });
  if($('audFirstPage')) $('audFirstPage').addEventListener('click', ()=>{ if(auditPage>1){ auditPage=1; auditLoad(); } });
  if($('audPrevPage')) $('audPrevPage').addEventListener('click', ()=>{ if(auditPage>1){ auditPage--; auditLoad(); } });
  if($('audNextPage')) $('audNextPage').addEventListener('click', ()=>{ if(auditPage<auditTotalPages){ auditPage++; auditLoad(); } });
  if($('audLastPage')) $('audLastPage').addEventListener('click', ()=>{ if(auditPage<auditTotalPages){ auditPage=auditTotalPages; auditLoad(); } });



  if($('btnCubAuditOpen')) $('btnCubAuditOpen').addEventListener('click', async ()=>{ await cubAuditInit(); cubAuditModal.show(); });
  if($('btnCubAuditRun')) $('btnCubAuditRun').addEventListener('click', cubAuditLoad);
  if($('cubAudCompany')) $('cubAudCompany').addEventListener('change', async ()=>{ await cubAuditLoadLocationsProducts(); await cubAuditLoad(); });
  ['cubAudLocation','cubAudDate','cubAudShift','cubAudProduct'].forEach(id=>{
    if($(id)) $(id).addEventListener('change', cubAuditLoad);
  });
  if($('cubAudSearch')) $('cubAudSearch').addEventListener('keydown', (ev)=>{ if(ev.key==='Enter'){ ev.preventDefault(); cubAuditLoad(); } });

  if($('btnDataReload')) $('btnDataReload').addEventListener('click', dataLoadTables);
  if($('btnDataGenerate')) $('btnDataGenerate').addEventListener('click', dataGenerateQuery);
  if($('btnDataRun')) $('btnDataRun').addEventListener('click', dataRunQuery);
  if($('btnDataDescribe')) $('btnDataDescribe').addEventListener('click', dataDescribeSelected);
  if($('dataTableSearch')) $('dataTableSearch').addEventListener('input', ()=>dataRenderTables($('dataTableSearch').value||''));
  document.addEventListener('change', (ev)=>{ if(ev.target && ev.target.matches('[data-data-table]')) dataRefreshRelationships(); });

  showPanel('loc');
});
</script>
</body>
</html>