import { Icon } from "@iconify/react/dist/iconify.js";
import {
  Box,
  Button,
  Chip,
  CircularProgress,
  Dialog,
  DialogActions,
  DialogContent,
  DialogTitle,
  Divider,
  FormControl,
  FormControlLabel,
  Grid,
  IconButton,
  InputAdornment,
  InputLabel,
  ListItemIcon,
  ListItemText,
  ListSubheader,
  Menu,
  MenuItem,
  Paper,
  Select,
  Switch,
  TextField,
  Tooltip,
  Typography,
} from "@mui/material";
import { useInfiniteQuery, useQuery } from "@tanstack/react-query";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { toast } from "sonner";
import styled from "styled-components";

import { UserAuth } from "../../context/AuthContent";
import { usePermission } from "../../hooks/usePermission";
import { useEmpresa } from "../../context/EmpresaContext";
import { useAuthStore } from "../../store/AuthStore";
import { useEmpresaStore } from "../../store/EmpresaStore";
import { usePosConfigQuery, useUpdatePosConfigMutation } from "../../tanstack/PosConfigStack";

import { getCategorias } from "../../api/categorias.service";
import { createCliente, getClientes, getCreditInfo, searchClientes, verificarCredito } from "../../api/clientes.service";
import { getDepositoPrincipal } from "../../api/depositos.service";
import { anularFactura, createFactura, getFacturas } from "../../api/facturas.service";
import { generateKude } from "../../api/kude.service";
import { getPrecioProducto } from "../../api/listas-precios.service";
import { getEmpresaMonedas, getUltimaCotizacion } from "../../api/monedas.service";
import { getNumeracionesBySucursal } from "../../api/numeraciones.service";
import { evaluarOfertas } from "../../api/ofertas.service";
import { getCondicionesPago, getMediosPagoActivos } from "../../api/pagos.service";
import { buscarProductos, getProductos } from "../../api/productos.service";
import { getReferenciales } from "../../api/referenciales.service";
import { getSucursalConfig, getSucursales } from "../../api/sucursales.service";
import { abrirCaja, cerrarCaja, getCajasDisponibles, getSesionActiva } from "../../api/tesoreria.service";
import { useOfflinePOS } from "../../hooks/useOfflinePOS";
import { useDnitAutofillFlat } from "../../hooks/useDnitAutofillFlat";
import { cacheCajaSession, cachePosConfig, clearCachedCajaSession, getCachedCajaSession, getCachedPosConfig, reservarNumeroLocal } from "../../offline/db";
import { connectQzTray, getPrinters, isQzConnected, printTicketWithFallback } from "../../services/qzTrayService";
import { generarCdc, generarEnlaceQr } from "../../utils/cdcGenerator";
import { FormatearNumeroDinero } from "../../utils/Conversiones";
import { calcularSubtotales, construirItemDetalle, resolverAfectacionIva } from "../../utils/invoiceCalculations";
import { generateOfflineTicket } from "../../utils/offlineTicketGenerator";
import { CerrarCajaDialog, parseMonto } from "../organismos/POSDesign/CajaDesign/CerrarCajaDialog";
import { ModalPinSupervisor } from "../organismos/POSDesign/CajaDesign/ModalPinSupervisor";
import { SyncStatusBar } from "../ui/SyncStatusBar";
import { aFechaISO, fmtFechaCalendario, hoyISO } from "../../utils/fecha";
import { MonedaInput } from "../common/MonedaInput";
import { cantidadDesdeMonto, decimalesCantidad, montosRapidos, permiteMontoEditable } from "../../utils/montoEditable";
import { useMonedaDecimals } from "../../hooks/useMonedaDecimals";
import { useRubroStore } from "../../store/RubroStore";
import { categoriaEnRubro, rubroDelPOS, sucursalIdDeLaCaja } from "../../utils/rubros";

const DEFAULT_CONFIG = {
  tipo_pos: "retail",
  show_product_image: true,
  show_product_stock: true,
  permitir_venta_sin_stock: true,
  card_size: "medium",
  retail_grid_columns: 0,
  barcode_auto_add: true,
  // Ventas a crédito: opt-in por caja desde Configuración POS.
  permitir_venta_credito: false,
};

// Umbral en ms para distinguir input de lector de códigos vs escritura manual.
// Un lector envía todos los caracteres en ~10-50ms, la escritura manual es >100ms.
const BARCODE_SCAN_THRESHOLD_MS = 80;

// Utilidad para calcular DV del RUC paraguayo
const calcularDV = (ruc) => {
  if (!ruc) return 0;
  const baseMax = 11;
  let total = 0;
  let k = 2;
  for (let i = ruc.length - 1; i >= 0; i--) {
    total += parseInt(ruc[i], 10) * k;
    k = k < baseMax ? k + 1 : 2;
  }
  const resto = total % 11;
  return resto > 1 ? 11 - resto : 0;
};

// Tamaños de tiles según configuración
const TILE_SIZES = {
  small: { size: 100, fontSize: 11 },
  medium: { size: 120, fontSize: 13 },
  large: { size: 150, fontSize: 15 },
};

// Las tarjetas usan un único color neutro: el color por producto/categoría
// competía visualmente con lo que de verdad importa en una venta (el carrito,
// el total y el ítem recién agregado). Se mantiene el color de la categoría
// solo como una línea de acento fina cuando la categoría lo define.
const getProductAccent = (producto) => producto.categoria?.color || null;

export function POSRetailTemplate() {
  const { user } = UserAuth();
  // Mismo permiso que gatea la anulación en la lista de Facturación
  // (VEN_FAC_FACTURA_ANULAR, el que exige el backend en PATCH /facturas/:id/anular).
  const { can: canVentas } = usePermission("VENTAS");
  const puedeAnularFactura = canVentas("VEN_FAC_FACTURA_ANULAR");
  const { cerrarSesion } = useAuthStore();
  const { dataempresa } = useEmpresaStore();
  const { empresa: empresaCtx } = useEmpresa();
  const barcodeInputRef = useRef(null);
  const loadMoreRef = useRef(null);
  // Último producto agregado — confirmación visible fija bajo el buscador.
  // Solo se guarda QUÉ producto fue el último: el nombre, el precio, la oferta y
  // la cantidad se leen del carrito en cada render. Así el banner muestra "×3"
  // al escanear tres veces el mismo producto, y desaparece solo cuando el ítem
  // se borra o se cierra la venta, en vez de quedar afirmando algo que ya no está.
  const [ultimoAgregadoId, setUltimoAgregadoId] = useState(null);
  const {
    isOnline,
    searchProductosFallback,
    searchClientesFallback,
    createFacturaFallback,
    getProductosFallback,
    getCategoriasFallback,
    getMediosPagoFallback,
    getCondicionesPagoFallback,
    getReferencialesFallback,
    getNumeracionesFallback,
  } = useOfflinePOS();

  // Estado del menú admin
  const [adminMenuAnchor, setAdminMenuAnchor] = useState(null);
  const adminMenuOpen = Boolean(adminMenuAnchor);

  // Pantalla completa vs. con menú lateral. Son dos rutas que montan este mismo
  // componente: "/pos" va sin <Layout> (sin sidebar) y "/pos-admin" con él.
  // Antes el acceso al panel hacía window.open(..., "_blank") y dejaba una
  // pestaña nueva por cada visita; ahora se navega en la misma pestaña.
  const navigate = useNavigate();
  const location = useLocation();
  const enPantallaCompleta = location.pathname === "/pos";
  const alternarPantallaCompleta = useCallback(() => {
    navigate(enPantallaCompleta ? "/pos-admin" : "/pos");
  }, [navigate, enPantallaCompleta]);

  // Detección de lector de códigos de barras
  const barcodeTimerRef = useRef(null);
  const keystrokesRef = useRef([]);
  const isBarcodeInputRef = useRef(false);

  // Estado principal
  const [sucursalActiva, setSucursalActiva] = useState(null);
  const [items, setItems] = useState([]);
  const [categoriaActiva, setCategoriaActiva] = useState(null);
  const [searchQuery, setSearchQuery] = useState("");
  const [searchResults, setSearchResults] = useState([]);
  const [isSearching, setIsSearching] = useState(false);

  // Estado de pago
  const [showPaymentDialog, setShowPaymentDialog] = useState(false);
  const [montoPagado, setMontoPagado] = useState("");
  const [metodoPagoSeleccionado, setMetodoPagoSeleccionado] = useState(null);
  const [pagosAgregados, setPagosAgregados] = useState([]);
  const [isSaving, setIsSaving] = useState(false);

  // Fase 2: Multi-moneda por pago
  const [monedaPagoSeleccionada, setMonedaPagoSeleccionada] = useState(null);
  const [cotizacionPago, setCotizacionPago] = useState(null);

  // Condición de pago de la venta (contado por defecto; crédito simple a plazo)
  const [condicionPagoActiva, setCondicionPagoActiva] = useState(null);

  // Fase 2: Control de crédito
  const [creditoInfo, setCreditoInfo] = useState(null);
  const [showCreditoBlockDialog, setShowCreditoBlockDialog] = useState(false);
  const [creditoOverridePin, setCreditoOverridePin] = useState("");
  const [creditoOverrideCallback, setCreditoOverrideCallback] = useState(null);

  // Estado de cliente
  const [clienteSeleccionado, setClienteSeleccionado] = useState(null);
  const [showClienteDialog, setShowClienteDialog] = useState(false);
  const [clienteSearch, setClienteSearch] = useState("");
  const [clientesResults, setClientesResults] = useState([]);

  // Estado para crear nuevo cliente
  const [showNuevoClienteDialog, setShowNuevoClienteDialog] = useState(false);
  // Cliente ya existente con el mismo documento, detectado antes de guardar.
  const [clienteDuplicado, setClienteDuplicado] = useState(null);
  const [nuevoClienteData, setNuevoClienteData] = useState({
    ruc: "",
    dv: 0,
    razon_social: "",
    nro_documento: "",
    tipo_documento_id: "",
    direccion: "",
    celular: "",
    email: "",
    naturaleza_id: "",
    tipo_contribuyente_id: "",
    tipo_operacion_id: "",
  });
  const [isCreatingCliente, setIsCreatingCliente] = useState(false);

  // Estado para editar item (precio/descuento/cantidad)
  const [editItemDialog, setEditItemDialog] = useState(false);
  // Modo estación: producto de combustible esperando que se cargue el monto
  // antes de entrar al carrito (ver addToCart).
  const [pendienteMonto, setPendienteMonto] = useState(null);
  const [montoNuevoItem, setMontoNuevoItem] = useState(0);
  const [editingItemIndex, setEditingItemIndex] = useState(null);
  const [editItemData, setEditItemData] = useState({
    precio: "",
    descuento: "",
    cantidad: "",
    tipoDescuento: "porcentaje", // "porcentaje" o "monto"
  });

  // Estado para descuento global
  const [descuentoGlobal, setDescuentoGlobal] = useState(0);
  const [tipoDescuentoGlobal, setTipoDescuentoGlobal] = useState("ninguno"); // "ninguno", "porcentaje", "monto"

  // Estado para moneda
  const [monedaActiva, setMonedaActiva] = useState(null);
  const [monedaInicializada, setMonedaInicializada] = useState(false);

  // Estado de cotización (como en POS Admin)
  const [cotizacionActiva, setCotizacionActiva] = useState(null);
  const [showCotizacionAlert, setShowCotizacionAlert] = useState(false);
  const [cotizacionPendiente, setCotizacionPendiente] = useState(null);
  const [cotizacionRequerida, setCotizacionRequerida] = useState(false);

  // Estado para modal de configuración
  const [showConfigDialog, setShowConfigDialog] = useState(false);
  const [qzStatus, setQzStatus] = useState("disconnected");
  const [availablePrinters, setAvailablePrinters] = useState([]);
  const [loadingPrinters, setLoadingPrinters] = useState(false);
  const [configForm, setConfigForm] = useState({
    printer_name: "",
    printer_paper_width: 80,
    auto_print_ticket: false,
  });

  // Estado para confirmación de impresión de ticket
  const [ticketDialog, setTicketDialog] = useState({
    open: false,
    pdfUrl: null,
    facturaId: null,
  });

  const finalizarTicket = useCallback(() => {
    setTicketDialog((prev) => {
      if (prev.pdfUrl) URL.revokeObjectURL(prev.pdfUrl);
      return { open: false, pdfUrl: null, facturaId: null };
    });
    setTimeout(() => barcodeInputRef.current?.focus(), 100);
  }, []);

  // Estado para diálogo de reintento de impresión
  const [printRetryDialog, setPrintRetryDialog] = useState({
    open: false,
    retryFn: null,
    error: "",
  });
  // Estado para diálogo de transacciones recientes
  const [showTransaccionesDialog, setShowTransaccionesDialog] = useState(false);
  const [transacciones, setTransacciones] = useState([]);
  const [transaccionesLoading, setTransaccionesLoading] = useState(false);
  const [transaccionesPage, setTransaccionesPage] = useState(1);
  const [transaccionesTotalPages, setTransaccionesTotalPages] = useState(1);
  const [reprintingId, setReprintingId] = useState(null);

  // Estado para anulación de factura
  const [anulacionDialog, setAnulacionDialog] = useState({
    open: false,
    factura: null,
  });
  const [anulacionMotivo, setAnulacionMotivo] = useState("");
  const [anulacionLoading, setAnulacionLoading] = useState(false);

  const finalizarBtnRef = useRef(null);
  const montoInputRef = useRef(null);

  // Cache de configuración POS para offline
  const [cachedPosConfigData, setCachedPosConfigData] = useState(null);

  // Estado para manejo de caja
  const [sesionCaja, setSesionCaja] = useState(null);
  // La grilla espera a saber cuál es la sesión (y por lo tanto el rubro de su
  // caja) antes de pedir productos. Si no, la primera carga salía con el rubro
  // del header —"Todos" para un usuario de varias sucursales— y mostraba un
  // instante el catálogo entero antes de corregirse.
  const [sesionResuelta, setSesionResuelta] = useState(false);
  const [showAbrirCajaDialog, setShowAbrirCajaDialog] = useState(false);
  const [showCerrarCajaDialog, setShowCerrarCajaDialog] = useState(false);
  const [montoApertura, setMontoApertura] = useState("");
  const [montoCierre, setMontoCierre] = useState("");
  const [observacionesCierre, setObservacionesCierre] = useState("");
  const [cajasDisponibles, setCajasDisponibles] = useState([]);
  const [cajaSeleccionada, setCajaSeleccionada] = useState(null);
  const [isLoadingCaja, setIsLoadingCaja] = useState(false);

  // Fase 1: estado de autorización supervisor
  const [showPinModal, setShowPinModal] = useState(false);
  const [pinOperacion, setPinOperacion] = useState(null);
  const [tokenAutorizacion, setTokenAutorizacion] = useState(null);
  const [pendingArqueoDetalle, setPendingArqueoDetalle] = useState(null);

  // Queries
  const { data: sucursalesData } = useQuery({
    queryKey: ["sucursales"],
    queryFn: () => getSucursales(),
  });

  // Rubro con el que el POS filtra su catálogo: el de la sucursal de la caja
  // abierta (ver utils/rubros.js). Sin esto el POS de un taller mostraba y
  // encontraba también los productos de la regalería.
  const rubroElegido = useRubroStore((state) => state.rubroId);
  const rubroPOS = rubroDelPOS(sesionCaja, rubroElegido);

  const { data: categoriasData } = useQuery({
    queryKey: ["categorias"],
    queryFn: () => getCategoriasFallback(getCategorias),
    retry: false,
  });

  // Política de stock de la sucursal ('estricto' | 'advertencia' | 'libre').
  // Es la MISMA fuente que valida el backend al facturar; antes el POS miraba
  // config.permitir_venta_sin_stock (de la caja), que por defecto viene en true
  // y hacía que "estricto" nunca bloqueara nada del lado del cliente.
  const { data: sucursalConfigData } = useQuery({
    queryKey: ["sucursal-config", sucursalActiva?.id],
    queryFn: () => getSucursalConfig(sucursalActiva?.id),
    enabled: !!sucursalActiva?.id,
    retry: false,
  });
  const politicaStock = sucursalConfigData?.data?.config?.politica_stock || "advertencia";

  // Depósito principal de la sucursal — declarado antes de las queries de
  // productos porque el stock del grid debe salir del mismo depósito que usa
  // la búsqueda (handleSearch más abajo), si no el número que se ve en la
  // grilla no coincide con lo que de verdad hay disponible para vender.
  const { data: depositoPrincipal } = useQuery({
    queryKey: ["deposito-principal", sucursalActiva?.id],
    queryFn: () => getDepositoPrincipal(sucursalActiva?.id),
    enabled: !!sucursalActiva?.id,
  });

  // Productos del grid (todos o filtrados por categoría), en tandas de 50 vía
  // scroll infinito — antes se pedía un único lote fijo de 50 sin importar
  // cuántos productos tuviera el catálogo ni cuánto espacio quedara libre en
  // pantalla; ahora se pide la siguiente página al acercarse al final del scroll.
  const {
    data: productosPages,
    // `isPending` = todavía no hay datos: esperando la sesión o la primera carga.
    isPending: gridPendiente,
    fetchNextPage,
    hasNextPage,
    isFetchingNextPage,
  } = useInfiniteQuery({
    queryKey: ["productos-pos-grid", categoriaActiva?.id, depositoPrincipal?.id, rubroPOS],
    queryFn: ({ pageParam }) => getProductosFallback(getProductos, pageParam, 50, depositoPrincipal?.id, categoriaActiva?.id ? [categoriaActiva.id] : null, rubroPOS),
    enabled: sesionResuelta,
    initialPageParam: 1,
    getNextPageParam: (lastPage) => {
      // El fallback offline devuelve { data: items } sin page/lastPage: no hay
      // paginación real fuera de línea, así que no se pide una página siguiente.
      if (lastPage?.page == null || lastPage?.lastPage == null) return undefined;
      return lastPage.page < lastPage.lastPage ? lastPage.page + 1 : undefined;
    },
    retry: false,
  });

  // Dispara la siguiente tanda cuando el sentinel del final del grid entra en pantalla.
  useEffect(() => {
    const el = loadMoreRef.current;
    if (!el || !hasNextPage) return;
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0]?.isIntersecting && !isFetchingNextPage) fetchNextPage();
      },
      { rootMargin: "200px" },
    );
    observer.observe(el);
    return () => observer.disconnect();
  }, [hasNextPage, isFetchingNextPage, fetchNextPage]);

  const { data: monedasData } = useQuery({
    queryKey: ["empresaMonedas"],
    queryFn: getEmpresaMonedas,
  });

  const { data: condicionesPagoData } = useQuery({
    queryKey: ["condicionesPago"],
    queryFn: () => getCondicionesPagoFallback(getCondicionesPago),
    retry: false,
  });

  const { data: mediosPagoData } = useQuery({
    queryKey: ["mediosPagoActivos"],
    queryFn: () => getMediosPagoFallback(getMediosPagoActivos),
    retry: false,
  });

  const { data: referencialesData } = useQuery({
    queryKey: ["referenciales"],
    queryFn: () => getReferencialesFallback(getReferenciales),
    retry: false,
  });

  // Función para obtener valores por defecto del formulario de cliente
  const getDefaultClienteData = useCallback(() => {
    const naturalezas = referencialesData?.naturaleza_receptor || [];
    const tiposOp = referencialesData?.tipo_operacion || [];

    const tiposDoc = referencialesData?.tipo_documento_identidad || [];
    // No Contribuyente por defecto (código 2): el usuario tipea el documento y la
    // consulta a la DNIT decide la naturaleza (misma lógica que el alta de clientes).
    const noContrib = naturalezas.find((n) => Number(n.codigo) === 2);
    const cedula = tiposDoc.find((t) => Number(t.codigo) === 1);
    // B2C por defecto (código 2) — coherente con No Contribuyente.
    const b2c = tiposOp.find((t) => Number(t.codigo) === 2);

    return {
      ruc: "",
      dv: 0,
      razon_social: "",
      nro_documento: "",
      tipo_documento_id: cedula?.id || "",
      direccion: "",
      celular: "",
      email: "",
      naturaleza_id: noContrib?.id || "",
      tipo_contribuyente_id: "",
      tipo_operacion_id: b2c?.id || "",
    };
  }, [referencialesData]);

  // Prellena el formulario de alta rápida con lo que el cajero ya escribió en el
  // buscador, para que no tenga que tipear el documento dos veces.
  //
  // El texto puede ser un documento o un nombre, y si es un documento puede ser
  // cédula o RUC. Antes se asumía siempre RUC de contribuyente y se dependía de
  // la respuesta del Padrón para corregirlo; con 6 o 7 dígitos es una cédula y
  // corresponde dejarlo como no contribuyente (el default), que es el caso más
  // común en mostrador.
  const prepararNuevoCliente = useCallback((texto) => {
    const base = getDefaultClienteData();
    const valor = (texto || "").trim();
    if (!valor) return base;

    if (!/^\d+$/.test(valor)) {
      // Texto: es un nombre.
      return { ...base, razon_social: valor };
    }

    if (valor.length <= 7) {
      // Cédula: naturaleza y tipo de documento ya vienen bien en el default.
      return { ...base, nro_documento: valor };
    }

    // RUC: contribuyente, con DV calculado y tipo según persona física/jurídica.
    const naturalezas = referencialesData?.naturaleza_receptor || [];
    const tiposOp = referencialesData?.tipo_operacion || [];
    const tiposContrib = referencialesData?.tipo_contribuyente || [];
    const contribuyente = naturalezas.find((n) => Number(n.codigo) === 1);
    const b2b = tiposOp.find((t) => Number(t.codigo) === 1);
    const esJuridica = parseInt(valor, 10) >= 80000000;
    const tipoContrib = tiposContrib.find((t) => Number(t.codigo) === (esJuridica ? 2 : 1));
    return {
      ...base,
      ruc: valor,
      dv: calcularDV(valor),
      naturaleza_id: contribuyente?.id || base.naturaleza_id,
      tipo_operacion_id: b2b?.id || base.tipo_operacion_id,
      tipo_contribuyente_id: tipoContrib?.id || "",
    };
  }, [getDefaultClienteData, referencialesData]);

  // Autocompletado DNIT automático en el modal "Nuevo Cliente Rápido".
  const { loadingDnit: loadingDnitPOS } = useDnitAutofillFlat({
    open: showNuevoClienteDialog,
    formData: nuevoClienteData,
    setFormData: setNuevoClienteData,
    referenciales: referencialesData,
  });

  // Si el alta rápida se abre con un documento ya prellenado desde el buscador,
  // verificar el duplicado de entrada: el cajero no llegó a salir del campo, así
  // que el onBlur no dispara.
  useEffect(() => {
    if (!showNuevoClienteDialog) return;
    const doc = nuevoClienteData.ruc || nuevoClienteData.nro_documento;
    if (doc) verificarClienteDuplicado(doc);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [showNuevoClienteDialog]);

  // Cuando la naturaleza cambia (incluido el cambio automático de la DNIT), re-sincroniza
  // el Tipo Operación: contribuyente → B2B (1); no contribuyente → B2C (2). Solo si el
  // actual no es válido para la naturaleza, para no pisar una elección manual coherente.
  useEffect(() => {
    if (!showNuevoClienteDialog || !nuevoClienteData.naturaleza_id) return;
    const naturalezas = referencialesData?.naturaleza_receptor || [];
    const tiposOp = referencialesData?.tipo_operacion || [];
    const esContrib = Number(naturalezas.find((n) => n.id === nuevoClienteData.naturaleza_id)?.codigo) === 1;
    const validos = esContrib ? [1, 3] : [2, 4];
    const actualCod = Number(tiposOp.find((t) => t.id === nuevoClienteData.tipo_operacion_id)?.codigo);
    if (!validos.includes(actualCod)) {
      const nuevo = tiposOp.find((t) => Number(t.codigo) === (esContrib ? 1 : 2));
      if (nuevo) setNuevoClienteData((prev) => ({ ...prev, tipo_operacion_id: nuevo.id }));
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [nuevoClienteData.naturaleza_id, showNuevoClienteDialog]);

  // Priorizar la sucursal de la sesión de caja activa; si no hay sesión, usar sucursalActiva
  const sucursalParaNumeracion = sucursalIdDeLaCaja(sesionCaja) || sucursalActiva?.id;

  const { data: numeracionesData } = useQuery({
    queryKey: ["numeraciones", sucursalParaNumeracion, isOnline],
    queryFn: () => getNumeracionesFallback(sucursalParaNumeracion, getNumeracionesBySucursal),
    enabled: !!sucursalParaNumeracion || !isOnline,
    retry: false,
  });

  // Numeración con la que se va a emitir: única fuente para el encabezado y para
  // executePayment. Antes el encabezado leía `numeracionesData[0]` crudo y la
  // emisión calculaba lo suyo aparte, así que el cajero podía ver "PE: 003" y
  // facturar con otro punto.
  //
  // Se filtra por la caja abierta porque el endpoint devuelve las numeraciones de
  // TODA la sucursal: sin eso la venta se numeraba con el punto de otra caja
  // mientras el movimiento caía en la propia. Los inactivos ya no deberían
  // llegar (el lookup del backend filtra por active), pero se descartan también
  // acá para no depender de la versión desplegada.
  //
  // Si ningún punto tiene caja asignada (configuración vieja) no se filtra por
  // caja, para no dejar la caja sin poder facturar.
  const numeracionFactura = useMemo(() => {
    const todas = (numeracionesData || []).filter(
      (n) => n.active !== false && n.punto_expedicion?.active !== false,
    );
    const cajaActivaId = cajaSeleccionada?.id || sesionCaja?.caja_id;
    const algunaTieneCaja = todas.some((n) => n.punto_expedicion?.caja_id);
    const deLaCaja =
      cajaActivaId && algunaTieneCaja
        ? todas.filter((n) => n.punto_expedicion?.caja_id === cajaActivaId)
        : todas;

    // El punto marcado como default primero; entre numeraciones, la default.
    const ordenadas = [...deLaCaja].sort(
      (a, b) =>
        (b.punto_expedicion?.es_default ? 1 : 0) - (a.punto_expedicion?.es_default ? 1 : 0) ||
        (b.es_default ? 1 : 0) - (a.es_default ? 1 : 0),
    );
    return ordenadas.find((n) => n.tipo_documento?.codigo === 1) || ordenadas[0] || null;
  }, [numeracionesData, cajaSeleccionada?.id, sesionCaja?.caja_id]);

  // Cliente "Sin Nombre" (cod_cliente: SN001), el que se usa en la venta
  // ocasional de mostrador.
  //
  // Se pide al backend por su código en vez de traer una página y filtrarla en
  // memoria: SN001 se crea junto con la empresa, y como el listado viene
  // ordenado por `created_at desc` es siempre el ÚLTIMO. Con más de 100 clientes
  // nunca aparecía, `clienteSeleccionado` quedaba en null y la venta se
  // rechazaba con "cabecera.cliente_id must be a UUID".
  const { data: clienteSinNombreData } = useQuery({
    queryKey: ["clienteSinNombre"],
    queryFn: async () => {
      const clientes = await getClientes(1, 20, "SN001");
      const lista = clientes?.data || clientes || [];
      return lista.find((c) => c.cod_cliente === "SN001") || null;
    },
  });

  const { data: posConfig } = usePosConfigQuery(cajaSeleccionada?.id);
  const updatePosConfigMutation = useUpdatePosConfigMutation();

  // Cachear posConfig en IndexedDB cuando llega de la API
  useEffect(() => {
    if (posConfig) {
      cachePosConfig(posConfig).catch(() => {});
    }
  }, [posConfig]);

  // Restaurar posConfig desde cache al montar (para offline)
  useEffect(() => {
    getCachedPosConfig()
      .then((cached) => {
        if (cached) setCachedPosConfigData(cached);
      })
      .catch(() => {});
  }, []);

  // Memoizado: sin esto, `config` tiene identidad nueva cada render y anula la
  // memoización de los hijos que lo reciben como prop.
  const config = useMemo(() => ({ ...DEFAULT_CONFIG, ...cachedPosConfigData, ...posConfig }), [cachedPosConfigData, posConfig]);

  // ¿El ítem que se está editando admite carga por monto? (modo estación).
  // Va después de `config`: antes quedaba en zona muerta temporal.
  const montoEditableEnEdicion = editingItemIndex !== null && permiteMontoEditable(items[editingItemIndex], config);

  /**
   * Modo estación: al cargar el monto, la cantidad se recalcula en el acto para
   * que se vea antes de guardar. Es función de render (no useCallback) a
   * propósito: así `config` siempre es el actual — con deps desactualizadas
   * capturaba el DEFAULT_CONFIG del primer render y derivaba con 2 decimales,
   * que hacía que el total no cerrara (100.000 -> 13,89 L -> 100.008).
   */
  const aplicarMonto = (montoNuevo) => {
    setEditItemData((prev) => {
      const monto = Number(montoNuevo) || 0;
      const precio = parseFloat(prev.precio) || 0;
      const descuento = parseFloat(prev.descuento) || 0;
      const precioNeto = precio - (prev.tipoDescuento === "monto" ? descuento : 0);
      const derivada = cantidadDesdeMonto(monto, precioNeto, decimalesCantidad(config));
      return {
        ...prev,
        monto,
        // Sin monto o sin precio, se deja la cantidad como está.
        cantidad: derivada === null ? prev.cantidad : String(derivada),
      };
    });
  };

  // Extraer datos
  const sucursales = Array.isArray(sucursalesData?.data) ? sucursalesData.data : Array.isArray(sucursalesData) ? sucursalesData : [];
  // Las pestañas siguen el rubro del POS: las categorías de otro rubro traerían
  // una grilla vacía (sus productos ya quedan fuera por `rubroPOS`).
  const categorias = (Array.isArray(categoriasData?.data) ? categoriasData.data : Array.isArray(categoriasData) ? categoriasData : [])
    .filter((c) => categoriaEnRubro(c, rubroPOS));
  // Aplanar todas las páginas cargadas por scroll infinito en un solo array.
  const productos = (productosPages?.pages || []).flatMap((p) => (Array.isArray(p?.data) ? p.data : Array.isArray(p) ? p : []));
  const monedas = Array.isArray(monedasData?.data) ? monedasData.data : Array.isArray(monedasData) ? monedasData : [];
  const mediosPago = Array.isArray(mediosPagoData?.data) ? mediosPagoData.data : Array.isArray(mediosPagoData) ? mediosPagoData : [];

  // Condiciones de pago disponibles en retail: contado, crédito a plazo
  // (cod_tipo_credito=1) y crédito en cuotas (cod_tipo_credito=2). Las que
  // exigen entrega inicial quedan fuera — implican cobrar en caja al emitir,
  // que es el flujo de POS admin.
  const condicionesPago = useMemo(
    () => (Array.isArray(condicionesPagoData?.data) ? condicionesPagoData.data : Array.isArray(condicionesPagoData) ? condicionesPagoData : []),
    [condicionesPagoData],
  );
  const condicionContado = useMemo(
    () => condicionesPago.find((c) => c.cod_condicion_venta === 1) || null,
    [condicionesPago],
  );
  const condicionesCredito = useMemo(
    () => condicionesPago.filter((c) => c.cod_condicion_venta === 2 && c.cuota_inicial !== true),
    [condicionesPago],
  );
  const condicionesAPlazo = useMemo(() => condicionesCredito.filter((c) => c.cod_tipo_credito !== 2), [condicionesCredito]);
  const condicionesEnCuotas = useMemo(() => condicionesCredito.filter((c) => c.cod_tipo_credito === 2), [condicionesCredito]);

  // Condición por defecto: contado. Sin condición de contado configurada, el
  // selector no se muestra y la venta sigue emitiéndose como hoy.
  useEffect(() => {
    if (!condicionPagoActiva && condicionContado) setCondicionPagoActiva(condicionContado);
  }, [condicionContado, condicionPagoActiva]);

  const esVentaCredito = condicionPagoActiva?.cod_condicion_venta === 2;

  // Cliente innominado: no se le puede facturar a crédito (no hay a quién cobrarle).
  const esClienteSinNombre = (cli) => {
    if (!cli) return false;
    return (
      cli.cod_cliente === "SN001" ||
      cli.personas?.razon_social?.toLowerCase() === "sin nombre" ||
      cli.personas?.tipo_documento?.codigo === 5 // Innominado
    );
  };

  // Motivo por el que el crédito no está habilitado, o null si sí lo está.
  // El cupo exacto se valida contra el backend al confirmar (verificarCredito);
  // esto es el filtro previo que decide si el botón "Crédito" está disponible.
  const creditoNoDisponibleMotivo = useMemo(() => {
    if (!config.permitir_venta_credito) return "Esta caja no tiene habilitada la venta a crédito";
    if (condicionesCredito.length === 0) return "No hay condiciones de crédito configuradas";
    if (!isOnline) return "Sin conexión: no se puede validar el crédito del cliente";
    if (!clienteSeleccionado || esClienteSinNombre(clienteSeleccionado))
      return "Seleccione un cliente nominado para facturar a crédito";
    if (creditoInfo && creditoInfo.credito_habilitado === false)
      return "Este cliente no tiene habilitada la venta a crédito";
    if (creditoInfo?.bloqueado)
      return creditoInfo.motivo_bloqueo ? `Crédito bloqueado: ${creditoInfo.motivo_bloqueo}` : "El cliente tiene el crédito bloqueado";
    // Sin tope cargado se factura igual: el tope es opcional y quien decide si
    // el cliente compra a crédito es `credito_habilitado` (clientes.service.ts,
    // verificarCredito). El cupo exacto lo valida el backend al confirmar.
    return null;
  }, [config.permitir_venta_credito, condicionesCredito.length, isOnline, clienteSeleccionado, creditoInfo]);


  // Al cambiar de condición se limpian los pagos: un pago cargado como contado
  // no tiene sentido en una venta a crédito (y viceversa).
  const handleCondicionPagoChange = (nuevaCond) => {
    setCondicionPagoActiva(nuevaCond);
    setPagosAgregados([]);
    setMetodoPagoSeleccionado(null);
    setMontoPagado("");
    setMonedaPagoSeleccionada(null);
    setCotizacionPago(null);
  };

  // Si el crédito deja de estar disponible con una condición a crédito ya
  // elegida (cambió el cliente, se cayó la conexión), volver a contado en vez
  // de dejar el cobro en un estado que el backend va a rechazar.
  useEffect(() => {
    if (esVentaCredito && creditoNoDisponibleMotivo && condicionContado) {
      setCondicionPagoActiva(condicionContado);
    }
  }, [esVentaCredito, creditoNoDisponibleMotivo, condicionContado]);

  // Sucursal activa: con caja abierta manda la sucursal de la CAJA, que es la
  // misma de la numeración. Sin esto, un usuario sin sucursal asignada que abría
  // la caja del taller descontaba stock del depósito de la primera sucursal de
  // la lista (el depósito sale de `sucursalActiva`).
  const sucursalCajaId = sucursalIdDeLaCaja(sesionCaja);
  useEffect(() => {
    if (sucursales.length === 0) return;
    const deLaCaja = sucursalCajaId ? sucursales.find((s) => s.id === sucursalCajaId) : null;
    if (deLaCaja) {
      if (sucursalActiva?.id !== deLaCaja.id) setSucursalActiva(deLaCaja);
      return;
    }
    if (!sucursalActiva) {
      const defaultSucursal = sucursales.find((s) => s.id === user?.sucursal_id) || sucursales[0];
      setSucursalActiva(defaultSucursal);
    }
  }, [sucursales, sucursalActiva, sucursalCajaId, user?.sucursal_id]);

  // Seleccionar moneda por defecto (desde config o PYG principal)
  useEffect(() => {
    // Solo inicializar una vez cuando tengamos monedas y posConfig cargados
    // posConfig debe estar definido (aunque sea null del servidor) para saber que ya se consultó
    if (monedas.length > 0 && !monedaInicializada && posConfig !== undefined) {
      let defaultMoneda = null;

      // Primero intentar usar la moneda configurada en pos_config
      if (posConfig?.moneda_default_id) {
        defaultMoneda = monedas.find((m) => m.id === posConfig.moneda_default_id);
      }

      // Si no hay configurada, usar la principal
      if (!defaultMoneda) {
        defaultMoneda = monedas.find((m) => m.es_principal && m.active) || monedas[0];
      }

      setMonedaActiva(defaultMoneda);
      setMonedaInicializada(true);
    }
  }, [monedas, monedaInicializada, posConfig]);

  // Verificar cotización cuando cambia la moneda (como en POS Admin)
  useEffect(() => {
    const verificarCotizacion = async () => {
      // Si es PYG no necesita cotización
      if (!monedaActiva || monedaActiva?.moneda?.codigo === "PYG") {
        setCotizacionActiva(null);
        setShowCotizacionAlert(false);
        setCotizacionRequerida(false);
        return;
      }

      const monedaId = monedaActiva.moneda_id;
      const hoy = hoyISO();

      try {
        // Obtener última cotización disponible
        const ultimaCotizacion = await getUltimaCotizacion(monedaId);

        if (!ultimaCotizacion) {
          toast.error(`No hay cotizaciones cargadas para ${monedaActiva.moneda?.descripcion || monedaActiva.moneda?.codigo}. Debe cargar una cotización para poder facturar.`);
          setCotizacionActiva(null);
          setShowCotizacionAlert(false);
          setCotizacionRequerida(true);
          return;
        }

        // Verificar si la cotización es del día
        const fechaCotizacion = ultimaCotizacion.fecha?.split("T")[0];

        if (fechaCotizacion === hoy) {
          // Cotización del día, usar directamente
          setCotizacionActiva(ultimaCotizacion);
          setShowCotizacionAlert(false);
          setCotizacionRequerida(false);
          toast.success(`Cotización del día: ${ultimaCotizacion.cotizacion_compra?.toLocaleString("es-PY")} / ${ultimaCotizacion.cotizacion_venta?.toLocaleString("es-PY")}`);
        } else {
          // Cotización anterior, mostrar alerta para confirmar
          setCotizacionPendiente(ultimaCotizacion);
          setShowCotizacionAlert(true);
          setCotizacionRequerida(true);
        }
      } catch (error) {
        toast.error(`No hay cotizaciones cargadas para ${monedaActiva.moneda?.descripcion || monedaActiva.moneda?.codigo}. Debe cargar una cotización para poder facturar.`);
        setCotizacionActiva(null);
        setCotizacionRequerida(true);
      }
    };

    verificarCotizacion();
  }, [monedaActiva]);

  // Handlers para aceptar o rechazar cotización pendiente
  const handleAceptarCotizacion = () => {
    setCotizacionActiva(cotizacionPendiente);
    setShowCotizacionAlert(false);
    setCotizacionRequerida(false);
    toast.success("Cotización aplicada");
  };

  const handleRechazarCotizacion = () => {
    setShowCotizacionAlert(false);
    setCotizacionPendiente(null);
    toast.info("Debe cargar una cotización del día para continuar");
  };

  // Sincronizar configForm cuando se abre el diálogo
  useEffect(() => {
    if (showConfigDialog) {
      setConfigForm({
        printer_name: posConfig?.printer_name || DEFAULT_CONFIG.printer_name || "",
        printer_paper_width: posConfig?.printer_paper_width || DEFAULT_CONFIG.printer_paper_width || 80,
        auto_print_ticket: posConfig?.auto_print_ticket ?? DEFAULT_CONFIG.auto_print_ticket ?? false,
      });
      setQzStatus(isQzConnected() ? "connected" : "disconnected");
    }
  }, [showConfigDialog, posConfig]);

  const handleDetectPrinters = useCallback(async () => {
    setLoadingPrinters(true);
    setQzStatus("connecting");
    try {
      const connected = await connectQzTray();
      if (connected) {
        setQzStatus("connected");
        const printers = await getPrinters();
        setAvailablePrinters(printers || []);
        if (printers?.length > 0) {
          toast.success(`${printers.length} impresora(s) detectada(s)`);
        }
      } else {
        setQzStatus("error");
        toast.error("No se pudo conectar a QZ Tray");
      }
    } catch (err) {
      setQzStatus("error");
      toast.error("Error al detectar impresoras: " + err.message);
    } finally {
      setLoadingPrinters(false);
    }
  }, []);

  const handleSaveConfig = useCallback(() => {
    if (!cajaSeleccionada?.id) {
      toast.error("No hay caja seleccionada");
      return;
    }
    updatePosConfigMutation.mutate(
      {
        cajaId: cajaSeleccionada.id,
        printer_name: configForm.printer_name,
        printer_paper_width: configForm.printer_paper_width,
        auto_print_ticket: configForm.auto_print_ticket,
      },
      {
        onSuccess: () => {
          toast.success("Configuración guardada");
          setShowConfigDialog(false);
        },
        onError: (err) => {
          toast.error("Error al guardar: " + err.message);
        },
      },
    );
  }, [cajaSeleccionada?.id, configForm, updatePosConfigMutation]);

  // Focus en input de código de barras
  useEffect(() => {
    barcodeInputRef.current?.focus();
  }, []);

  // Cargar cliente "Sin Nombre" por defecto
  useEffect(() => {
    if (clienteSinNombreData && !clienteSeleccionado) {
      setClienteSeleccionado(clienteSinNombreData);
    }
  }, [clienteSinNombreData, clienteSeleccionado]);

  // Fase 2: Cargar info de crédito cuando cambia el cliente seleccionado
  useEffect(() => {
    const fetchCredito = async () => {
      if (!clienteSeleccionado?.id) {
        setCreditoInfo(null);
        return;
      }
      try {
        const data = await getCreditInfo(clienteSeleccionado.id);
        setCreditoInfo(data);
      } catch {
        // Si falla (cliente sin crédito configurado, offline, etc.), limpiar
        setCreditoInfo(null);
      }
    };
    fetchCredito();
  }, [clienteSeleccionado?.id]);

  // Cargar sesión de caja activa y cajas disponibles
  useEffect(() => {
    const cargarDatosCaja = async () => {
      // 1) Restaurar desde cache local primero (instantáneo, funciona offline)
      try {
        const cached = await getCachedCajaSession();
        console.log("Cache de caja leído:", cached);
        // Manejar ambos formatos: directo {id,...} o wrapper {message, data:{id,...}}
        const sesion = cached?.sesion?.id ? cached.sesion : cached?.sesion?.data?.id ? cached.sesion.data : null;
        if (sesion) {
          console.log("Restaurando sesión de caja desde cache:", sesion.id);
          setSesionCaja(sesion);
          if (cached.caja) setCajaSeleccionada(cached.caja);
          setCajasDisponibles(cached.cajas || []);
          // Una caché guardada con la sucursal de la caja ya alcanza para saber
          // el rubro: no hace falta esperar a la API (y offline es lo único).
          if (sesion.cajas?.empresas_sucursales) setSesionResuelta(true);
        }
      } catch (e) {
        console.warn("No se pudo leer cache de caja:", e);
      }

      // 2) Intentar refrescar desde la API (actualiza con datos frescos)
      try {
        const cajas = await getCajasDisponibles();
        setCajasDisponibles(cajas || []);

        const sesion = await getSesionActiva();
        if (sesion) {
          setSesionCaja(sesion);
          const cajaActiva = (cajas || []).find((c) => c.id === sesion.caja_id);
          if (cajaActiva) setCajaSeleccionada(cajaActiva);
          cacheCajaSession({
            sesion,
            caja: cajaActiva || null,
            cajas: cajas || [],
          });
        } else {
          setSesionCaja(null);
          setCajaSeleccionada(null);
          cacheCajaSession({ sesion: null, caja: null, cajas: cajas || [] });
        }
      } catch (error) {
        console.warn("API de caja no disponible, usando cache local:", error?.message);
      } finally {
        // Con o sin respuesta de la API: la grilla no puede quedar bloqueada.
        setSesionResuelta(true);
      }
    };
    cargarDatosCaja();
  }, []);

  // Funciones para manejo de caja
  const handleAbrirCaja = async (tokenOrEvent) => {
    if (!cajaSeleccionada) {
      toast.error("Seleccione una caja");
      return;
    }
    const token = typeof tokenOrEvent === "string" ? tokenOrEvent : null;
    const authToken = token || tokenAutorizacion;
    // Verificar si requiere autorización
    if (posConfig?.requiere_autorizacion_apertura && !authToken) {
      setPinOperacion("apertura_caja");
      setShowPinModal(true);
      return;
    }
    const monto = parseInt(montoApertura.replace(/\D/g, "") || "0");

    setIsLoadingCaja(true);
    try {
      const response = await abrirCaja({
        caja_id: cajaSeleccionada.id,
        monto_apertura: monto,
        sucursal_id: sucursalActiva?.id,
        token_autorizacion: authToken || undefined,
      });
      // abrirCaja devuelve { message, data: sesion } — extraer la sesión real
      const sesion = response?.data || response;
      setSesionCaja(sesion);
      setShowAbrirCajaDialog(false);
      setMontoApertura("");
      setTokenAutorizacion(null);
      toast.success("Caja abierta exitosamente");
      // Cachear sesión para uso offline
      cacheCajaSession({
        sesion,
        caja: cajaSeleccionada,
        cajas: cajasDisponibles,
      });
    } catch (error) {
      console.error("Error abriendo caja:", error);
      toast.error(error.response?.data?.message || "Error al abrir la caja");
      setTokenAutorizacion(null);
    } finally {
      setIsLoadingCaja(false);
    }
  };

  const handleCerrarCaja = async (tokenOrEvent, arqueoDetalle) => {
    if (!sesionCaja) {
      toast.info("No hay caja abierta. Debe abrir una caja primero.");
      setShowCerrarCajaDialog(false);
      setShowAbrirCajaDialog(true);
      return;
    }
    // Guardar arqueoDetalle si viene (para recuperarlo tras PIN)
    if (arqueoDetalle) {
      setPendingArqueoDetalle(arqueoDetalle);
    }
    const token = typeof tokenOrEvent === "string" ? tokenOrEvent : null;
    const authToken = token || tokenAutorizacion;
    // Verificar si requiere autorización
    if (posConfig?.requiere_autorizacion_cierre && !authToken) {
      setPinOperacion("cierre_caja");
      setShowPinModal(true);
      return;
    }
    // parseMonto y no parseInt(replace(/\D/g)): ese replace borra el separador
    // decimal, así que "2.492.687,3" se habría enviado como 24.926.873.
    const monto = parseMonto(montoCierre);

    setIsLoadingCaja(true);
    try {
      const detalle = arqueoDetalle || pendingArqueoDetalle;
      await cerrarCaja(sesionCaja.id, {
        monto_cierre: monto,
        observaciones: observacionesCierre || null,
        token_autorizacion: authToken || undefined,
        arqueo_detalle: detalle || undefined,
      });
      setSesionCaja(null);
      setCajaSeleccionada(null);
      setShowCerrarCajaDialog(false);
      setMontoCierre("");
      setObservacionesCierre("");
      setTokenAutorizacion(null);
      setPendingArqueoDetalle(null);
      toast.success("Caja cerrada exitosamente");
      // Limpiar cache de sesión de caja
      clearCachedCajaSession();
      // Recargar cajas disponibles
      const cajas = await getCajasDisponibles();
      setCajasDisponibles(cajas || []);
    } catch (error) {
      console.error("Error cerrando caja:", error);
      toast.error(error.response?.data?.message || "Error al cerrar la caja");
      setTokenAutorizacion(null);
    } finally {
      setIsLoadingCaja(false);
    }
  };

  const handlePinSuccess = useCallback(
    (data) => {
      setTokenAutorizacion(data.token);
      setShowPinModal(false);
      // Re-ejecutar la operación con el token
      if (pinOperacion === "apertura_caja") {
        setTimeout(() => handleAbrirCaja(data.token), 100);
      } else if (pinOperacion === "cierre_caja") {
        setTimeout(() => handleCerrarCaja(data.token), 100);
      } else if (pinOperacion === "anulacion") {
        // Tras autorización, abrir diálogo de motivo de anulación
        setAnulacionDialog((prev) => ({ ...prev, open: true }));
      }
    },
    [pinOperacion],
  );

  // Anulación de factura: paso 1 - solicitar PIN
  const handleAnularFactura = useCallback(
    (factura) => {
      if (!puedeAnularFactura) {
        toast.error("No tenés permiso para anular facturas.");
        return;
      }
      setAnulacionDialog({ open: false, factura });
      setAnulacionMotivo("");
      setPinOperacion("anulacion");
      setShowPinModal(true);
    },
    [puedeAnularFactura],
  );

  // Anulación de factura: paso 2 - confirmar con motivo
  const handleConfirmAnulacion = useCallback(async () => {
    const factura = anulacionDialog.factura;
    if (!factura || !anulacionMotivo.trim()) {
      toast.error("Debe ingresar un motivo de anulación");
      return;
    }
    setAnulacionLoading(true);
    try {
      const result = await anularFactura(factura.id, anulacionMotivo, tokenAutorizacion);
      toast.success(result.message || "Factura anulada exitosamente");
      if (result.data?.sifen?.message) {
        toast.info(`SIFEN: ${result.data.sifen.message}`, { duration: 5000 });
      }
      setAnulacionDialog({ open: false, factura: null });
      setAnulacionMotivo("");
      setTokenAutorizacion(null);
      // Recargar transacciones
      loadTransacciones(transaccionesPage);
    } catch (error) {
      const msg = error.response?.data?.message || "Error al anular la factura";
      toast.error(msg);
    } finally {
      setAnulacionLoading(false);
    }
  }, [anulacionDialog.factura, anulacionMotivo, tokenAutorizacion, transaccionesPage]);

  // Calcular totales usando calcularSubtotales de invoiceCalculations
  const totales = useMemo(() => {
    // console.log("Items para calcular totales:", items);
    const subtotalesCalculados = calcularSubtotales(items);
    // console.log("Subtotales calculados:", subtotalesCalculados);

    // Aplicar descuento global si existe
    let descuentoGlobalTotal = 0;
    if (tipoDescuentoGlobal === "porcentaje") {
      descuentoGlobalTotal = subtotalesCalculados.total_neto_operacion * (descuentoGlobal / 100);
    } else if (tipoDescuentoGlobal === "monto") {
      descuentoGlobalTotal = descuentoGlobal;
    }

    // Redondeo a los decimales de la moneda: con cantidades fraccionadas
    // (modo estación: 11,1235 L x 8.990) el total quedaba en 100.000,265, y en
    // guaraníes no existen los centavos. Para cantidades enteras es un no-op.
    const decimalesMonedaFactura = (monedaActiva?.moneda?.codigo || "PYG") === "PYG" ? 0 : (monedaActiva?.moneda?.decimales_estandar ?? 2);
    const factorRedondeo = 10 ** decimalesMonedaFactura;
    const totalFinal = Math.round((subtotalesCalculados.total_neto_operacion - descuentoGlobalTotal) * factorRedondeo) / factorRedondeo;

    return {
      ...subtotalesCalculados,
      subtotal: subtotalesCalculados.total_neto_operacion,
      total: totalFinal,
      total_descuento_global: descuentoGlobalTotal,
      totalIva5: subtotalesCalculados.total_liquidacion_iva5,
      totalIva10: subtotalesCalculados.total_liquidacion_iva10,
      totalExentas: subtotalesCalculados.subtotal_operacion_exenta,
    };
  }, [items, descuentoGlobal, tipoDescuentoGlobal, monedaActiva]);

  // Cronograma de la venta a crédito. Alimenta el panel del cobro y el payload
  // de la factura con el mismo cálculo, así lo que ve el cajero es exactamente
  // lo que queda en la cuenta a cobrar.
  //
  // A plazo (cod_tipo_credito=1): una cuota única a `dias_plazo`.
  // En cuotas (cod_tipo_credito=2): `cuotas` cuotas iguales cada
  // `intervalo_dias`, la primera a un intervalo de hoy. El reparto se hace en
  // unidades mínimas de la moneda (guaraníes enteros, centavos en USD) y el
  // sobrante del redondeo va en la última cuota, para que la suma cierre exacta
  // contra el total de la factura.
  const decimalesMonedaActiva = useMonedaDecimals(monedaActiva?.moneda?.codigo || "PYG");
  const cronogramaCredito = useMemo(() => {
    if (!esVentaCredito || !condicionPagoActiva) return [];
    const moneda_iso = monedaActiva?.moneda?.codigo || "PYG";
    const factor = 10 ** decimalesMonedaActiva;
    const totalMinimas = Math.round((Number(totales.total) || 0) * factor);
    if (totalMinimas <= 0) return [];

    const sumarDias = (dias) => {
      const f = new Date();
      f.setDate(f.getDate() + dias);
      return aFechaISO(f);
    };

    if (condicionPagoActiva.cod_tipo_credito !== 2) {
      return [{
        nro_cuota: 1,
        monto_cuota: totalMinimas / factor,
        vencimiento: sumarDias(Number(condicionPagoActiva.dias_plazo) || 30),
        moneda_iso,
      }];
    }

    const cantidad = Math.max(1, Number(condicionPagoActiva.cuotas) || 1);
    const intervalo = Number(condicionPagoActiva.intervalo_dias) || 30;
    const base = Math.floor(totalMinimas / cantidad);
    return Array.from({ length: cantidad }, (_, i) => ({
      nro_cuota: i + 1,
      monto_cuota: (i === cantidad - 1 ? totalMinimas - base * (cantidad - 1) : base) / factor,
      vencimiento: sumarDias(intervalo * (i + 1)),
      moneda_iso,
    }));
  }, [esVentaCredito, condicionPagoActiva, totales.total, monedaActiva, decimalesMonedaActiva]);

  // Helper de formateo de moneda consistente
  const fmtMoney = useCallback(
    (value, currencyCode, iso) => {
      return FormatearNumeroDinero(Number(value) || 0, currencyCode || monedaActiva?.moneda?.codigo || "PYG", iso || monedaActiva?.moneda?.iso || "PY");
    },
    [monedaActiva],
  );

  // Banner del último producto agregado: se arma con lo que hay AHORA en el
  // carrito, así la cantidad acompaña a los escaneos repetidos y el banner se
  // apaga solo si el ítem se borra o la venta se cierra.
  const ultimoAgregado = useMemo(() => {
    if (!ultimoAgregadoId) return null;
    const item = items.find((it) => it.producto_id === ultimoAgregadoId);
    if (!item) return null;
    return {
      descripcion: item.descripcion,
      precio: Number(item.precio_unitario) || 0,
      cantidad: Number(item.cantidad) || 1,
      oferta: item._oferta?.nombre_oferta || null,
    };
  }, [ultimoAgregadoId, items]);

  // Montos del crédito del cliente: van en la moneda del crédito (que puede no
  // ser la de la caja) y un tope vacío se muestra como "Sin tope", no como 0.
  const fmtCredito = useCallback(
    (value) => fmtMoney(value, creditoInfo?.moneda_credito),
    [fmtMoney, creditoInfo],
  );
  const textoTope = useCallback(
    (value) => (creditoInfo?.sin_tope || !Number(value) ? "Sin tope" : fmtCredito(value)),
    [creditoInfo, fmtCredito],
  );

  // Búsqueda de productos
  const handleSearch = useCallback(
    async (query) => {
      if (query.length < 2) {
        setSearchResults([]);
        return;
      }
      setIsSearching(true);
      try {
        // Usar el depósito principal de la sucursal para consultar stock
        const depositoId = depositoPrincipal?.id || null;
        const results = await searchProductosFallback(query, 20, (q, limit) => buscarProductos(q, limit, depositoId, null, false, null, rubroPOS), rubroPOS);
        setSearchResults(Array.isArray(results) ? results : []);
      } catch (error) {
        console.error("Error buscando productos:", error);
        setSearchResults([]);
      } finally {
        setIsSearching(false);
      }
    },
    [depositoPrincipal, searchProductosFallback, rubroPOS],
  );

  // Debounce search
  useEffect(() => {
    const timer = setTimeout(() => {
      if (searchQuery) {
        handleSearch(searchQuery);
      } else {
        setSearchResults([]);
      }
    }, 300);
    return () => clearTimeout(timer);
  }, [searchQuery, handleSearch]);

  // Búsqueda de clientes
  const handleClienteSearch = useCallback(
    async (query) => {
      if (query.length < 2) {
        setClientesResults([]);
        return;
      }
      try {
        const results = await searchClientesFallback(query, searchClientes);
        setClientesResults(Array.isArray(results) ? results : []);
      } catch (error) {
        console.error("Error buscando clientes:", error);
        setClientesResults([]);
      }
    },
    [searchClientesFallback],
  );

  // Selecciona un cliente ya existente y cierra el flujo de búsqueda/alta.
  const usarClienteExistente = useCallback((cliente) => {
    setClienteSeleccionado({
      ...cliente,
      razon_social: cliente.personas?.razon_social || cliente.razon_social,
      ruc: cliente.personas?.ruc || cliente.ruc,
      dv: cliente.personas?.dv || cliente.dv,
    });
    setShowNuevoClienteDialog(false);
    setShowClienteDialog(false);
    setClienteSearch("");
    setClientesResults([]);
    setClienteDuplicado(null);
  }, []);

  // Busca si ya hay un cliente con ese documento mientras se carga el alta
  // rápida. Sin esto el cajero se entera recién al guardar, cuando el backend
  // responde 409, y tiene que cerrar todo y volver a buscar a mano.
  const verificarClienteDuplicado = useCallback(
    async (documento) => {
      const doc = String(documento || "").trim();
      if (doc.length < 2) {
        setClienteDuplicado(null);
        return;
      }
      try {
        const results = await searchClientesFallback(doc, searchClientes);
        const lista = Array.isArray(results) ? results : [];
        // Coincidencia exacta de documento: un match parcial por nombre no
        // significa que sea el mismo cliente.
        const existente = lista.find(
          (c) => String(c.personas?.ruc || "") === doc || String(c.personas?.nro_documento || "") === doc,
        );
        setClienteDuplicado(existente || null);
      } catch {
        setClienteDuplicado(null);
      }
    },
    [searchClientesFallback],
  );

  // Debounce cliente search
  useEffect(() => {
    const timer = setTimeout(() => {
      if (clienteSearch) {
        handleClienteSearch(clienteSearch);
      } else {
        setClientesResults([]);
      }
    }, 300);
    return () => clearTimeout(timer);
  }, [clienteSearch, handleClienteSearch]);

  /**
   * Arma el ítem y lo mete en el carrito. Se extrajo de addToCart para que el
   * flujo normal (cantidad 1) y el del modo estación (cantidad derivada del
   * monto) compartan exactamente la misma construcción.
   */
  const insertarItemEnCarrito = useCallback(
    ({ producto, productoMapeado, precioUnitario, descuentoTotalPct, precioListaInfo, ofertaInfo, cantidad }) => {
      const afectacionesIva = referencialesData?.afectacion_iva || [];
      // Opción C: precio_unitario = precioBase, descuento = combinado de lista + oferta
      const datosEdicion = {
        cantidad,
        precio: precioUnitario,
        descuento: descuentoTotalPct,
        tipoDescuento: "porcentaje",
        descripcion: producto.descripcion || producto.nombre,
        unidad_medida: productoMapeado.unidad_medida,
      };

      const nuevoItem = construirItemDetalle(productoMapeado, datosEdicion, clienteSeleccionado, afectacionesIva, {
        cotizacion: null,
        codigoMoneda: monedaActiva?.moneda?.codigo || "PYG",
        precioListaInfo,
        ofertaInfo,
      });

      // Adjuntar metadata de oferta al item
      if (ofertaInfo) {
        nuevoItem._oferta = ofertaInfo;
      }

      console.log("Producto mapeado:", productoMapeado);
      console.log("Nuevo item construido:", nuevoItem);

      setItems((prev) => {
        const existingIndex = prev.findIndex((item) => item.producto_id === nuevoItem.producto_id);
        if (existingIndex >= 0) {
          const itemExistente = prev[existingIndex];
          const nuevaCantidad = itemExistente.cantidad + 1;

          // Reconstruir item con nueva cantidad
          const datosActualizados = {
            cantidad: nuevaCantidad,
            precio: itemExistente.precio_unitario,
            descuento: itemExistente.porcentaje_descuento_item || 0,
            tipoDescuento: "porcentaje",
            descripcion: itemExistente.descripcion,
            unidad_medida: itemExistente.cod_unidad_medida,
          };

          const itemActualizado = construirItemDetalle(productoMapeado, datosActualizados, clienteSeleccionado, afectacionesIva, {
            cotizacion: null,
            codigoMoneda: monedaActiva?.moneda?.codigo || "PYG",
          });
          // Preservar metadata de oferta
          if (itemExistente._oferta) {
            itemActualizado._oferta = itemExistente._oferta;
          }
          // Mover item actualizado al inicio del array
          const sinItem = prev.filter((_, i) => i !== existingIndex);
          return [itemActualizado, ...sinItem];
        }
        // Agregar nuevo item al inicio
        return [nuevoItem, ...prev];
      });
      setSearchQuery("");
      setSearchResults([]);
      barcodeInputRef.current?.focus();
      // Confirmación persistente del último ítem: en venta rápida un toast se va
      // antes de que el cajero levante la vista del escáner, y queda la duda de
      // si el producto entró. El banner no se va solo, así que el toast de
      // "agregado" solo tapaba el carrito repitiendo lo que ya se ve.
      setUltimoAgregadoId(nuevoItem.producto_id);
    },
    [referencialesData, clienteSeleccionado, monedaActiva],
  );

  /** Confirma el monto cargado y agrega el combustible con la cantidad derivada. */
  const confirmarMontoNuevoItem = () => {
    if (!pendienteMonto) return;
    const cantidad = cantidadDesdeMonto(montoNuevoItem, pendienteMonto.precioUnitario, decimalesCantidad(config));
    if (cantidad === null) {
      toast.error(montoNuevoItem > 0 ? "El producto no tiene precio: no se puede calcular la cantidad." : "Cargá un monto mayor a cero.");
      return;
    }
    insertarItemEnCarrito({ ...pendienteMonto, cantidad });
    setPendienteMonto(null);
    setMontoNuevoItem(0);
  };

  const cancelarMontoNuevoItem = () => {
    setPendienteMonto(null);
    setMontoNuevoItem(0);
  };

  // Agregar producto al carrito usando construirItemDetalle
  const addToCart = useCallback(
    async (producto) => {
      // Mapear campos del producto para construirItemDetalle. La afectación, la
      // tasa y la proporción las resuelve resolverAfectacionIva: el mapeo manual
      // con `|| 10` / `|| 100` convertía la tasa 0 de un exento en 10, mandando
      // dTasaIVA=10 al DE para un ítem con iAfecIVA=3.
      const afectacion = resolverAfectacionIva(producto);
      // Mapear campos del producto para construirItemDetalle
      const productoMapeado = {
        ...producto,
        porcentaje_iva: afectacion.porcentaje,
        proporcion_iva: afectacion.proporcion,
        codigo_afectacion_iva: afectacion.codigo,
        descripcion_afectacion_iva: afectacion.descripcion,
        unidad_medida: producto.unidades_de_medida?.representacion || "UNI",
      };

      // Opción C: Resolver precio con desglose - precioBase + descuentos
      const precioProducto = producto.precio || producto.p_venta || producto.precio_venta || 0;
      let precioUnitario = precioProducto;
      let precioListaFinal = null;
      let descuentoTotalPct = 0;
      let precioListaInfo = null;

      if (clienteSeleccionado?.lista_precios_id && producto.id && user?.empresa_id) {
        try {
          const response = await getPrecioProducto({
            productoId: producto.id,
            empresaId: user.empresa_id,
            clienteId: clienteSeleccionado.id,
          });
          const resolved = response?.data || response;
          if (resolved?.listaAplicada) {
            // Opción C: usar precioBase del backend como precio unitario
            precioUnitario = resolved.precioBase ?? precioProducto;
            precioListaFinal = resolved.precioFinal ?? null;
            // Sumar descuentos de la lista (general + por producto)
            descuentoTotalPct = (Number(resolved.descuentoGeneral) || 0) + (Number(resolved.descuentos) || 0);
            precioListaInfo = { listaAplicada: resolved.listaAplicada };
          }
        } catch (error) {
          console.error("Error resolviendo precio por lista:", error);
        }
      }

      // Evaluar ofertas para este producto
      let ofertaInfo = null;
      try {
        const evalResult = await evaluarOfertas({
          items: [
            {
              producto_id: producto.id,
              cantidad: 1,
              precio_unitario: precioUnitario,
              ...(precioListaFinal != null && {
                precio_lista: precioListaFinal,
              }),
            },
          ],
          ...(sucursalActiva?.id && { sucursal_id: sucursalActiva.id }),
        });
        const itemResult = evalResult?.items?.[0];
        if (itemResult?.oferta_id) {
          ofertaInfo = {
            oferta_id: itemResult.oferta_id,
            nombre_oferta: itemResult.nombre_oferta,
            tipo_oferta: itemResult.tipo_oferta,
            descuento_oferta: itemResult.descuento,
            precio_sin_oferta: precioListaFinal ?? precioUnitario,
            origen_precio: itemResult.origen_precio,
          };
          // Opción C: calcular descuento de oferta como % de precioUnitario
          const precioAntes = precioListaFinal ?? precioUnitario;
          const precioConOferta = itemResult.precio_final;
          if (precioUnitario > 0 && precioAntes > precioConOferta) {
            const descuentoOfertaMonto = precioAntes - precioConOferta;
            descuentoTotalPct += (descuentoOfertaMonto / precioUnitario) * 100;
          }
        }
      } catch (error) {
        console.error("Error evaluando ofertas:", error);
      }

      // Redondear descuento a 2 decimales
      descuentoTotalPct = Math.round(descuentoTotalPct * 100) / 100;

      // Validar stock según la política de la sucursal (misma regla que aplica
      // el backend al facturar). Se compara contra lo que YA está en el carrito:
      // validar solo "stock <= 0" dejaba pasar el caso de agregar 3 unidades de
      // un producto con stock 2 y recién fallaba al confirmar la venta.
      if (producto.maneja_inventario && producto.stock_disponible !== undefined && politicaStock !== "libre") {
        const yaEnCarrito = items.filter((it) => it.producto_id === producto.id).reduce((suma, it) => suma + Number(it.cantidad || 0), 0);
        const requerido = yaEnCarrito + 1;
        const disponible = Number(producto.stock_disponible);

        if (requerido > disponible) {
          // Un id por producto: escanear cinco veces el mismo artículo sin stock
          // actualiza un solo aviso en lugar de apilar cinco.
          const avisoStockId = `stock-${producto.id}`;
          if (politicaStock === "estricto") {
            toast.error(`Stock insuficiente para "${producto.descripcion || producto.nombre}". Disponible: ${disponible}, requerido: ${requerido}`, {
              id: avisoStockId,
            });
            return;
          }
          toast.warning(`Stock insuficiente para "${producto.descripcion || producto.nombre}" (disponible: ${disponible}). Se generará stock negativo.`, {
            id: avisoStockId,
          });
        }
      }

      // Modo estación: el combustible se vende por monto, no por litros. Se pide
      // el monto acá (es cuando el cliente lo dice) y la cantidad sale de él.
      // Sin esto el ítem entraba con cantidad 1 y el monto terminaba tipeándose
      // en "Monto recibido" del cobro, que significa efectivo entregado y por
      // eso calculaba vuelto.
      if (permiteMontoEditable({ unidad_medida: productoMapeado.unidad_medida }, config)) {
        setPendienteMonto({
          producto,
          productoMapeado,
          precioUnitario,
          descuentoTotalPct,
          precioListaInfo,
          ofertaInfo,
        });
        return;
      }

      insertarItemEnCarrito({
        producto,
        productoMapeado,
        precioUnitario,
        descuentoTotalPct,
        precioListaInfo,
        ofertaInfo,
        cantidad: 1,
      });
    },
    [referencialesData, clienteSeleccionado, monedaActiva, user?.empresa_id, sucursalActiva?.id, politicaStock, items, config],
  );

  // Modificar cantidad
  const updateQuantity = useCallback(
    (index, delta) => {
      const afectacionesIva = referencialesData?.afectacion_iva || [];

      setItems((prev) => {
        const updated = [...prev];
        const itemActual = updated[index];
        const newQty = itemActual.cantidad + delta;

        if (newQty <= 0) {
          updated.splice(index, 1);
        } else {
          // Reconstruir item con nueva cantidad
          const datosEdicion = {
            cantidad: newQty,
            precio: itemActual.precio_unitario,
            descuento: itemActual.porcentaje_descuento_item || 0,
            tipoDescuento: "porcentaje",
            descripcion: itemActual.descripcion,
            unidad_medida: itemActual.cod_unidad_medida || "UN",
          };

          updated[index] = construirItemDetalle(
            {
              id: itemActual.producto_id,
              porcentaje_iva: itemActual.porcentaje_iva,
              codigo_afectacion_iva: itemActual.codigo_afectacion_iva,
              proporcion_iva: itemActual.proporcion_iva,
            },
            datosEdicion,
            clienteSeleccionado,
            afectacionesIva,
            {
              cotizacion: null,
              codigoMoneda: monedaActiva?.moneda?.codigo || "PYG",
            },
          );
        }
        return updated;
      });
    },
    [referencialesData, clienteSeleccionado, monedaActiva],
  );

  // Eliminar item
  const removeItem = useCallback((index) => {
    setItems((prev) => prev.filter((_, i) => i !== index));
  }, []);

  // Limpiar carrito
  const clearCart = useCallback(() => {
    setItems([]);
    barcodeInputRef.current?.focus();
  }, []);

  // Abrir diálogo para editar item
  const openEditItemDialog = useCallback(
    (index) => {
      const item = items[index];
      setEditingItemIndex(index);
      // Determinar tipo de descuento basado en los valores del item
      const tieneDescuentoMonto = item.descuento_particular_precio_unitario_item > 0;

      const montoEditable = permiteMontoEditable(item, config);
      setEditItemData({
        precio: (item.precio_unitario || item.precio_venta)?.toString() || "",
        descuento: tieneDescuentoMonto ? item.descuento_particular_precio_unitario_item?.toString() || "0" : (item.porcentaje_descuento_item || item.descuento)?.toString() || "0",
        // Con el modo activo ambos arrancan en 0: monto y cantidad son las dos
        // caras de lo mismo, dejar la cantidad vieja al lado de un monto en 0
        // confundía. Se carga el monto y los dos se llenan juntos.
        cantidad: montoEditable ? "0" : item.cantidad?.toString() || "1",
        monto: 0,
        tipoDescuento: tieneDescuentoMonto ? "monto" : "porcentaje",
      });
      setEditItemDialog(true);
    },
    // `config` va en las deps: sin él se capturaba el DEFAULT_CONFIG del primer
    // render, que fue la causa del total mal calculado.
    [items, config],
  );

  // Guardar cambios del item editado usando construirItemDetalle
  const saveEditedItem = useCallback(() => {
    if (editingItemIndex === null) return;

    const precio = parseFloat(editItemData.precio) || 0;
    const descuento = parseFloat(editItemData.descuento) || 0;
    // parseFloat, no parseInt: en litros/kg la cantidad es fraccionada y
    // truncarla perdía el decimal (1,1765 L -> 1 L).
    const cantidad = parseFloat(editItemData.cantidad) || 1;
    const tipoDescuento = editItemData.tipoDescuento || "porcentaje";

    // La cantidad ya viene derivada del monto en vivo (ver aplicarMonto), así que
    // acá sólo se usa lo que muestra el campo.

    // Validar según el tipo de descuento
    if (precio < 0 || descuento < 0 || cantidad <= 0) {
      toast.error("Valores inválidos");
      return;
    }
    if (tipoDescuento === "porcentaje" && descuento > 100) {
      toast.error("El porcentaje de descuento no puede ser mayor a 100%");
      return;
    }
    if (tipoDescuento === "monto" && descuento > precio) {
      toast.error("El descuento no puede ser mayor al precio");
      return;
    }

    const afectacionesIva = referencialesData?.afectacion_iva || [];

    setItems((prev) => {
      const updated = [...prev];
      const itemActual = updated[editingItemIndex];

      const datosEdicion = {
        cantidad: cantidad,
        precio: precio,
        descuento: descuento,
        tipoDescuento: tipoDescuento,
        descripcion: itemActual.descripcion,
        unidad_medida: itemActual.cod_unidad_medida || "UN",
      };

      // Reconstruir el item con los nuevos valores
      updated[editingItemIndex] = construirItemDetalle(
        {
          id: itemActual.producto_id,
          porcentaje_iva: itemActual.porcentaje_iva,
          codigo_afectacion_iva: itemActual.codigo_afectacion_iva,
          proporcion_iva: itemActual.proporcion_iva,
        },
        datosEdicion,
        clienteSeleccionado,
        afectacionesIva,
        {
          cotizacion: null,
          codigoMoneda: monedaActiva?.moneda?.codigo || "PYG",
        },
      );
      return updated;
    });

    setEditItemDialog(false);
    setEditingItemIndex(null);
    toast.success("Item actualizado");
  }, [editingItemIndex, editItemData, referencialesData, clienteSeleccionado, monedaActiva]);

  // Fase 2: Procesar override de crédito bloqueado
  const handleCreditoOverride = useCallback(() => {
    const SUPERVISOR_PIN = "1234"; // TODO: Configurar PIN de supervisor desde backend
    if (creditoOverridePin === SUPERVISOR_PIN) {
      setShowCreditoBlockDialog(false);
      setCreditoOverridePin("");
      if (creditoOverrideCallback) {
        creditoOverrideCallback();
        setCreditoOverrideCallback(null);
      }
    } else {
      toast.error("PIN de supervisor incorrecto");
    }
  }, [creditoOverridePin, creditoOverrideCallback]);

  // Procesar pago
  const handlePayment = async () => {
    if (items.length === 0) {
      toast.error("No hay productos en el carrito");
      return;
    }

    if (esVentaCredito) {
      // Las condiciones del gate se revalidan acá porque entre que se eligió
      // "Crédito" y se confirmó pudo cambiar el cliente o caerse la conexión.
      if (creditoNoDisponibleMotivo) {
        toast.error(creditoNoDisponibleMotivo);
        return;
      }
      // Cupo real contra el backend: es la única fuente de verdad del saldo
      // disponible (el saldo cacheado en creditoInfo puede estar viejo).
      // isSaving se levanta ya acá: la verificación es un viaje a la API y sin
      // esto un segundo Enter emitiría la factura dos veces.
      setIsSaving(true);
      try {
        const verificacion = await verificarCredito(clienteSeleccionado.id, totales.total || 0);
        if (!verificacion?.permitido) {
          toast.error(verificacion?.mensaje || "El cliente no tiene crédito disponible para este monto");
          setIsSaving(false);
          return;
        }
      } catch {
        toast.error("No se pudo verificar el crédito del cliente. Intente de nuevo o cobre al contado.");
        setIsSaving(false);
        return;
      }
      await executePayment();
      return;
    }

    // Fase 2: Verificar crédito bloqueado
    if (creditoInfo?.bloqueado) {
      setShowCreditoBlockDialog(true);
      setCreditoOverrideCallback(() => () => executePayment());
      return;
    }

    await executePayment();
  };

  // ¿Está deshabilitado el cobro? (misma regla que el botón "Confirmar Venta").
  // A crédito no se cobra nada en caja: alcanza con tener items y cliente.
  const cobroDeshabilitado =
    isSaving ||
    (!esVentaCredito &&
      ((pagosAgregados.length === 0 && !metodoPagoSeleccionado) ||
        (pagosAgregados.length === 0 && metodoPagoSeleccionado?.codigo === 1 && parseInt(montoPagado || 0) < totales.total) ||
        (pagosAgregados.length > 0 && pagosAgregados.reduce((sum, p) => sum + (Number(p.monto) || 0), 0) < totales.total)));

  // Enter dentro del diálogo de pago = Confirmar Venta (venta rápida), respetando la
  // misma condición del botón. Ignora Enter en textarea (notas) y con Shift.
  const handlePaymentKeyDown = (e) => {
    if (e.key !== "Enter" || e.shiftKey) return;
    if (e.target?.tagName === "TEXTAREA") return;
    if (cobroDeshabilitado) return;
    e.preventDefault();
    handlePayment();
  };

  const executePayment = async () => {
    setIsSaving(true);
    try {
      // Una venta a crédito nunca se encola offline: sin conexión no se puede
      // validar el cupo del cliente ni crear la cuenta a cobrar en el momento.
      if (esVentaCredito && !isOnline) {
        toast.error("Sin conexión: no se puede facturar a crédito");
        setIsSaving(false);
        return;
      }

      // Validar cotización si moneda != PYG
      if (monedaActiva?.moneda?.codigo !== "PYG" && cotizacionRequerida && !cotizacionActiva) {
        toast.error("Debe cargar una cotización válida para facturar en esta moneda");
        setIsSaving(false);
        return;
      }

      // Misma numeración que muestra el encabezado (ver numeracionFactura): lo
      // que ve el cajero tiene que ser exactamente lo que se emite.
      const numeracion = numeracionFactura;
      if (!numeracion) {
        toast.error(
          cajaSeleccionada?.id || sesionCaja?.caja_id
            ? "No hay numeración activa configurada para esta caja"
            : "No hay numeración disponible",
        );
        setIsSaving(false);
        return;
      }

      // Validar que los medios de pago cubran EXACTAMENTE el total (venta contado).
      // Si no se agregaron pagos manuales, se usa un pago único por el total (siempre cuadra).
      // A crédito no se cobra nada en caja, así que esta validación no corre.
      if (!esVentaCredito && pagosAgregados.length > 0) {
        const totalPagado = pagosAgregados.reduce((s, p) => s + (Number(p.monto) || 0), 0);
        const totalFactura = Number(totales.total) || 0;
        const dif = Math.round(totalPagado - totalFactura);
        if (dif < 0) {
          toast.error(`El total pagado (${fmtMoney(totalPagado)}) es menor al total de la factura (${fmtMoney(totalFactura)}).`);
          setIsSaving(false);
          return;
        }
        if (dif > 0) {
          toast.error(`El total pagado (${fmtMoney(totalPagado)}) supera el total de la factura (${fmtMoney(totalFactura)}). En contado debe ser exacto.`);
          setIsSaving(false);
          return;
        }
      }

      // Condición de operación según la condición de pago elegida (Contado=1, Crédito=2)
      const codCondicionVenta = condicionPagoActiva?.cod_condicion_venta || 1;
      const esCreditoEnCuotas = esVentaCredito && condicionPagoActiva?.cod_tipo_credito === 2;
      const condicionOperacion = referencialesData?.condicion_operacion?.find((c) => c.codigo === codCondicionVenta);

      // Obtener forma_procesamiento_pago "Otro" de referenciales (código 9)
      const formaProcesamiento = referencialesData?.forma_procesamiento_pago?.find((f) => f.descripcion?.toLowerCase() === "otro" || f.codigo === 9);

      // Construir cabecera - igual que POSAdminTemplate
      const cabecera = {
        cliente_id: clienteSeleccionado?.id || config.cliente_default_id || null,
        numeracion_id: numeracion.id,
        deposito_id: depositoPrincipal?.id || null,
        dfeemide: new Date().toISOString().replace("T", " ").substring(0, 19),
        dserienum: numeracion.serie || null,
        dcondticam: monedaActiva?.moneda?.codigo !== "PYG" ? "1" : null,
        dticam: cotizacionActiva?.cotizacion_venta || null,
        tipo_transaccion_id: referencialesData?.tipo_transaccion?.find((t) => t.codigo === 1)?.id || null,
        tipo_impuesto_id: referencialesData?.tipo_impuesto?.find((t) => t.codigo === 1)?.id || null,
        moneda_id: monedaActiva?.moneda_id || null,
        indicador_presencia_id: referencialesData?.indicador_presencia?.find((i) => i.codigo === 1)?.id || null,
        condicion_operacion_id: condicionOperacion?.id || null,
        tipo_operacion_id: clienteSeleccionado?.tipo_operacion_id || null,
        // Campos de crédito. dmonent (entrega inicial) siempre va null: a crédito
        // no se cobra nada en caja, las condiciones con cuota inicial quedan fuera.
        icondcred: esVentaCredito ? (esCreditoEnCuotas ? "2" : "1") : null,
        // dplazocre solo aplica al crédito a plazo; en cuotas SIFEN espera dcuotas.
        dplazocre: esVentaCredito && !esCreditoEnCuotas ? condicionPagoActiva?.descripcion || null : null,
        dcuotas: esVentaCredito && esCreditoEnCuotas ? cronogramaCredito.length : null,
        dmonent: null,
      };

      // Cuota única de la venta a crédito. Se manda explícita en vez de dejar
      // que el backend la infiera del texto de dplazocre (facturas.service.ts:
      // "Factura a credito sin cuotas"): así el vencimiento que queda en la
      // cuenta a cobrar es exactamente el que se calculó acá con dias_plazo.
      const cuotasFactura = esVentaCredito ? cronogramaCredito : [];

      // Calcular porcentaje de descuento global para distribuir a items
      const totalNetoSinDescGlobal = totales.subtotal || 0;
      let porcentajeDescuentoGlobal = 0;
      if (tipoDescuentoGlobal === "porcentaje") {
        porcentajeDescuentoGlobal = descuentoGlobal;
      } else if (tipoDescuentoGlobal === "monto" && totalNetoSinDescGlobal > 0) {
        porcentajeDescuentoGlobal = (descuentoGlobal / totalNetoSinDescGlobal) * 100;
      }

      // Construir items con descuento global distribuido
      const itemsFactura = items.map((item) => {
        // Calcular descuento global unitario para este item
        const descuentoGlobalUnitario = (item.precio_unitario * porcentajeDescuentoGlobal) / 100;
        // Recalcular total_operacion con descuento global
        const totalOperacionConDescGlobal = (item.precio_unitario - (item.descuento_particular_precio_unitario_item || 0) - descuentoGlobalUnitario) * item.cantidad;

        // Recalcular base gravada y liquidación IVA con el nuevo total_operacion
        // Se resuelve con el helper y no con `|| 10` / `|| 100`: la tasa 0 y la
        // proporción 0 de un exento son válidas, y ese default las convertía en
        // 10 / 100, mandando al DE un ítem con iAfecIVA=3 y dTasaIVA=10.
        const { codigo: codigoAfectacion, porcentaje: porcentajeIva, proporcion: proporcionIva } = resolverAfectacionIva(item);

        let baseGravadaRecalc = 0;
        let liquidacionIvaRecalc = 0;
        let baseGravadaExentaRecalc = 0;

        // Solo recalcular si hay descuento global
        if (porcentajeDescuentoGlobal > 0) {
          // Fórmula según NT SIFEN: base_gravada = [100 * total_operacion * proporcion_iva] / [10000 + porcentaje_iva * proporcion_iva]
          if (codigoAfectacion !== 2 && codigoAfectacion !== 3) {
            baseGravadaRecalc = (100 * totalOperacionConDescGlobal * proporcionIva) / (10000 + porcentajeIva * proporcionIva);
            liquidacionIvaRecalc = baseGravadaRecalc * (porcentajeIva / 100);
          }
          // Base exenta para gravado parcial (código 4)
          if (codigoAfectacion === 4) {
            baseGravadaExentaRecalc = (100 * totalOperacionConDescGlobal * (100 - proporcionIva)) / (10000 + porcentajeIva * proporcionIva);
          }
        } else {
          // Sin descuento global, usar valores originales
          baseGravadaRecalc = item.base_gravada || 0;
          liquidacionIvaRecalc = item.liquidacion_iva || 0;
          baseGravadaExentaRecalc = item.base_gravada_exenta || 0;
        }

        return {
          producto_id: item.producto_id,
          unidad_medida: item.cod_unidad_medida || "UNI",
          descripcion: item.descripcion,
          cantidad: item.cantidad,
          precio_unitario: item.precio_unitario,
          total_bruto: item.total_bruto_operacion_item || item.precio_unitario * item.cantidad,
          descuento: item.descuento_particular_precio_unitario_item || 0,
          porcentaje_descuento: item.porcentaje_descuento_item || 0,
          descuento_global: descuentoGlobalUnitario,
          total_operacion: totalOperacionConDescGlobal,
          total_gs: monedaActiva?.moneda?.codigo !== "PYG" ? totalOperacionConDescGlobal * (cotizacionActiva?.cotizacion_venta || 1) : 0,
          afectacion_iva: codigoAfectacion,
          porcentaje_iva: porcentajeIva,
          proporcion_iva: proporcionIva,
          base_gravada: baseGravadaRecalc,
          liquidacion_iva: liquidacionIvaRecalc,
          base_gravada_exenta: baseGravadaExentaRecalc,
        };
      });

      // Construir pagos - igual que POSAdminTemplate.
      // A crédito no entra dinero a la caja: la deuda se cobra después desde Cobros.
      const pagosFactura = esVentaCredito
        ? []
        : pagosAgregados.length > 0
          ? pagosAgregados.map((pago) => ({
              medio_pago_id: pago.medio_pago_id,
              moneda_id: pago.moneda_pago_id || pago.moneda_id || monedaActiva?.moneda_id || null,
              monto_pago: pago.monto_moneda_original || pago.monto,
              moneda_pago_id: pago.moneda_pago_id || null,
              monto_moneda_original: pago.monto_moneda_original || null,
              sesion_caja_id: sesionCaja?.id || null,
              numero_cheque: null,
              banco_emisor: null,
              numero_tarjeta: null,
              nro_autorizacion_procesadora: null,
              nombre_titular_tarjeta: null,
              tarjeta_id: null,
              forma_procesamiento_pago_id: formaProcesamiento?.id || null,
              razon_social_procesadora: null,
              ruc_procesadora: null,
              dv_procesadora: null,
              tarjeta_descripcion_otro: null,
            }))
          : [
              {
                medio_pago_id: metodoPagoSeleccionado?.id || mediosPago.find((m) => m.codigo === 1)?.id,
                moneda_id: monedaActiva?.moneda_id || null,
                monto_pago: totales.total,
                moneda_pago_id: null,
                monto_moneda_original: null,
                sesion_caja_id: sesionCaja?.id || null,
                numero_cheque: null,
                banco_emisor: null,
                numero_tarjeta: null,
                nro_autorizacion_procesadora: null,
                nombre_titular_tarjeta: null,
                tarjeta_id: null,
                forma_procesamiento_pago_id: formaProcesamiento?.id || null,
                razon_social_procesadora: null,
                ruc_procesadora: null,
                dv_procesadora: null,
                tarjeta_descripcion_otro: null,
              },
            ];

      // Calcular totales recalculados desde los items con descuento global
      const totalesRecalculados = itemsFactura.reduce(
        (acc, item) => {
          acc.totalDescuentoGlobal += item.descuento_global * item.cantidad;
          acc.totalOperacion += item.total_operacion;
          acc.totalLiquidacionIva += item.liquidacion_iva;
          acc.totalBaseGravada += item.base_gravada;

          const afectacion = parseInt(item.afectacion_iva) || 1;
          const tasaIva = parseInt(item.porcentaje_iva) || 10;

          // Exento (código 3)
          if (afectacion === 3) {
            acc.subtotalExento += item.total_operacion;
          }
          // Exonerado (código 2)
          else if (afectacion === 2) {
            acc.subtotalExonerado += item.total_operacion;
          }
          // Gravado Total (código 1) o Gravado Parcial (código 4)
          else if (afectacion === 1 || afectacion === 4) {
            if (tasaIva === 5) {
              acc.liquidacionIva5 += item.liquidacion_iva;
              acc.baseGravada5 += item.base_gravada;
              acc.subtotalIva5 += item.total_operacion;
            } else if (tasaIva === 10) {
              acc.liquidacionIva10 += item.liquidacion_iva;
              acc.baseGravada10 += item.base_gravada;
              acc.subtotalIva10 += item.total_operacion;
            }
          }

          return acc;
        },
        {
          totalDescuentoGlobal: 0,
          totalOperacion: 0,
          totalLiquidacionIva: 0,
          totalBaseGravada: 0,
          liquidacionIva5: 0,
          liquidacionIva10: 0,
          baseGravada5: 0,
          baseGravada10: 0,
          subtotalIva5: 0,
          subtotalIva10: 0,
          subtotalExento: 0,
          subtotalExonerado: 0,
        },
      );

      // Construir subtotal con valores recalculados
      const subtotal = {
        subtotal_exento: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.subtotalExento : totales.subtotal_operacion_exenta || 0,
        subtotal_exonerado: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.subtotalExonerado : totales.subtotal_operacion_exonerada || 0,
        subtotal_5: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.subtotalIva5 : totales.subtotal_operacion_iva5 || 0,
        subtotal_10: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.subtotalIva10 : totales.subtotal_operacion_iva10 || 0,
        liquidacion_iva_5: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.liquidacionIva5 : totales.total_liquidacion_iva5 || 0,
        liquidacion_iva_10: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.liquidacionIva10 : totales.total_liquidacion_iva10 || 0,
        liquidacion_redondeo_5: 0,
        liquidacion_redondeo_10: 0,
        liquidacion_iva_comision: 0,
        total_iva: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.totalLiquidacionIva : totales.total_liquidacion_iva || 0,
        total_operacion: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.totalOperacion : totales.total || 0,
        total_descuento: (totales.total_descuento_particular_item || 0) + totalesRecalculados.totalDescuentoGlobal,
        total_descuento_global_item: totalesRecalculados.totalDescuentoGlobal,
        total_descuento_particular_item: totales.total_descuento_particular_item || 0,
        total_bruto_operacion: totales.total_bruto_operacion || 0,
        total_anticipo_item: 0,
        total_anticipo_global_item: 0,
        porcentaje_descuento_total: porcentajeDescuentoGlobal,
        total_anticipo_operacion: 0,
        total_redondeo: 0,
        total_comision: 0,
        total_general_operacion: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.totalOperacion : totales.total || 0,
        total_base_gravada_5: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.baseGravada5 : totales.total_base_gravada_iva5 || 0,
        total_base_gravada_10: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.baseGravada10 : totales.total_base_gravada_iva10 || 0,
        total_base_gravada: porcentajeDescuentoGlobal > 0 ? totalesRecalculados.totalBaseGravada : totales.total_base_gravada_iva || 0,
        total_gs: monedaActiva?.moneda?.codigo !== "PYG" ? totalesRecalculados.totalOperacion * (cotizacionActiva?.cotizacion_venta || 1) : 0,
      };

      // Estructura final de la factura - igual que POSAdminTemplate
      const facturaData = {
        cabecera,
        items: itemsFactura,
        pagos: pagosFactura,
        subtotal,
        cuotas: cuotasFactura, // Vacío al contado; una cuota única a crédito
      };

      // Si estamos offline, pre-asignar numeración y generar CDC antes de encolar
      if (!isOnline) {
        try {
          const reserva = await reservarNumeroLocal(numeracion.id);
          if (!reserva) {
            toast.error("No se pudo reservar número de documento offline");
            setIsSaving(false);
            return;
          }
          const { numero, numeracion: numData } = reserva;
          const offDest = numData.punto_expedicion?.empresas_sucursales?.punto_establecimiento || "001";
          const offPunExp = numData.punto_expedicion?.punto_expedicion || "001";
          const offNumDoc = String(numero).padStart(7, "0");

          // Obtener datos empresa para CDC (empresaCtx tiene relaciones completas)
          const empData = empresaCtx || dataempresa;
          const empresaRuc = empData?.ruc || "";
          const empresaDv = empData?.dv || "0";
          const tipContCodigo = empData?.tipo_contribuyente_empresas?.codigo || 1;

          // Tipo documento electrónico (código del tipo_transaccion)
          const tipoTransCodigo = referencialesData?.tipo_transaccion?.find((t) => t.codigo === 1)?.codigo || 1;

          // Generar CDC y enlace QR
          const offCdc = generarCdc({
            iTiDE: tipoTransCodigo,
            dRucEm: empresaRuc,
            dDVEmi: empresaDv,
            dEst: offDest,
            dPunExp: offPunExp,
            dNumDoc: offNumDoc,
            iTipCont: tipContCodigo,
            dFeEmiDE: new Date(),
            iTipEmi: 1,
          });
          const offEnlaceQr = generarEnlaceQr(offCdc);

          // Enriquecer cabecera con datos pre-asignados
          facturaData.cabecera.dest = offDest;
          facturaData.cabecera.dpunexp = offPunExp;
          facturaData.cabecera.dnumdoc = offNumDoc;
          facturaData.cabecera.cdc = offCdc;
          facturaData.cabecera.enlace_qr = offEnlaceQr;
        } catch (err) {
          console.error("Error pre-asignando numeración offline:", err);
        }
      }

      console.log("Factura a enviar:", facturaData);
      const result = await createFacturaFallback(facturaData, createFactura);
      if (result.offline) {
        toast.success("¡Venta realizada con éxito!");

        // Si cayó offline por network error y no se pre-asignó CDC (isOnline estaba stale),
        // hacerlo ahora antes de imprimir
        if (!facturaData.cabecera.cdc) {
          try {
            const reserva = await reservarNumeroLocal(numeracion.id);
            if (reserva) {
              const { numero, numeracion: numData } = reserva;
              const offDest = numData.punto_expedicion?.empresas_sucursales?.punto_establecimiento || "001";
              const offPunExp = numData.punto_expedicion?.punto_expedicion || "001";
              const offNumDoc = String(numero).padStart(7, "0");

              const empData = empresaCtx || dataempresa;
              const empresaRuc = empData?.ruc || "";
              const empresaDv = empData?.dv || "0";
              const tipContCodigo = empData?.tipo_contribuyente_empresas?.codigo || 1;
              const tipoTransCodigo = referencialesData?.tipo_transaccion?.find((t) => t.codigo === 1)?.codigo || 1;

              const offCdc = generarCdc({
                iTiDE: tipoTransCodigo,
                dRucEm: empresaRuc,
                dDVEmi: empresaDv,
                dEst: offDest,
                dPunExp: offPunExp,
                dNumDoc: offNumDoc,
                iTipCont: tipContCodigo,
                dFeEmiDE: new Date(),
                iTipEmi: 1,
              });
              const offEnlaceQr = generarEnlaceQr(offCdc);

              facturaData.cabecera.dest = offDest;
              facturaData.cabecera.dpunexp = offPunExp;
              facturaData.cabecera.dnumdoc = offNumDoc;
              facturaData.cabecera.cdc = offCdc;
              facturaData.cabecera.enlace_qr = offEnlaceQr;
            }
          } catch (err) {
            console.error("Error pre-asignando numeración offline (fallback):", err);
          }
        }

        // Generar e imprimir ticket offline
        const offCab = facturaData.cabecera;
        let printToastId; // visible también en el .catch, para no dejar el toast de "enviando" colgado si falla
        if (offCab.cdc) {
          generateOfflineTicket({
            empresa: empresaCtx || (Array.isArray(dataempresa) ? null : dataempresa),
            sucursal: sucursalActiva || null,
            cliente: clienteSeleccionado,
            cabecera: offCab,
            items: facturaData.items,
            subtotal: facturaData.subtotal,
            moneda: monedaActiva?.moneda || { codigo: "PYG" },
            condicionOperacion: "Contado",
            timbrado: numeracion?.timbrado || null,
          })
            .then(async (blob) => {
              // Auto-imprimir desactivado: solo vista previa, sin intentar imprimir.
              if (!config.auto_print_ticket) {
                setTicketDialog({
                  open: true,
                  pdfUrl: URL.createObjectURL(blob),
                  facturaId: null,
                });
                return;
              }

              const printerName = config.printer_name || null;
              const paperWidth = config.printer_paper_width || 80;

              // Sin esto, si la impresora tarda unos segundos en responder el
              // cajero no tiene ninguna señal entre "cobré" y "salió el ticket"
              // y puede pensar que se colgó.
              printToastId = toast.loading("Enviando a la impresora…");
              const { method, pdfUrl } = await printTicketWithFallback({
                pdfBlob: blob,
                printerName,
                paperWidth,
              });

              // Impresión silenciosa real (QZ Tray) exitosa: no interrumpir con
              // el modal, solo confirmar y seguir con la próxima venta — igual
              // que Cobros. El modal queda como red de seguridad para cuando QZ
              // no está disponible y se cayó al fallback del navegador.
              if (method === "qz") {
                toast.success("Ticket impreso", { id: printToastId });
                barcodeInputRef.current?.focus();
                return;
              }

              toast.dismiss(printToastId);
              setTicketDialog({
                open: true,
                pdfUrl,
                facturaId: null,
              });
            })
            .catch((err) => {
              console.error("Error al imprimir ticket offline:", err);
              if (printToastId) toast.dismiss(printToastId);
              setPrintRetryDialog({
                open: true,
                error: err?.message || "Error desconocido",
                retryFn: () => {
                  setPrintRetryDialog({
                    open: false,
                    retryFn: null,
                    error: "",
                  });
                  generateOfflineTicket({
                    empresa: empresaCtx || (Array.isArray(dataempresa) ? null : dataempresa),
                    sucursal: sucursalActiva || null,
                    cliente: clienteSeleccionado,
                    cabecera: offCab,
                    items: facturaData.items,
                    subtotal: facturaData.subtotal,
                    moneda: monedaActiva?.moneda || { codigo: "PYG" },
                    condicionOperacion: "Contado",
                    timbrado: numeracion?.timbrado || null,
                  })
                    .then(async (blob) => {
                      const pName = config.printer_name || null;
                      const pWidth = config.printer_paper_width || 80;
                      const { method, pdfUrl: pu } = await printTicketWithFallback({
                        pdfBlob: blob,
                        printerName: pName,
                        paperWidth: pWidth,
                      });
                      const url = method === "qz" ? URL.createObjectURL(blob) : pu;
                      setTicketDialog({
                        open: true,
                        pdfUrl: url,
                        facturaId: null,
                      });
                    })
                    .catch((e) => {
                      console.error("Reintento de impresión falló:", e);
                      toast.error("No se pudo imprimir el ticket");
                    });
                },
              });
            });
        }
      } else {
        console.log("Factura creada sin offline:", result);
        toast.success("¡Venta realizada con éxito!");

        // Auto-imprimir ticket KUDE y mostrar diálogo de confirmación
        const facturaCreada = result?.result?.data;
        console.log("Factura creada:", facturaCreada);
        let printToastId; // visible también en el .catch, para no dejar el toast de "enviando" colgado si falla
        if (facturaCreada?.id) {
          console.log("imprimiendo ticket");
          generateKude({
            documento_id: facturaCreada.id,
            tipo_documento: 1,
            ticket: true,
            tipo_retorno: "view",
          })
            .then(async (blob) => {
              // Si el auto-imprimir está desactivado: no intentamos imprimir (evita el
              // error de impresión sin impresora), solo dejamos la vista previa del ticket
              // para imprimir manualmente desde el diálogo si el cajero lo desea.
              if (!config.auto_print_ticket) {
                setTicketDialog({
                  open: true,
                  pdfUrl: URL.createObjectURL(blob),
                  facturaId: facturaCreada.id,
                });
                return;
              }

              const printerName = config.printer_name || null;
              const paperWidth = config.printer_paper_width || 80;

              // Sin esto, si la impresora tarda unos segundos en responder el
              // cajero no tiene ninguna señal entre "cobré" y "salió el ticket"
              // y puede pensar que se colgó.
              printToastId = toast.loading("Enviando a la impresora…");
              const { method, pdfUrl } = await printTicketWithFallback({
                pdfBlob: blob,
                printerName,
                paperWidth,
              });

              // Impresión silenciosa real (QZ Tray) exitosa: no interrumpir con
              // el modal, solo confirmar y seguir con la próxima venta — igual
              // que Cobros. El modal queda como red de seguridad para cuando QZ
              // no está disponible y se cayó al fallback del navegador.
              if (method === "qz") {
                toast.success("Ticket impreso", { id: printToastId });
                barcodeInputRef.current?.focus();
                return;
              }

              toast.dismiss(printToastId);
              setTicketDialog({
                open: true,
                pdfUrl,
                facturaId: facturaCreada.id,
              });
            })
            .catch((err) => {
              console.error("Error al imprimir ticket:", err);
              if (printToastId) toast.dismiss(printToastId);
              const fId = facturaCreada.id;
              setPrintRetryDialog({
                open: true,
                error: err?.message || "Error desconocido",
                retryFn: () => {
                  setPrintRetryDialog({
                    open: false,
                    retryFn: null,
                    error: "",
                  });
                  generateKude({
                    documento_id: fId,
                    tipo_documento: 1,
                    ticket: true,
                    tipo_retorno: "view",
                  })
                    .then(async (blob) => {
                      const pName = config.printer_name || null;
                      const pWidth = config.printer_paper_width || 80;
                      const { method, pdfUrl: pu } = await printTicketWithFallback({
                        pdfBlob: blob,
                        printerName: pName,
                        paperWidth: pWidth,
                      });
                      const url = method === "qz" ? URL.createObjectURL(blob) : pu;
                      setTicketDialog({
                        open: true,
                        pdfUrl: url,
                        facturaId: fId,
                      });
                    })
                    .catch((e) => {
                      console.error("Reintento de impresión falló:", e);
                      toast.error("No se pudo imprimir el ticket");
                    });
                },
              });
            });
        }
      }
      clearCart();
      setShowPaymentDialog(false);
      setMontoPagado("");
      setMetodoPagoSeleccionado(null);
      setPagosAgregados([]);
      setMonedaPagoSeleccionada(null);
      setCotizacionPago(null);
      // Reset para la próxima venta: volver al consumidor final y limpiar el descuento global.
      setClienteSeleccionado(clienteSinNombreData || null);
      setCondicionPagoActiva(condicionContado);
      setDescuentoGlobal(0);
      setTipoDescuentoGlobal("ninguno");
      focusSearchInput();
    } catch (error) {
      console.error("Error al procesar venta:", error);
      const payload = error?.response?.data;
      const mensaje = payload?.message || "Error al procesar la venta";
      const errores = Array.isArray(payload?.errores) ? payload.errores : [];
      if (errores.length > 0) {
        toast.error(
          <div>
            <div style={{ fontWeight: 600, marginBottom: 4 }}>{mensaje}</div>
            <ul style={{ margin: 0, paddingLeft: 18 }}>
              {errores.map((e, i) => (
                <li key={i}>{e}</li>
              ))}
            </ul>
          </div>,
          { autoClose: 10000 },
        );
      } else {
        toast.error(mensaje);
      }
    } finally {
      setIsSaving(false);
      focusSearchInput();
    }
  };

  // Focus en campo monto recibido al abrir el diálogo de pago
  useEffect(() => {
    if (showPaymentDialog) {
      const timer = setTimeout(() => {
        montoInputRef.current?.focus();
      }, 120);
      return () => clearTimeout(timer);
    }
  }, [showPaymentDialog]);

  // Cargar transacciones recientes (solo de la caja activa)
  const loadTransacciones = useCallback(
    async (page = 1) => {
      if (!navigator.onLine) {
        toast.error("Transacciones no disponible sin conexión");
        return;
      }
      setTransaccionesLoading(true);
      try {
        const now = new Date();
        const hoy = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-${String(now.getDate()).padStart(2, "0")}`;
        const result = await getFacturas({
          page,
          limit: 10,
          fechaDesde: hoy,
          sesionCajaId: sesionCaja?.id,
        });
        const items = result?.items || result?.data || result?.facturas || [];
        setTransacciones(Array.isArray(items) ? items : []);
        setTransaccionesTotalPages(result?.lastPage || result?.totalPages || result?.meta?.totalPages || 1);
        setTransaccionesPage(page);
      } catch (error) {
        console.error("Error cargando transacciones:", error);
        toast.error("Error al cargar transacciones");
        setTransacciones([]);
      } finally {
        setTransaccionesLoading(false);
      }
    },
    [sesionCaja?.id],
  );

  // Abrir diálogo de transacciones
  const openTransaccionesDialog = useCallback(() => {
    setShowTransaccionesDialog(true);
    loadTransacciones(1);
  }, [loadTransacciones]);

  // Reimprimir ticket de una factura
  const handleReprintTicket = useCallback(
    async (facturaId) => {
      setReprintingId(facturaId);
      try {
        const blob = await generateKude({
          documento_id: facturaId,
          tipo_documento: 1,
          ticket: true,
          tipo_retorno: "view",
        });
        const printerName = config.printer_name || null;
        const paperWidth = config.printer_paper_width || 80;

        const { method, pdfUrl } = await printTicketWithFallback({
          pdfBlob: blob,
          printerName,
          paperWidth,
        });

        const url = method === "qz" ? URL.createObjectURL(blob) : pdfUrl;
        setTicketDialog({ open: true, pdfUrl: url, facturaId });
        toast.success("Ticket reimpreso");
      } catch (error) {
        console.error("Error reimprimiendo ticket:", error);
        toast.error("Error al reimprimir el ticket");
      } finally {
        setReprintingId(null);
      }
    },
    [config.printer_name, config.printer_paper_width],
  );

  // Ver ticket (PDF preview) de una factura
  const handleViewTicket = useCallback(async (facturaId) => {
    setReprintingId(facturaId);
    try {
      const blob = await generateKude({
        documento_id: facturaId,
        tipo_documento: 1,
        ticket: true,
        tipo_retorno: "view",
      });
      const url = URL.createObjectURL(blob);
      setTicketDialog({ open: true, pdfUrl: url, facturaId });
    } catch (error) {
      console.error("Error al ver ticket:", error);
      toast.error("Error al generar vista previa del ticket");
    } finally {
      setReprintingId(null);
    }
  }, []);

  // Helper: enfocar el buscador de productos (lector de código de barras)
  const focusSearchInput = useCallback(() => {
    setTimeout(() => {
      barcodeInputRef.current?.focus();
    }, 100);
  }, []);

  // Focus en buscador al montar el componente (carga inicial / F5)
  useEffect(() => {
    focusSearchInput();
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  // Focus en buscador al cerrar el dialog de pago
  useEffect(() => {
    if (!showPaymentDialog) {
      focusSearchInput();
    }
  }, [showPaymentDialog, focusSearchInput]);

  // Focus en buscador al cerrar el dialog de cliente
  useEffect(() => {
    if (!showClienteDialog) {
      focusSearchInput();
    }
  }, [showClienteDialog, focusSearchInput]);

  // Info de moneda de pago activa (resuelve moneda, cotización, equivalencias)
  const pagoInfo = useMemo(() => {
    const PRECISION = 8; // decimales internos para cálculos

    const codigoFactura = monedaActiva?.moneda?.codigo || "PYG";
    const isoFactura = monedaActiva?.moneda?.iso || "PY";
    const simboloFactura = monedaActiva?.moneda?.simbolo || "₲";
    // El guaraní no tiene centavos. No se lee `decimales_estandar` para PYG
    // porque el registro de moneda trae 2, y con eso el monto exacto salía
    // como 100000.27 y la máscara entera lo convertía en 10.000.027.
    const decimalesFactura = codigoFactura === "PYG" ? 0 : (monedaActiva?.moneda?.decimales_estandar ?? 2);

    const codigoPago = monedaPagoSeleccionada?.moneda?.codigo || monedaPagoSeleccionada?.codigo || codigoFactura;
    const isoPago = monedaPagoSeleccionada?.moneda?.iso || monedaPagoSeleccionada?.iso || isoFactura;
    const simboloPago = monedaPagoSeleccionada?.moneda?.simbolo || monedaPagoSeleccionada?.simbolo || simboloFactura;
    const decimalesPago = codigoPago === "PYG" ? 0 : (monedaPagoSeleccionada?.moneda?.decimales_estandar ?? 2);

    const esDiferenteMoneda = codigoPago !== codigoFactura;
    const esPYGPago = codigoPago === "PYG";

    const cotVenta = Number(cotizacionPago?.cotizacion_venta || cotizacionPago?.data?.cotizacion_venta || 0);

    // Redondeo con precisión configurable
    const redondear = (valor, dec = PRECISION) => {
      const f = Math.pow(10, dec);
      return Math.round(valor * f) / f;
    };

    // Ceil inteligente: si el valor ya está muy cerca de un entero en la
    // precisión pedida, lo trata como exacto (evita ceil por error de FP)
    const ceilPrecision = (valor, dec) => {
      const f = Math.pow(10, dec);
      const shifted = valor * f;
      if (Math.abs(shifted - Math.round(shifted)) < 1e-6) {
        return Math.round(shifted) / f;
      }
      return Math.ceil(shifted) / f;
    };

    // Total ya pagado en moneda factura (base)
    const totalPagadoBase = pagosAgregados.reduce((sum, p) => sum + p.monto, 0);
    const restanteBase = redondear(Math.max(0, (totales.total || 0) - totalPagadoBase), decimalesFactura);
    const pagoCompleto = totalPagadoBase >= (totales.total || 0);

    // Convertir montos base → moneda de pago (precisión interna 8 dec)
    const convertirAMonedaPago = (montoBase) => {
      if (!esDiferenteMoneda || cotVenta <= 0) return montoBase;
      if (codigoFactura === "PYG") {
        return redondear(montoBase / cotVenta);
      }
      return redondear(montoBase * cotVenta);
    };

    // Convertir moneda de pago → base (precisión interna 8 dec)
    const convertirABase = (montoPago) => {
      if (!esDiferenteMoneda || cotVenta <= 0) return montoPago;
      if (codigoFactura === "PYG") {
        return redondear(montoPago * cotVenta);
      }
      return redondear(montoPago / cotVenta);
    };

    // Tolerancia de redondeo: 1 unidad mínima de moneda pago expresada en base
    // Ej: 1 centavo USD = 0.01 USD × 6900 = 69 PYG
    const toleranciaRedondeo = esDiferenteMoneda && cotVenta > 0 ? Math.abs(convertirABase(1 / Math.pow(10, decimalesPago))) : 0;

    const totalEnMonedaPago = convertirAMonedaPago(totales.total || 0);
    const restanteEnMonedaPago = convertirAMonedaPago(restanteBase);

    // Monto exacto: ceil a decimales de la moneda de pago para cubrir el total
    const montoExactoEnMonedaPago = restanteBase > 0 ? ceilPrecision(restanteEnMonedaPago, decimalesPago) : 0;

    // Denominaciones rápidas según moneda de pago
    const quickAmounts = esPYGPago ? [1000, 5000, 10000, 20000, 50000, 100000] : [5, 10, 20, 50, 100, 500];

    return {
      codigoFactura,
      isoFactura,
      simboloFactura,
      decimalesFactura,
      codigoPago,
      isoPago,
      simboloPago,
      decimalesPago,
      esDiferenteMoneda,
      esPYGPago,
      cotVenta,
      totalPagadoBase,
      restanteBase,
      restanteEnMonedaPago,
      totalEnMonedaPago,
      montoExactoEnMonedaPago,
      pagoCompleto,
      quickAmounts,
      toleranciaRedondeo,
      convertirABase,
      convertirAMonedaPago,
      ceilPrecision,
      redondear,
    };
  }, [monedaActiva, monedaPagoSeleccionada, cotizacionPago, pagosAgregados, totales.total]);

  // Calcular vuelto (siempre en moneda de factura)
  const vuelto = useMemo(() => {
    const montoIngresado = pagoInfo.esPYGPago ? parseInt(montoPagado) || 0 : parseFloat(montoPagado) || 0;
    const montoEnBase = pagoInfo.convertirABase(montoIngresado);
    const totalConPagos = montoEnBase + pagoInfo.totalPagadoBase;
    const diff = totalConPagos - (totales.total || 0);
    // Absorber excedentes que son puro artefacto de redondeo de moneda cruzada
    if (pagoInfo.esDiferenteMoneda && diff > 0 && diff <= pagoInfo.toleranciaRedondeo) {
      return 0;
    }
    return pagoInfo.redondear(Math.max(0, diff), pagoInfo.decimalesFactura);
  }, [montoPagado, totales.total, pagoInfo]);

  /**
   * Detectar si el input viene de un lector de códigos de barras.
   * Los lectores envían caracteres muy rápido (~10-50ms entre cada uno)
   * y terminan con Enter. La escritura manual es mucho más lenta.
   */
  const handleSearchInputChange = useCallback(
    (e) => {
      const value = e.target.value;
      setSearchQuery(value);

      if (!config.barcode_auto_add) return;

      const now = Date.now();
      keystrokesRef.current.push(now);

      // Limpiar timer anterior
      if (barcodeTimerRef.current) clearTimeout(barcodeTimerRef.current);

      // Después de un periodo sin input, resetear el tracking
      barcodeTimerRef.current = setTimeout(() => {
        keystrokesRef.current = [];
        isBarcodeInputRef.current = false;
      }, 200);

      // Evaluar si es input de barcode: >= 4 chars con intervalos < umbral
      if (keystrokesRef.current.length >= 4) {
        const strokes = keystrokesRef.current;
        const intervals = [];
        for (let i = 1; i < strokes.length; i++) {
          intervals.push(strokes[i] - strokes[i - 1]);
        }
        const avgInterval = intervals.reduce((a, b) => a + b, 0) / intervals.length;
        isBarcodeInputRef.current = avgInterval < BARCODE_SCAN_THRESHOLD_MS;
      }
    },
    [config.barcode_auto_add],
  );

  // Manejar tecla Enter en búsqueda
  const handleSearchKeyDown = useCallback(
    async (e) => {
      if (e.key !== "Enter") return;

      const wasBarcodeScan = isBarcodeInputRef.current;

      // Resetear tracking de barcode
      keystrokesRef.current = [];
      isBarcodeInputRef.current = false;
      if (barcodeTimerRef.current) clearTimeout(barcodeTimerRef.current);

      // Si es scan de barcode con auto-add habilitado: buscar y agregar directo
      if (wasBarcodeScan && config.barcode_auto_add && searchQuery.trim()) {
        e.preventDefault();
        try {
          const depositoId = depositoPrincipal?.id || null;
          const results = await searchProductosFallback(searchQuery.trim(), 5, (q, limit) => buscarProductos(q, limit, depositoId, null, false, null, rubroPOS), rubroPOS);
          const found = Array.isArray(results) ? results : [];

          if (found.length === 1) {
            addToCart(found[0]);
            setSearchQuery("");
            setSearchResults([]);
          } else if (found.length > 1) {
            // Múltiples resultados: mostrar para selección manual
            setSearchResults(found);
            toast.info(`${found.length} productos encontrados. Seleccione uno.`, { id: "scan-resultado" });
          } else {
            toast.warning("Producto no encontrado", { id: "scan-resultado" });
            setSearchQuery("");
          }
        } catch (error) {
          console.error("Error en búsqueda por barcode:", error);
        }
        return;
      }

      // Escritura manual + Enter: agregar primer resultado si hay
      if (searchResults.length > 0) {
        addToCart(searchResults[0]);
        setSearchQuery("");
        setSearchResults([]);
      }
    },
    [searchQuery, searchResults, config.barcode_auto_add, depositoPrincipal, searchProductosFallback, addToCart, rubroPOS],
  );

  // Obtener información de la sesión de caja
  const cajaInfo = numeracionFactura?.punto_expedicion;
  // Si no hay sesión de caja activa, mostrar pantalla de apertura
  if (!sesionCaja) {
    return (
      <Container>
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            justifyContent: "center",
            minHeight: "calc(100vh - 100px)",
            p: 4,
          }}
        >
          <Icon icon="lucide:lock-open" width={64} style={{ color: "#f88533" }} />
          <Typography variant="h4" fontWeight={700} sx={{ mt: 3 }}>
            Apertura de Caja
          </Typography>
          <Typography variant="body1" color="text.secondary" sx={{ mt: 1, mb: 4 }}>
            Selecciona una caja disponible para comenzar a operar
          </Typography>

          {cajasDisponibles.length === 0 ? (
            <Box sx={{ textAlign: "center" }}>
              <Icon icon="lucide:alert-circle" width={48} style={{ color: "#f88533" }} />
              <Typography variant="h6" sx={{ mt: 2, fontWeight: 600 }}>
                Sin cajas disponibles
              </Typography>
              <Typography variant="body2" color="text.secondary" sx={{ mt: 1, maxWidth: 400 }}>
                No hay cajas disponibles para abrir. Verifica que existan cajas configuradas en tu sucursal.
              </Typography>
              <Button variant="outlined" startIcon={<Icon icon="lucide:settings" />} sx={{ mt: 3 }} onClick={() => (window.location.href = "/configuracion")}>
                Ir a Configuración
              </Button>
            </Box>
          ) : (
            <>
              <Grid container spacing={3} sx={{ maxWidth: 800, justifyContent: "center" }}>
                {cajasDisponibles.map((caja) => {
                  const isSelected = cajaSeleccionada?.id === caja.id;
                  const enUso = caja.tiene_sesion_abierta;

                  return (
                    <Grid item xs={12} sm={6} md={4} key={caja.id}>
                      <Paper
                        elevation={isSelected ? 8 : 2}
                        sx={{
                          p: 3,
                          cursor: enUso ? "not-allowed" : "pointer",
                          opacity: enUso ? 0.6 : 1,
                          border: isSelected ? "2px solid #f88533" : "2px solid transparent",
                          transition: "all 0.2s",
                          "&:hover": {
                            transform: enUso ? "none" : "translateY(-4px)",
                            boxShadow: enUso ? 2 : 8,
                          },
                        }}
                        onClick={() => !enUso && setCajaSeleccionada(caja)}
                      >
                        <Box
                          sx={{
                            display: "flex",
                            alignItems: "flex-start",
                            justifyContent: "space-between",
                            mb: 2,
                          }}
                        >
                          <Box
                            sx={{
                              width: 48,
                              height: 48,
                              borderRadius: 2,
                              display: "flex",
                              alignItems: "center",
                              justifyContent: "center",
                              bgcolor: enUso ? "error.main" : isSelected ? "warning.main" : "success.main",
                            }}
                          >
                            <Icon icon={enUso ? "lucide:lock" : "lucide:unlock"} width={24} color="white" />
                          </Box>
                          <Chip label={enUso ? "En uso" : "Disponible"} size="small" color={enUso ? "error" : "success"} variant={enUso ? "filled" : "outlined"} />
                        </Box>
                        <Typography variant="h6" fontWeight={600}>
                          {caja.descripcion || "Caja"}
                        </Typography>
                        <Box
                          sx={{
                            display: "flex",
                            alignItems: "center",
                            gap: 1,
                            mt: 1,
                          }}
                        >
                          <Icon icon="lucide:store" width={16} color="#888" />
                          <Typography variant="body2" color="text.secondary">
                            {/* El backend devuelve la relación como
                                `empresas_sucursales`; leer `caja.sucursal`
                                daba siempre undefined y caía al fallback,
                                etiquetando toda caja con la sucursal activa
                                aunque fuera de otra. */}
                            {caja.empresas_sucursales?.descripcion || caja.sucursal?.descripcion || "Sin sucursal"}
                          </Typography>
                        </Box>
                      </Paper>
                    </Grid>
                  );
                })}
              </Grid>

              {cajaSeleccionada && (
                <Paper sx={{ mt: 4, p: 3, maxWidth: 400, width: "100%" }}>
                  <Typography variant="subtitle1" fontWeight={600} gutterBottom>
                    Monto de Apertura
                  </Typography>
                  <TextField
                    fullWidth
                    placeholder="0"
                    value={montoApertura ? parseInt(montoApertura.replace(/\D/g, "") || 0).toLocaleString("es-PY") : ""}
                    onChange={(e) => {
                      const numericValue = e.target.value.replace(/\D/g, "");
                      setMontoApertura(numericValue);
                    }}
                    InputProps={{
                      startAdornment: <Typography sx={{ mr: 1, color: "text.secondary" }}>Gs.</Typography>,
                    }}
                    sx={{ mb: 2 }}
                  />
                  <Button
                    variant="contained"
                    fullWidth
                    size="large"
                    disabled={isLoadingCaja}
                    onClick={handleAbrirCaja}
                    sx={{
                      bgcolor: "#f88533",
                      "&:hover": { bgcolor: "#e07020" },
                    }}
                    startIcon={<Icon icon="lucide:unlock" />}
                  >
                    {isLoadingCaja ? "Abriendo..." : "Abrir Caja"}
                  </Button>
                </Paper>
              )}
            </>
          )}
        </Box>
      </Container>
    );
  }

  return (
    <Container>
      {/* Alerta de cotización pendiente */}
      {showCotizacionAlert && cotizacionPendiente && (
        <CotizacionAlert>
          <div className="alert-content">
            <Icon icon="mdi:alert-circle" width="24" />
            <div className="alert-text">
              <strong>No hay cotización del día para {monedaActiva?.moneda?.descripcion || monedaActiva?.moneda?.codigo}</strong>
              <p>
                Última cotización disponible: <strong>{new Date(cotizacionPendiente.fecha).toLocaleDateString("es-PY")}</strong>
                <br />
                Compra: <strong>{cotizacionPendiente.cotizacion_compra?.toLocaleString("es-PY")}</strong> | Venta: <strong>{cotizacionPendiente.cotizacion_venta?.toLocaleString("es-PY")}</strong>
              </p>
            </div>
          </div>
          <div className="alert-actions">
            <button onClick={handleAceptarCotizacion} className="accept">
              <Icon icon="mdi:check" /> Usar esta cotización
            </button>
            <button onClick={handleRechazarCotizacion} className="reject">
              <Icon icon="mdi:close" /> Cargar nueva
            </button>
          </div>
        </CotizacionAlert>
      )}

      {/* Barra superior de información */}
      <TopInfoBar>
        <div className="info-left">
          <IconButton size="small" onClick={(e) => setAdminMenuAnchor(e.currentTarget)} sx={{ color: "text.secondary", mr: 0.5 }} title="Menú">
            <Icon icon="mdi:menu" width={22} />
          </IconButton>
          <Menu
            anchorEl={adminMenuAnchor}
            open={adminMenuOpen}
            onClose={() => setAdminMenuAnchor(null)}
            anchorOrigin={{ vertical: "bottom", horizontal: "left" }}
            transformOrigin={{ vertical: "top", horizontal: "left" }}
            slotProps={{
              paper: {
                sx: { minWidth: 220, mt: 0.5 },
              },
            }}
          >
            <MenuItem
              onClick={() => {
                setAdminMenuAnchor(null);
                alternarPantallaCompleta();
              }}
            >
              <ListItemIcon>
                <Icon icon={enPantallaCompleta ? "mdi:view-dashboard-outline" : "mdi:fullscreen"} width={20} />
              </ListItemIcon>
              <ListItemText>{enPantallaCompleta ? "Panel de Administración" : "Pantalla completa"}</ListItemText>
            </MenuItem>
            <Divider />
            <MenuItem
              onClick={() => {
                setAdminMenuAnchor(null);
                setShowConfigDialog(true);
              }}
            >
              <ListItemIcon>
                <Icon icon="mdi:cog-outline" width={20} />
              </ListItemIcon>
              <ListItemText>Configuración POS</ListItemText>
            </MenuItem>
            <MenuItem
              disabled={!isOnline}
              onClick={() => {
                setAdminMenuAnchor(null);
                openTransaccionesDialog();
              }}
            >
              <ListItemIcon>
                <Icon icon="mdi:receipt-text-outline" width={20} />
              </ListItemIcon>
              <ListItemText>Transacciones{!isOnline ? " (sin conexión)" : ""}</ListItemText>
            </MenuItem>
            {sesionCaja && (
              <MenuItem
                onClick={() => {
                  setAdminMenuAnchor(null);
                  setShowCerrarCajaDialog(true);
                }}
              >
                <ListItemIcon>
                  <Icon icon="mdi:cash-lock" width={20} />
                </ListItemIcon>
                <ListItemText>Cerrar Caja</ListItemText>
              </MenuItem>
            )}
            <Divider />
            <MenuItem
              onClick={() => {
                setAdminMenuAnchor(null);
                cerrarSesion();
              }}
              sx={{ color: "error.main" }}
            >
              <ListItemIcon sx={{ color: "error.main" }}>
                <Icon icon="mdi:logout" width={20} />
              </ListItemIcon>
              <ListItemText>Cerrar Sesión</ListItemText>
            </MenuItem>
          </Menu>
          <div className="info-item">
            <Icon icon="mdi:store" />
            <span>{sucursalActiva?.descripcion || "Sin sucursal"}</span>
          </div>
          <div className="info-item">
            <Icon icon="mdi:cash-register" />
            <span>Caja: {cajaInfo?.cajas?.descripcion || "N/A"}</span>
          </div>
          <div className="info-item">
            <Icon icon="mdi:receipt" />
            <span>PE: {cajaInfo?.punto_expedicion || "001"}</span>
          </div>
          <div className="info-item">
            <Icon icon="mdi:currency-usd" />
            <span>{monedaActiva?.moneda?.codigo || "PYG"}</span>
          </div>
          <IconButton size="small" onClick={alternarPantallaCompleta} sx={{ ml: 1, color: "text.secondary" }} title={enPantallaCompleta ? "Minimizar (mostrar menú lateral)" : "Pantalla completa"}>
            <Icon icon={enPantallaCompleta ? "mdi:fullscreen-exit" : "mdi:fullscreen"} width={20} />
          </IconButton>
          <IconButton size="small" onClick={() => setShowConfigDialog(true)} sx={{ ml: 1, color: "text.secondary" }} title="Configuración">
            <Icon icon="mdi:cog" width={20} />
          </IconButton>
          <SyncStatusBar compact />
        </div>
        <div className="info-right">
          <div className="info-item user">
            <Icon icon="mdi:account-circle" />
            <span>{user?.nombres || user?.email || "Usuario"}</span>
          </div>
          <div className="info-item time">
            <Icon icon="mdi:clock-outline" />
            <span>
              {new Date().toLocaleTimeString("es-PY", {
                hour: "2-digit",
                minute: "2-digit",
              })}
            </span>
          </div>
        </div>
      </TopInfoBar>

      <MainContent>
        {/* Panel izquierdo - Productos */}
        <LeftPanel>
          {/* Barra de búsqueda */}
          <SearchBar>
            <Icon icon="mdi:barcode-scan" className="icon" />
            <input ref={barcodeInputRef} type="text" placeholder="Escanear código o buscar producto..." value={searchQuery} onChange={handleSearchInputChange} onKeyDown={handleSearchKeyDown} />
            {searchQuery && (
              <IconButton size="small" onClick={() => setSearchQuery("")}>
                <Icon icon="mdi:close" />
              </IconButton>
            )}
          </SearchBar>

          {/* Confirmación fija del último producto agregado */}
          {ultimoAgregado && searchResults.length === 0 && (
            <UltimoAgregadoStrip>
              <Icon icon="mdi:check-circle" className="ok" />
              <span className="name">{ultimoAgregado.descripcion}</span>
              {ultimoAgregado.cantidad > 1 && <span className="cantidad">×{ultimoAgregado.cantidad}</span>}
              {ultimoAgregado.oferta && <span className="oferta">🏷️ {ultimoAgregado.oferta}</span>}
              <span className="precio">{FormatearNumeroDinero(ultimoAgregado.precio || 0, monedaActiva?.moneda?.codigo || dataempresa?.currency || "PYG", monedaActiva?.moneda?.iso || "PY")}</span>
              <IconButton size="small" onClick={() => setUltimoAgregadoId(null)} title="Ocultar">
                <Icon icon="mdi:close" width={16} />
              </IconButton>
            </UltimoAgregadoStrip>
          )}

          {/* Resultados de búsqueda */}
          {searchResults.length > 0 && (
            <SearchResultsPanel>
              <SearchResultsHeader>
                <Icon icon="mdi:check-circle" />
                <span>
                  {searchResults.length} {searchResults.length === 1 ? "resultado encontrado" : "resultados encontrados"}
                </span>
                <kbd>Enter</kbd>
                <span className="hint">agrega el primero</span>
              </SearchResultsHeader>
              <SearchResults>
                {searchResults.slice(0, 8).map((producto, index) => (
                  <ProductCard key={producto.id} $primary={index === 0} onClick={() => addToCart(producto)}>
                    {producto.url ? (
                      <div className="product-image">
                        <img src={producto.url} alt={producto.descripcion} loading="lazy" decoding="async" />
                      </div>
                    ) : (
                      <div className="product-image placeholder">
                        <Icon icon="mdi:package-variant" width={24} />
                      </div>
                    )}
                    <div className="info">
                      <span className="name">{producto.descripcion || producto.nombre}</span>
                      <span className="code">{producto.cod_producto || producto.codigo_barra}</span>
                    </div>
                    <div className="details">
                      <span className="price">
                        {FormatearNumeroDinero(producto.precio || producto.p_venta || 0, monedaActiva?.moneda?.codigo || dataempresa?.currency || "PYG", monedaActiva?.moneda?.iso || "PY")}
                      </span>
                      {config.show_product_stock && producto.maneja_inventario && (
                        <span className={`stock ${(producto.stock_disponible || 0) <= 0 ? "out" : ""}`}>Stock: {producto.stock_disponible || 0}</span>
                      )}
                    </div>
                    {index === 0 && (
                      <span className="enter-chip">
                        <Icon icon="mdi:keyboard-return" /> Enter
                      </span>
                    )}
                  </ProductCard>
                ))}
              </SearchResults>
            </SearchResultsPanel>
          )}

          {/* Grid de productos */}
          <ProductsGrid $tileSize={TILE_SIZES[config.card_size]?.size || 120} $columns={config.retail_grid_columns || 0}>
            {productos.length === 0 && gridPendiente ? (
              <EmptyProducts>
                <Icon icon="mdi:loading" className="spin" />
                <span>Cargando productos…</span>
              </EmptyProducts>
            ) : productos.length === 0 ? (
              <EmptyProducts>
                <Icon icon="mdi:package-variant" />
                <span>No hay productos</span>
                <small>Busca un producto o selecciona una categoría</small>
              </EmptyProducts>
            ) : (
              productos.map((producto) => (
                <ProductTile key={producto.id} onClick={() => addToCart(producto)} $accent={getProductAccent(producto)} $hasImage={!!producto.url}>
                  {producto.url && (
                    <div className="tile-image">
                      {/* Lazy: con scroll infinito el grid puede acumular cientos
                          de tiles; sin esto el navegador dispara una request por
                          imagen apenas se monta la tanda, aunque estén fuera de
                          pantalla. */}
                      <img src={producto.url} alt={producto.descripcion} loading="lazy" decoding="async" />
                    </div>
                  )}
                  <div className="tile-header">
                    <span className="code">{producto.cod_producto || producto.codigo_barra?.slice(-4) || ""}</span>
                  </div>
                  <div className="tile-content">
                    <span className="name">{producto.descripcion || producto.nombre}</span>
                  </div>
                  <div className="tile-footer">
                    <span className="price">
                      {FormatearNumeroDinero(producto.precio || producto.p_venta || 0, monedaActiva?.moneda?.codigo || dataempresa?.currency || "PYG", monedaActiva?.moneda?.iso || "PY")}
                    </span>
                    {config.show_product_stock && producto.maneja_inventario && <span className={`stock ${(producto.stock_disponible || 0) <= 0 ? "out" : ""}`}>{producto.stock_disponible || 0}</span>}
                  </div>
                </ProductTile>
              ))
            )}
            {/* Sentinela: al entrar en viewport dispara la carga de la siguiente tanda */}
            {hasNextPage && (
              <LoadMoreSentinel ref={loadMoreRef}>
                {isFetchingNextPage && (
                  <>
                    <Icon icon="mdi:loading" className="spin" />
                    <span>Cargando más productos…</span>
                  </>
                )}
              </LoadMoreSentinel>
            )}
          </ProductsGrid>

          {/* Categorías en la parte inferior */}
          <CategoriesBar>
            <CategoryButton $active={!categoriaActiva} onClick={() => setCategoriaActiva(null)}>
              <Icon icon="mdi:apps" />
              <span>Todos</span>
            </CategoryButton>
            {categorias.map((cat) => (
              <CategoryButton key={cat.id} $active={categoriaActiva?.id === cat.id} onClick={() => setCategoriaActiva(cat)} $color={cat.color}>
                <span>{cat.descripcion || cat.nombre}</span>
              </CategoryButton>
            ))}
          </CategoriesBar>
        </LeftPanel>

        {/* Panel derecho - Carrito */}
        <RightPanel>
          <CartHeader>
            <Box
              sx={{
                display: "flex",
                alignItems: "center",
                justifyContent: "space-between",
                width: "100%",
              }}
            >
              <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
                <Typography variant="h6" fontWeight={700}>
                  <Icon icon="mdi:cart" style={{ marginRight: 8 }} />
                  Carrito
                </Typography>
                <Chip label={`${items.length} items`} size="small" />
                {/* Vaciar carrito: vive acá, junto al contador de ítems, que es
                    donde se lo busca. Antes era un IconButton al lado de COBRAR
                    (fullWidth), que lo empujaba fuera de la vista. */}
                {items.length > 0 && (
                  <Button size="small" color="error" startIcon={<Icon icon="mdi:delete-sweep" width={18} />} onClick={clearCart} sx={{ ml: 0.5, textTransform: "none" }}>
                    Vaciar
                  </Button>
                )}
              </Box>
              {/* Botón de Caja */}
              <Button
                variant={sesionCaja ? "contained" : "outlined"}
                color={sesionCaja ? "success" : "warning"}
                size="small"
                startIcon={<Icon icon={sesionCaja ? "mdi:cash-register" : "mdi:cash-lock"} />}
                onClick={() => {
                  if (sesionCaja) {
                    setShowCerrarCajaDialog(true);
                  } else {
                    setShowAbrirCajaDialog(true);
                  }
                }}
                sx={{ minWidth: 100 }}
              >
                {sesionCaja ? "Cerrar Caja" : "Abrir Caja"}
              </Button>
            </Box>
          </CartHeader>

          {/* Sección de cliente */}
          <ClienteSection onClick={() => setShowClienteDialog(true)}>
            <Icon icon="mdi:account" className="icon" />
            <div className="info">
              {clienteSeleccionado ? (
                <>
                  <span className="name">{clienteSeleccionado.personas?.razon_social || clienteSeleccionado.razon_social || "Sin Nombre"}</span>
                  <span className="ruc">
                    {clienteSeleccionado.personas?.ruc || clienteSeleccionado.ruc || ""}-{clienteSeleccionado.personas?.dv || clienteSeleccionado.dv || ""}
                  </span>
                  {(clienteSeleccionado.lista_precios || clienteSeleccionado.tipo_cliente || creditoInfo) && (
                    <div className="badges">
                      {clienteSeleccionado.lista_precios && (
                        <span className="badge blue">
                          <Icon icon="mdi:tag-outline" width={12} />
                          {clienteSeleccionado.lista_precios.nombre}
                        </span>
                      )}
                      {clienteSeleccionado.tipo_cliente && (
                        <span className="badge orange">
                          <Icon icon="mdi:account-group-outline" width={12} />
                          {clienteSeleccionado.tipo_cliente.charAt(0).toUpperCase() + clienteSeleccionado.tipo_cliente.slice(1)}
                        </span>
                      )}
                      {clienteSeleccionado.zona && (
                        <span className="badge purple">
                          <Icon icon="mdi:map-marker-outline" width={12} />
                          {clienteSeleccionado.zona}
                        </span>
                      )}
                      {creditoInfo && (
                        <span
                          className={`badge ${creditoInfo.bloqueado ? "red" : creditoInfo.saldo_pendiente > creditoInfo.limite_credito * 0.8 ? "orange" : "green"}`}
                          title={`Límite: ${textoTope(creditoInfo.limite_credito)} | Saldo: ${fmtCredito(creditoInfo.saldo_pendiente || 0)}`}
                        >
                          <Icon icon={creditoInfo.bloqueado ? "mdi:credit-card-off-outline" : "mdi:credit-card-outline"} width={12} />
                          {creditoInfo.bloqueado ? "Crédito bloqueado" : `Saldo: ${fmtCredito(creditoInfo.saldo_pendiente || 0)}`}
                        </span>
                      )}
                    </div>
                  )}
                </>
              ) : (
                <>
                  <span className="name">Sin Nombre</span>
                  <span className="ruc">Venta Contado (Ocasional)</span>
                </>
              )}
            </div>
            <Icon icon="mdi:chevron-right" className="arrow" />
          </ClienteSection>

          <CartItems>
            {items.length === 0 ? (
              <EmptyCart>
                <Icon icon="mdi:cart-outline" />
                <span>Carrito vacío</span>
                <small>Escanea o busca productos</small>
              </EmptyCart>
            ) : (
              items.map((item, index) => {
                const precioUnitario = item.precio_unitario || item.precio_venta || 0;
                const descuentoUnitario = item.descuento_particular_precio_unitario_item || 0;
                const descuentoTotal = descuentoUnitario * item.cantidad;
                const tieneDescuento = descuentoUnitario > 0 || item.porcentaje_descuento_item > 0;
                const totalItem = item.total_operacion_item || item.cantidad * precioUnitario;

                return (
                  <CartItem key={`${item.id}-${index}`}>
                    {/* Fila 1: Nombre del producto y descuento */}
                    <div className="product-name">
                      <span className="name">{item.descripcion}</span>
                      <Box
                        sx={{
                          display: "flex",
                          gap: 0.5,
                          ml: 1,
                          flexWrap: "wrap",
                        }}
                      >
                        {item._oferta && <Chip icon={<Icon icon="mdi:tag-outline" width={14} />} label={item._oferta.nombre_oferta} size="small" color="success" sx={{ height: 20, fontSize: 11 }} />}
                        {tieneDescuento && (
                          <>
                            {item.porcentaje_descuento_item > 0 && <Chip label={`-${item.porcentaje_descuento_item}%`} size="small" color="warning" sx={{ height: 20, fontSize: 11 }} />}
                            <Chip label={`-${fmtMoney(descuentoTotal)}`} size="small" color="error" sx={{ height: 20, fontSize: 11 }} />
                          </>
                        )}
                      </Box>
                    </div>

                    {/* Fila 2: Precio unitario */}
                    <div className="price-row">
                      <span className="unit-price">
                        P.Unit: {fmtMoney(precioUnitario)}
                        {item._oferta && (
                          <span
                            style={{
                              textDecoration: "line-through",
                              color: "#999",
                              fontSize: "0.85em",
                              marginLeft: 6,
                            }}
                          >
                            {fmtMoney(item._oferta.precio_sin_oferta)}
                          </span>
                        )}
                      </span>
                    </div>

                    {/* Fila 3: Precio x cantidad = Total */}
                    <div className="price-row">
                      <span className="unit-price">
                        {fmtMoney(precioUnitario)} x {item.cantidad}
                      </span>
                      <span className="total-price">{fmtMoney(totalItem)}</span>
                    </div>

                    {/* Fila 3: Controles */}
                    <div className="controls-row">
                      <div className="quantity-controls">
                        <IconButton
                          size="medium"
                          onClick={() => updateQuantity(index, -1)}
                          sx={{
                            bgcolor: "action.hover",
                            "&:hover": { bgcolor: "action.selected" },
                          }}
                        >
                          <Icon icon="mdi:minus" width={20} />
                        </IconButton>
                        <Typography variant="h6" sx={{ minWidth: 40, textAlign: "center" }}>
                          {item.cantidad}
                        </Typography>
                        <IconButton
                          size="medium"
                          onClick={() => updateQuantity(index, 1)}
                          sx={{
                            bgcolor: "action.hover",
                            "&:hover": { bgcolor: "action.selected" },
                          }}
                        >
                          <Icon icon="mdi:plus" width={20} />
                        </IconButton>
                      </div>
                      <div className="action-buttons">
                        <IconButton
                          size="medium"
                          onClick={() => openEditItemDialog(index)}
                          title="Editar precio/descuento"
                          sx={{
                            bgcolor: "action.hover",
                            color: "primary.main",
                            border: "1px solid",
                            borderColor: "divider",
                            "&:hover": {
                              bgcolor: "primary.main",
                              color: "primary.contrastText",
                            },
                          }}
                        >
                          <Icon icon="mdi:pencil" width={20} />
                        </IconButton>
                        <IconButton
                          size="medium"
                          onClick={() => removeItem(index)}
                          title="Eliminar"
                          sx={{
                            bgcolor: "action.hover",
                            color: "error.main",
                            border: "1px solid",
                            borderColor: "divider",
                            "&:hover": {
                              bgcolor: "error.main",
                              color: "error.contrastText",
                            },
                          }}
                        >
                          <Icon icon="mdi:delete" width={20} />
                        </IconButton>
                      </div>
                    </div>
                  </CartItem>
                );
              })
            )}
          </CartItems>

          <CartFooter>
            <TotalsSection>
              <div className="row">
                <span>Subtotal</span>
                <span>{fmtMoney(totales.subtotal || 0)}</span>
              </div>

              {/* Descuento Global */}
              <Box
                sx={{
                  display: "flex",
                  alignItems: "center",
                  gap: 1,
                  my: 1,
                }}
              >
                <TextField
                  select
                  size="small"
                  value={tipoDescuentoGlobal}
                  onChange={(e) => {
                    setTipoDescuentoGlobal(e.target.value);
                    setDescuentoGlobal(0);
                  }}
                  sx={{ width: 100 }}
                  SelectProps={{ native: true }}
                >
                  <option value="ninguno">Sin Dto.</option>
                  <option value="porcentaje">% Dto.</option>
                  <option value="monto">{monedaActiva?.moneda?.simbolo || monedaActiva?.moneda?.codigo || "Gs."} Dto.</option>
                </TextField>
                {tipoDescuentoGlobal !== "ninguno" && (
                  <TextField
                    size="small"
                    type={tipoDescuentoGlobal === "porcentaje" ? "number" : "text"}
                    placeholder={tipoDescuentoGlobal === "porcentaje" ? "%" : monedaActiva?.moneda?.simbolo || monedaActiva?.moneda?.codigo || "Gs."}
                    value={tipoDescuentoGlobal === "monto" && descuentoGlobal ? descuentoGlobal.toLocaleString("es-PY") : descuentoGlobal || ""}
                    onChange={(e) => {
                      if (tipoDescuentoGlobal === "monto") {
                        const numericValue = parseInt(e.target.value.replace(/\D/g, "") || 0);
                        setDescuentoGlobal(numericValue);
                      } else {
                        setDescuentoGlobal(parseFloat(e.target.value) || 0);
                      }
                    }}
                    inputProps={{
                      min: 0,
                      max: tipoDescuentoGlobal === "porcentaje" ? 100 : totales.subtotal,
                    }}
                    sx={{ width: 120 }}
                  />
                )}
              </Box>

              {totales.total_descuento_global > 0 && (
                <div className="row" style={{ color: "#f57c00" }}>
                  <span>Descuento</span>
                  <span>-{fmtMoney(totales.total_descuento_global || 0)}</span>
                </div>
              )}

              <Divider />
              <div className="row total">
                <span>TOTAL</span>
                <span>{fmtMoney(totales.total || 0)}</span>
              </div>
            </TotalsSection>

            {/* Cobro rápido: preselecciona el medio de pago y abre el diálogo
                en un solo click, en vez de abrir → elegir medio → confirmar. */}
            {items.length > 0 && (
              <QuickPayRow>
                {/* Códigos SIFEN reales: 1=Efectivo, 3=Tarjeta de crédito,
                    5=Transferencia (2 es Cheque, no tarjeta). */}
                {[
                  { codigo: 1, label: "Efectivo", icon: "mdi:cash" },
                  { codigo: 3, label: "Tarjeta", icon: "mdi:credit-card" },
                  { codigo: 5, label: "Transfer.", icon: "mdi:bank-transfer" },
                ].map((atajo) => {
                  const medio = mediosPago.find((m) => m.codigo === atajo.codigo);
                  if (!medio) return null;
                  return (
                    <button
                      key={atajo.codigo}
                      type="button"
                      onClick={() => {
                        setMetodoPagoSeleccionado(medio);
                        setMontoPagado(String(totales.total || 0));
                        setShowPaymentDialog(true);
                      }}
                    >
                      <Icon icon={atajo.icon} width={20} />
                      <span>{atajo.label}</span>
                    </button>
                  );
                })}
              </QuickPayRow>
            )}

            <ActionButtonsSection>
              <Button
                variant="contained"
                color="success"
                size="large"
                startIcon={<Icon icon="mdi:cash-register" width={28} />}
                onClick={() => {
                  // Establecer efectivo por defecto y monto total
                  const efectivo = mediosPago.find((m) => m.codigo === 1);
                  setMetodoPagoSeleccionado(efectivo || mediosPago[0]);
                  setMontoPagado(String(totales.total || 0));
                  setShowPaymentDialog(true);
                }}
                disabled={items.length === 0}
                fullWidth
                sx={{
                  py: 1.8,
                  fontSize: "1.2rem",
                  fontWeight: 700,
                  boxShadow: "0 4px 12px rgba(76, 175, 80, 0.4)",
                  "&:hover": {
                    boxShadow: "0 6px 16px rgba(76, 175, 80, 0.5)",
                    transform: "translateY(-1px)",
                  },
                  transition: "all 0.2s ease",
                }}
              >
                COBRAR {fmtMoney(totales.total || 0)}
              </Button>
            </ActionButtonsSection>
          </CartFooter>
        </RightPanel>
      </MainContent>

      {/* Diálogo de pago */}
      <Dialog
        open={showPaymentDialog}
        onKeyDown={handlePaymentKeyDown}
        onClose={() => {
          setShowPaymentDialog(false);
          setMonedaPagoSeleccionada(null);
          setCotizacionPago(null);
        }}
        maxWidth="sm"
        fullWidth
        PaperProps={{
          sx: {
            m: { xs: 0, sm: 2 },
            width: { xs: "100%", sm: "auto" },
            maxHeight: { xs: "100dvh", sm: "90vh" },
            borderRadius: { xs: "16px 16px 0 0", sm: 3 },
            position: { xs: "fixed", sm: "relative" },
            bottom: { xs: 0, sm: "auto" },
          },
        }}
      >
        <DialogTitle>
          <Box display="flex" alignItems="center" gap={1}>
            <Icon icon="mdi:cash-register" />
            Cobrar Venta
          </Box>
        </DialogTitle>
        <DialogContent>
          <Box sx={{ py: 2 }}>
            {/* Total a pagar */}
            <Typography variant="h3" textAlign="center" fontWeight={700} color="primary" sx={{ mb: 1 }}>
              Total: {fmtMoney(totales.total || 0)}
            </Typography>
            {pagoInfo.esDiferenteMoneda && pagoInfo.cotVenta > 0 && (
              <Typography variant="body1" textAlign="center" color="info.main" fontWeight={600} sx={{ mb: 2 }}>
                ≈ {fmtMoney(pagoInfo.totalEnMonedaPago, pagoInfo.codigoPago, pagoInfo.isoPago)}
              </Typography>
            )}
            {pagoInfo.restanteBase > 0 && pagoInfo.restanteBase < (totales.total || 0) && (
              <Typography variant="body2" textAlign="center" color="warning.main" fontWeight={600} sx={{ mb: 2 }}>
                Restante: {fmtMoney(pagoInfo.restanteBase)}
                {pagoInfo.esDiferenteMoneda && pagoInfo.cotVenta > 0 && <> ≈ {fmtMoney(pagoInfo.restanteEnMonedaPago, pagoInfo.codigoPago, pagoInfo.isoPago)}</>}
              </Typography>
            )}

            {/* Condición de pago. Solo aparece si la empresa tiene condiciones
                de crédito simple a plazo cargadas; si no, la venta es contado
                como siempre. */}
            {/* Condición de pago en dos pasos: primero la decisión grande
                (cobrar ahora o fiar) y solo al elegir crédito aparece el detalle.
                Con varias condiciones cargadas una fila de botones no escala, y
                "30 días" y "6 cuotas" no son la misma decisión para el cajero. */}
            {config.permitir_venta_credito && condicionContado && condicionesCredito.length > 0 && (
              <Box sx={{ mb: 3 }}>
                <Typography variant="subtitle2" sx={{ mb: 1.5 }}>
                  Condición de pago
                </Typography>
                <Box sx={{ display: "flex", gap: 1 }}>
                  <Button
                    variant={esVentaCredito ? "outlined" : "contained"}
                    onClick={() => handleCondicionPagoChange(condicionContado)}
                    startIcon={<Icon icon="mdi:cash" />}
                    sx={{ flex: 1, py: 1.2 }}
                  >
                    Contado
                  </Button>
                  <Tooltip title={creditoNoDisponibleMotivo || ""} arrow>
                    {/* El span es necesario: un Button deshabilitado no dispara
                        los eventos que el Tooltip necesita para mostrar el motivo. */}
                    <span style={{ flex: 1, display: "flex" }}>
                      <Button
                        fullWidth
                        variant={esVentaCredito ? "contained" : "outlined"}
                        color="warning"
                        disabled={!!creditoNoDisponibleMotivo}
                        onClick={() => handleCondicionPagoChange(condicionesCredito[0])}
                        startIcon={<Icon icon="mdi:credit-card-clock-outline" />}
                        sx={{ py: 1.2 }}
                      >
                        Crédito
                      </Button>
                    </span>
                  </Tooltip>
                </Box>

                {esVentaCredito && (
                  <FormControl size="small" fullWidth sx={{ mt: 2 }}>
                    <InputLabel id="condicion-credito-label">Condición</InputLabel>
                    <Select
                      labelId="condicion-credito-label"
                      label="Condición"
                      value={condicionPagoActiva?.id || ""}
                      onChange={(e) => {
                        const cond = condicionesCredito.find((c) => c.id === e.target.value);
                        if (cond) handleCondicionPagoChange(cond);
                      }}
                    >
                      {/* Los grupos solo aparecen si hay de los dos tipos: con un
                          solo tipo el encabezado es ruido. */}
                      {condicionesAPlazo.length > 0 && condicionesEnCuotas.length > 0 && (
                        <ListSubheader>A plazo</ListSubheader>
                      )}
                      {condicionesAPlazo.map((cond) => (
                        <MenuItem key={cond.id} value={cond.id}>
                          {cond.descripcion || `${cond.dias_plazo || 30} días`}
                        </MenuItem>
                      ))}
                      {condicionesAPlazo.length > 0 && condicionesEnCuotas.length > 0 && (
                        <ListSubheader>En cuotas</ListSubheader>
                      )}
                      {condicionesEnCuotas.map((cond) => (
                        <MenuItem key={cond.id} value={cond.id}>
                          {cond.descripcion || `${cond.cuotas || 1} cuotas`}
                        </MenuItem>
                      ))}
                    </Select>
                  </FormControl>
                )}
              </Box>
            )}

            {esVentaCredito && (
              <Paper variant="outlined" sx={{ p: 2, mb: 2, borderColor: "warning.main" }}>
                <Box display="flex" alignItems="center" gap={1} sx={{ mb: 1 }}>
                  <Icon icon="mdi:credit-card-clock-outline" width={20} />
                  <Typography variant="subtitle2" fontWeight={700}>
                    Venta a crédito — no se cobra en caja
                  </Typography>
                </Box>
                <Typography variant="body2" color="text.secondary">
                  Cliente: <strong>{clienteSeleccionado?.personas?.razon_social || clienteSeleccionado?.razon_social}</strong>
                </Typography>
                {creditoInfo && (
                  <Typography variant="body2" color="text.secondary" sx={{ mb: 1 }}>
                    Crédito disponible:{" "}
                    <strong>
                      {creditoInfo.sin_tope || !Number(creditoInfo.limite_credito)
                        ? "Sin tope"
                        : fmtCredito(Math.max(0, creditoInfo.limite_credito - (creditoInfo.saldo_pendiente || 0)))}
                    </strong>
                  </Typography>
                )}

                {cronogramaCredito.length === 1 ? (
                  <Typography variant="body2" color="text.secondary">
                    Vence el <strong>{fmtFechaCalendario(cronogramaCredito[0].vencimiento)}</strong> ({condicionPagoActiva?.dias_plazo || 30} días)
                  </Typography>
                ) : (
                  <Box sx={{ mt: 0.5 }}>
                    <Typography variant="caption" color="text.secondary">
                      {cronogramaCredito.length} cuotas cada {condicionPagoActiva?.intervalo_dias || 30} días
                    </Typography>
                    <Box
                      component="table"
                      sx={{
                        width: "100%",
                        mt: 0.5,
                        borderCollapse: "collapse",
                        "& td": { py: 0.4, fontSize: 13, borderBottom: "1px solid", borderColor: "divider" },
                        "& td:last-of-type": { textAlign: "right", fontWeight: 600 },
                        "& tr:last-of-type td": { borderBottom: "none" },
                      }}
                    >
                      <tbody>
                        {cronogramaCredito.map((cuota) => (
                          <tr key={cuota.nro_cuota}>
                            <td>#{cuota.nro_cuota}</td>
                            <td>{fmtFechaCalendario(cuota.vencimiento)}</td>
                            <td>{fmtMoney(cuota.monto_cuota)}</td>
                          </tr>
                        ))}
                      </tbody>
                    </Box>
                  </Box>
                )}
              </Paper>
            )}

            {!esVentaCredito && (
              <>
            {/* Métodos de pago - deshabilitados si el pago está completo */}
            <Typography variant="subtitle2" sx={{ mb: 1.5 }}>
              Seleccionar método de pago
              {pagoInfo.pagoCompleto && (
                <Typography component="span" variant="caption" color="success.main" sx={{ ml: 1 }}>
                  (Pago completo)
                </Typography>
              )}
            </Typography>
            <Box
              sx={{
                display: "flex",
                gap: 1,
                flexWrap: "wrap",
                mb: 3,
                opacity: pagoInfo.pagoCompleto ? 0.5 : 1,
              }}
            >
              {mediosPago.map((medio) => {
                // Códigos SIFEN de medios de pago (tabla medio_pago).
                const iconMap = {
                  1: "mdi:cash",
                  2: "mdi:checkbook",
                  3: "mdi:credit-card",
                  4: "mdi:credit-card-outline",
                  5: "mdi:bank-transfer",
                  6: "mdi:cash-fast",
                  7: "mdi:wallet-outline",
                  8: "mdi:credit-card-settings-outline",
                  9: "mdi:ticket-percent-outline",
                  17: "mdi:cellphone",
                  21: "mdi:qrcode-scan",
                };
                const icon = iconMap[medio.codigo] || "mdi:currency-usd";
                const isSelected = metodoPagoSeleccionado?.id === medio.id;

                return (
                  <Paper
                    key={medio.id}
                    elevation={isSelected ? 8 : 1}
                    sx={{
                      width: 100,
                      height: 80,
                      p: 1.5,
                      cursor: pagoInfo.pagoCompleto ? "not-allowed" : "pointer",
                      border: isSelected ? 2 : 1,
                      borderColor: isSelected ? "primary.main" : "divider",
                      bgcolor: isSelected ? "primary.dark" : "background.paper",
                      transition: "all 0.2s",
                      display: "flex",
                      flexDirection: "column",
                      alignItems: "center",
                      justifyContent: "center",
                      "&:hover": pagoInfo.pagoCompleto
                        ? {}
                        : {
                            borderColor: "primary.main",
                            transform: "scale(1.02)",
                          },
                    }}
                    onClick={() => !pagoInfo.pagoCompleto && setMetodoPagoSeleccionado(medio)}
                  >
                    <Icon icon={icon} width={28} />
                    <Typography variant="caption" fontWeight={isSelected ? 700 : 400} textAlign="center" sx={{ mt: 0.5, lineHeight: 1.2 }}>
                      {medio.descripcion}
                    </Typography>
                  </Paper>
                );
              })}
            </Box>

            {/* Input de monto - siempre visible con botones rápidos */}
            <Box sx={{ mb: 2 }}>
              <TextField
                label={`Monto recibido (${pagoInfo.codigoPago})`}
                fullWidth
                type={pagoInfo.esPYGPago ? "text" : "number"}
                value={pagoInfo.esPYGPago ? (montoPagado ? parseInt(montoPagado.replace(/\D/g, "") || 0).toLocaleString("es-PY") : "") : montoPagado || ""}
                onChange={(e) => {
                  if (pagoInfo.esPYGPago) {
                    const numericValue = e.target.value.replace(/\D/g, "");
                    setMontoPagado(numericValue);
                  } else {
                    setMontoPagado(e.target.value);
                  }
                }}
                inputRef={montoInputRef}
                inputProps={{
                  ...(!pagoInfo.esPYGPago ? { step: "0.01", min: 0 } : {}),
                  inputMode: "numeric",
                }}
                InputProps={{
                  startAdornment: <Typography sx={{ mr: 1, fontWeight: 700, fontSize: "1.6rem" }}>{pagoInfo.simboloPago}</Typography>,
                }}
                placeholder="0"
                /* Junto al total, es el dato crítico del cobro: el cajero tipea
                   acá lo que recibe y de eso sale el vuelto. */
                sx={{
                  "& .MuiInputBase-input": {
                    fontSize: "2rem",
                    fontWeight: 700,
                    py: 1.5,
                  },
                  "& .MuiInputLabel-root": { fontSize: "1rem" },
                }}
              />
              {/* Equivalencia en moneda base al escribir */}
              {pagoInfo.esDiferenteMoneda && montoPagado && (
                <Typography variant="caption" color="text.secondary" sx={{ mt: 0.5, display: "block" }}>
                  ≈ {fmtMoney(pagoInfo.convertirABase(pagoInfo.esPYGPago ? parseInt(montoPagado) || 0 : parseFloat(montoPagado) || 0))}
                </Typography>
              )}

              {/* Botones rápidos de monto - según moneda de pago */}
              <Grid container spacing={0.75} sx={{ mt: 1 }}>
                {pagoInfo.quickAmounts.map((monto) => (
                  <Grid item xs={4} key={monto}>
                    <Button
                      variant="outlined"
                      fullWidth
                      size="medium"
                      sx={{
                        py: { xs: 1, sm: 0.75 },
                        fontSize: { xs: "0.8rem", sm: "0.75rem" },
                      }}
                      onClick={() => {
                        if (pagoInfo.esPYGPago) {
                          setMontoPagado(String(parseInt(montoPagado || 0) + monto));
                        } else {
                          setMontoPagado(String(Math.round(((parseFloat(montoPagado) || 0) + monto) * 100) / 100));
                        }
                      }}
                    >
                      +
                      {pagoInfo.esPYGPago
                        ? monto.toLocaleString("es-PY")
                        : monto.toLocaleString("en-US", {
                            minimumFractionDigits: 0,
                          })}
                    </Button>
                  </Grid>
                ))}
              </Grid>

              <Button
                variant="text"
                fullWidth
                sx={{ mt: 1 }}
                onClick={() => {
                  setMontoPagado(String(pagoInfo.montoExactoEnMonedaPago));
                }}
              >
                Monto exacto
                {pagoInfo.esDiferenteMoneda && pagoInfo.cotVenta > 0 && (
                  <Typography component="span" variant="caption" sx={{ ml: 1 }}>
                    ({fmtMoney(pagoInfo.montoExactoEnMonedaPago, pagoInfo.codigoPago, pagoInfo.isoPago)})
                  </Typography>
                )}
              </Button>
            </Box>

            {/* Fase 2: Selector de moneda de pago */}
            {monedas.length > 1 && (
              <Box sx={{ mb: 2 }}>
                <Typography variant="subtitle2" sx={{ mb: 0.5 }}>
                  Moneda de pago
                </Typography>
                <Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
                  {monedas
                    .filter((m) => m.active !== false)
                    .map((m) => {
                      const codigo = m.moneda?.codigo || m.codigo;
                      const isSelected = monedaPagoSeleccionada
                        ? (monedaPagoSeleccionada.moneda_id || monedaPagoSeleccionada.id) === (m.moneda_id || m.id)
                        : codigo === (monedaActiva?.moneda?.codigo || "PYG");
                      return (
                        <Paper
                          key={m.id || m.moneda_id}
                          elevation={isSelected ? 4 : 0}
                          sx={{
                            px: 2,
                            py: 0.75,
                            cursor: "pointer",
                            border: isSelected ? 2 : 1,
                            borderColor: isSelected ? "info.main" : "divider",
                            bgcolor: isSelected ? "info.dark" : "background.paper",
                            borderRadius: 2,
                            transition: "all 0.15s",
                            "&:hover": { borderColor: "info.main" },
                          }}
                          onClick={async () => {
                            setMonedaPagoSeleccionada(m);
                            const codigoPago = m.moneda?.codigo || m.codigo;
                            const codigoFactura = monedaActiva?.moneda?.codigo || "PYG";
                            const decPago = m.moneda?.decimales_estandar ?? (codigoPago === "PYG" ? 0 : 2);
                            const decFactura = monedaActiva?.moneda?.decimales_estandar ?? (codigoFactura === "PYG" ? 0 : 2);
                            // Calcular restante actual en base
                            const pagadoBase = pagosAgregados.reduce((s, p) => s + p.monto, 0);
                            const restBase = Math.max(0, (totales.total || 0) - pagadoBase);

                            if (codigoPago !== codigoFactura) {
                              try {
                                const cot = await getUltimaCotizacion(m.moneda_id || m.id);
                                setCotizacionPago(cot);
                                // Auto-fill con monto convertido
                                const cv = Number(cot?.cotizacion_venta || cot?.data?.cotizacion_venta || 0);
                                if (cv > 0 && restBase > 0) {
                                  const raw = codigoFactura === "PYG" ? restBase / cv : restBase * cv;
                                  // Ceil a decimales de la moneda de pago
                                  const f = Math.pow(10, decPago);
                                  const shifted = raw * f;
                                  const exacto = Math.abs(shifted - Math.round(shifted)) < 1e-6 ? Math.round(shifted) / f : Math.ceil(shifted) / f;
                                  setMontoPagado(String(exacto));
                                }
                              } catch {
                                setCotizacionPago(null);
                                toast.error(`No hay cotización para ${codigoPago}`);
                              }
                            } else {
                              setCotizacionPago(null);
                              // Misma moneda: auto-fill con restante
                              if (restBase > 0) {
                                const f = Math.pow(10, decFactura);
                                setMontoPagado(String(Math.round(restBase * f) / f));
                              }
                            }
                          }}
                        >
                          <Typography variant="body2" fontWeight={isSelected ? 700 : 400}>
                            {m.moneda?.simbolo || m.simbolo || ""} {codigo}
                          </Typography>
                        </Paper>
                      );
                    })}
                </Box>
                {cotizacionPago && monedaPagoSeleccionada && (
                  <Typography variant="caption" color="info.main" sx={{ mt: 0.5, display: "block" }}>
                    Cotización: 1 {monedaPagoSeleccionada.moneda?.codigo || monedaPagoSeleccionada.codigo} ={" "}
                    {Number(cotizacionPago.cotizacion_venta || cotizacionPago.data?.cotizacion_venta).toLocaleString("es-PY")} {monedaActiva?.moneda?.codigo || "PYG"}
                  </Typography>
                )}
              </Box>
            )}

            {/* Vuelto - solo para efectivo */}
            {metodoPagoSeleccionado?.codigo === 1 && vuelto > 0 && (
              <Paper sx={{ p: 2, bgcolor: "success.dark", mb: 2 }}>
                <Typography variant="h5" textAlign="center" fontWeight={700}>
                  Vuelto: {fmtMoney(vuelto)}
                </Typography>
                {pagoInfo.esDiferenteMoneda && pagoInfo.cotVenta > 0 && (
                  <Typography variant="body2" textAlign="center" color="success.light">
                    ≈ {fmtMoney(pagoInfo.convertirAMonedaPago(vuelto), pagoInfo.codigoPago, pagoInfo.isoPago)}
                  </Typography>
                )}
              </Paper>
            )}

            {/* Pagos agregados */}
            {pagosAgregados.length > 0 && (
              <Box sx={{ mb: 2 }}>
                <Typography variant="subtitle2" sx={{ mb: 1 }}>
                  Pagos registrados
                </Typography>
                {pagosAgregados.map((pago, index) => (
                  <Paper
                    key={index}
                    sx={{
                      p: 1.5,
                      mb: 1,
                      display: "flex",
                      justifyContent: "space-between",
                      alignItems: "center",
                    }}
                  >
                    <Box display="flex" alignItems="center" gap={1}>
                      <Icon icon="mdi:check-circle" color="#4caf50" />
                      <Typography variant="body2">{pago.descripcion}</Typography>
                    </Box>
                    <Box display="flex" alignItems="center" gap={1}>
                      <Box textAlign="right">
                        {pago.monto_moneda_original && pago.codigo_moneda_pago && (
                          <Typography variant="caption" color="text.secondary" display="block">
                            {pago.codigo_moneda_pago}{" "}
                            {pago.monto_moneda_original.toLocaleString("es-PY", {
                              minimumFractionDigits: pago.codigo_moneda_pago === "PYG" ? 0 : 2,
                              maximumFractionDigits: pago.codigo_moneda_pago === "PYG" ? 0 : 2,
                            })}
                          </Typography>
                        )}
                        <Typography fontWeight={700}>{fmtMoney(pago.monto)}</Typography>
                      </Box>
                      <IconButton
                        size="small"
                        color="error"
                        onClick={() => {
                          setPagosAgregados((prev) => prev.filter((_, i) => i !== index));
                        }}
                      >
                        <Icon icon="mdi:close" width={16} />
                      </IconButton>
                    </Box>
                  </Paper>
                ))}
                <Divider sx={{ my: 1 }} />
                <Box display="flex" justifyContent="space-between">
                  <Typography variant="body2">Total pagado:</Typography>
                  <Typography fontWeight={700} color="primary">
                    {fmtMoney(pagoInfo.totalPagadoBase)}
                  </Typography>
                </Box>
                {pagoInfo.restanteBase > 0 && (
                  <Box display="flex" justifyContent="space-between">
                    <Typography variant="body2">Restante:</Typography>
                    <Box textAlign="right">
                      <Typography fontWeight={700} color="warning.main">
                        {fmtMoney(pagoInfo.restanteBase)}
                      </Typography>
                      {pagoInfo.esDiferenteMoneda && pagoInfo.cotVenta > 0 && (
                        <Typography variant="caption" color="info.main">
                          ≈ {fmtMoney(pagoInfo.restanteEnMonedaPago, pagoInfo.codigoPago, pagoInfo.isoPago)}
                        </Typography>
                      )}
                    </Box>
                  </Box>
                )}
              </Box>
            )}

            {/* Botón agregar pago (para pago mixto) */}
            {metodoPagoSeleccionado && parseFloat(montoPagado || 0) > 0 && (
              <Button
                variant="outlined"
                color="primary"
                fullWidth
                sx={{ mb: 2 }}
                startIcon={<Icon icon="mdi:plus" />}
                onClick={() => {
                  const monto = pagoInfo.esPYGPago ? parseInt(montoPagado || 0) : parseFloat(montoPagado || 0);
                  if (monto > 0) {
                    let montoEnBase = pagoInfo.convertirABase(monto);
                    // Redondear a decimales de factura
                    montoEnBase = pagoInfo.redondear(montoEnBase, pagoInfo.decimalesFactura);
                    // Capear al restante si la diferencia es puro redondeo
                    const diff = montoEnBase - pagoInfo.restanteBase;
                    if (diff > 0 && diff <= pagoInfo.toleranciaRedondeo) {
                      montoEnBase = pagoInfo.restanteBase;
                    }
                    setPagosAgregados((prev) => [
                      ...prev,
                      {
                        medio_pago_id: metodoPagoSeleccionado.id,
                        descripcion: metodoPagoSeleccionado.descripcion,
                        monto: montoEnBase,
                        moneda_id: monedaActiva?.moneda_id || monedas[0]?.moneda_id || monedas[0]?.id,
                        moneda_pago_id: pagoInfo.esDiferenteMoneda ? monedaPagoSeleccionada?.moneda_id || monedaPagoSeleccionada?.id : null,
                        monto_moneda_original: pagoInfo.esDiferenteMoneda ? monto : null,
                        codigo_moneda_pago: pagoInfo.esDiferenteMoneda ? pagoInfo.codigoPago : null,
                      },
                    ]);
                    setMontoPagado("");
                    setMetodoPagoSeleccionado(null);
                  }
                }}
              >
                Agregar pago de {fmtMoney(pagoInfo.esPYGPago ? parseInt(montoPagado || 0) : parseFloat(montoPagado || 0), pagoInfo.codigoPago, pagoInfo.isoPago)}
              </Button>
            )}

            {/* Info del método seleccionado (para métodos que requieren integración) */}
            {metodoPagoSeleccionado && ![1].includes(metodoPagoSeleccionado.codigo) && (
              <Paper
                sx={{
                  p: 2,
                  bgcolor: "background.default",
                  textAlign: "center",
                  mb: 2,
                }}
              >
                <Typography variant="caption" color="text.secondary">
                  {metodoPagoSeleccionado.codigo === 2 || metodoPagoSeleccionado.codigo === 3
                    ? "Próximamente: Integración con POS de pagos"
                    : metodoPagoSeleccionado.codigo === 4
                      ? "Próximamente: Generación de QR para cobro"
                      : metodoPagoSeleccionado.codigo === 6
                        ? "Próximamente: Integración con billeteras digitales"
                        : ""}
                </Typography>
              </Paper>
            )}
              </>
            )}
          </Box>
        </DialogContent>
        <DialogActions
          sx={{
            p: 2,
            gap: 1,
            flexDirection: { xs: "column-reverse", sm: "row" },
          }}
        >
          {/* Misma altura en ambos; el confirmar pesa el doble de ancho para
              que la acción principal se distinga de la de escape. */}
          <Button
            variant="outlined"
            size="large"
            sx={{ flex: 1, py: 1.4 }}
            onClick={() => {
              setShowPaymentDialog(false);
              setMetodoPagoSeleccionado(null);
              setMontoPagado("");
              setPagosAgregados([]);
              setMonedaPagoSeleccionada(null);
              setCotizacionPago(null);
              setCondicionPagoActiva(condicionContado);
            }}
            disabled={isSaving}
          >
            Cancelar
          </Button>
          <Button
            variant="contained"
            color="success"
            size="large"
            sx={{ flex: 2, py: 1.4, fontWeight: 700 }}
            onClick={handlePayment}
            disabled={cobroDeshabilitado}
            startIcon={isSaving ? <Icon icon="mdi:loading" className="spin" /> : <Icon icon="mdi:check" />}
          >
            {isSaving ? "Procesando..." : esVentaCredito ? "Facturar a Crédito (Enter)" : "Confirmar Venta (Enter)"}
          </Button>
        </DialogActions>
      </Dialog>

      {/* Diálogo de búsqueda de cliente */}
      <Dialog open={showClienteDialog} onClose={() => setShowClienteDialog(false)} maxWidth="sm" fullWidth>
        <DialogTitle>
          <Box display="flex" alignItems="center" gap={1}>
            <Icon icon="mdi:account-search" />
            Buscar Cliente
          </Box>
        </DialogTitle>
        <DialogContent>
          {/* Fase 2: Sección de crédito del cliente seleccionado */}
          {clienteSeleccionado && creditoInfo && (
            <Box
              sx={{
                mb: 2,
                p: 2,
                borderRadius: 2,
                border: "1px solid",
                borderColor: creditoInfo.bloqueado ? "error.main" : "divider",
                bgcolor: creditoInfo.bloqueado ? "error.50" : "background.default",
              }}
            >
              <Box
                sx={{
                  display: "flex",
                  alignItems: "center",
                  gap: 1,
                  mb: 1.5,
                }}
              >
                <Icon icon={creditoInfo.bloqueado ? "mdi:credit-card-off-outline" : "mdi:credit-card-check-outline"} width={20} color={creditoInfo.bloqueado ? "#ef4444" : "#4caf50"} />
                <Typography variant="subtitle2" fontWeight={600}>
                  Crédito - {clienteSeleccionado.personas?.razon_social || clienteSeleccionado.razon_social}
                </Typography>
                {creditoInfo.bloqueado && <Chip label="BLOQUEADO" size="small" color="error" sx={{ height: 20, fontSize: 10, fontWeight: 700 }} />}
              </Box>
              <Grid container spacing={1.5}>
                <Grid item xs={6}>
                  <Typography variant="caption" color="text.secondary" display="block">
                    Límite de crédito
                  </Typography>
                  <Typography variant="body2" fontWeight={600}>
                    {textoTope(creditoInfo.limite_credito)}
                  </Typography>
                </Grid>
                <Grid item xs={6}>
                  <Typography variant="caption" color="text.secondary" display="block">
                    Saldo pendiente
                  </Typography>
                  <Typography variant="body2" fontWeight={600} color={creditoInfo.saldo_pendiente > creditoInfo.limite_credito * 0.8 ? "error.main" : "text.primary"}>
                    {fmtCredito(creditoInfo.saldo_pendiente || 0)}
                  </Typography>
                </Grid>
                <Grid item xs={6}>
                  <Typography variant="caption" color="text.secondary" display="block">
                    Disponible
                  </Typography>
                  <Typography variant="body2" fontWeight={600} color="success.main">
                    {creditoInfo.sin_tope || !Number(creditoInfo.limite_credito)
                      ? "Sin tope"
                      : fmtCredito(Math.max(0, creditoInfo.limite_credito - (creditoInfo.saldo_pendiente || 0)))}
                  </Typography>
                </Grid>
                <Grid item xs={6}>
                  <Typography variant="caption" color="text.secondary" display="block">
                    Mora tolerada
                  </Typography>
                  <Typography variant="body2" fontWeight={600}>
                    {/* El endpoint devuelve dias_mora_maximo; `dias_credito` no
                        existe y hacía que esto mostrara siempre "0 días". */}
                    {creditoInfo.dias_mora_maximo ?? 0} días
                  </Typography>
                </Grid>
              </Grid>
              {creditoInfo.motivo_bloqueo && (
                <Typography variant="caption" color="error.main" sx={{ mt: 1, display: "block" }}>
                  Motivo bloqueo: {creditoInfo.motivo_bloqueo}
                </Typography>
              )}
            </Box>
          )}

          <TextField
            fullWidth
            placeholder="Buscar por RUC, CI o nombre..."
            value={clienteSearch}
            onChange={(e) => setClienteSearch(e.target.value)}
            sx={{ mt: 1 }}
            autoFocus
            InputProps={{
              startAdornment: <Icon icon="mdi:magnify" style={{ marginRight: 8 }} />,
            }}
          />

          {/* Alta rápida como acción secundaria: el camino natural es buscar
              primero y crear solo si no está. Cuando la búsqueda no trae nada, la
              acción principal aparece abajo, ya con el dato cargado. */}
          <Button
            variant="text"
            color="primary"
            size="small"
            sx={{ mt: 1 }}
            startIcon={<Icon icon="mdi:account-plus" />}
            onClick={() => {
              setNuevoClienteData(prepararNuevoCliente(clienteSearch));
              setShowNuevoClienteDialog(true);
            }}
          >
            Agregar nuevo cliente
          </Button>

          <Box sx={{ mt: 2, maxHeight: 300, overflow: "auto" }}>
            {clientesResults.length === 0 ? (
              clienteSearch.trim().length < 2 ? (
                <Typography color="text.secondary">Escriba al menos 2 caracteres para buscar</Typography>
              ) : (
                <Box sx={{ textAlign: "center", py: 2 }}>
                  <Icon icon="mdi:account-search-outline" width={40} style={{ opacity: 0.4 }} />
                  <Typography color="text.secondary" sx={{ mb: 1.5 }}>
                    Sin resultados para &quot;{clienteSearch.trim()}&quot;
                  </Typography>
                  <Button
                    variant="contained"
                    startIcon={<Icon icon="mdi:account-plus" />}
                    onClick={() => {
                      setNuevoClienteData(prepararNuevoCliente(clienteSearch));
                      setShowNuevoClienteDialog(true);
                    }}
                  >
                    {/^\d+$/.test(clienteSearch.trim())
                      ? `Crear cliente con ${clienteSearch.trim().length <= 7 ? "CI" : "RUC"} ${clienteSearch.trim()}`
                      : `Crear cliente «${clienteSearch.trim()}»`}
                  </Button>
                </Box>
              )
            ) : (
              clientesResults.map((cliente) => (
                <Paper
                  key={cliente.id}
                  sx={{
                    p: 2,
                    mb: 1,
                    cursor: "pointer",
                    "&:hover": { bgcolor: "action.hover" },
                  }}
                  onClick={() => usarClienteExistente(cliente)}
                >
                  <Typography fontWeight={600}>{cliente.personas?.razon_social || cliente.razon_social}</Typography>
                  <Typography variant="body2" color="text.secondary">
                    RUC: {cliente.personas?.ruc || cliente.ruc}-{cliente.personas?.dv || cliente.dv}
                  </Typography>
                  {(cliente.lista_precios || cliente.tipo_cliente) && (
                    <Box
                      sx={{
                        display: "flex",
                        gap: 0.5,
                        mt: 0.5,
                        flexWrap: "wrap",
                      }}
                    >
                      {cliente.lista_precios && (
                        <Chip
                          icon={<Icon icon="mdi:tag-outline" width={14} />}
                          label={cliente.lista_precios.nombre}
                          size="small"
                          sx={{
                            height: 22,
                            fontSize: 11,
                            bgcolor: "#2196f315",
                            color: "#2196f3",
                          }}
                        />
                      )}
                      {cliente.tipo_cliente && (
                        <Chip
                          label={cliente.tipo_cliente.charAt(0).toUpperCase() + cliente.tipo_cliente.slice(1)}
                          size="small"
                          sx={{
                            height: 22,
                            fontSize: 11,
                            bgcolor: "#ff980015",
                            color: "#ff9800",
                          }}
                        />
                      )}
                    </Box>
                  )}
                </Paper>
              ))
            )}
          </Box>
        </DialogContent>
        <DialogActions sx={{ p: 2, justifyContent: "space-between" }}>
          <Button
            variant="text"
            color="warning"
            onClick={() => {
              setClienteSeleccionado(null);
              setShowClienteDialog(false);
            }}
            startIcon={<Icon icon="mdi:account-off" />}
          >
            Sin Nombre (Contado)
          </Button>
          <Button onClick={() => setShowClienteDialog(false)}>Cerrar</Button>
        </DialogActions>
      </Dialog>

      {/* Diálogo para editar item */}
      {/* Modo estación: cuánto carga el cliente, antes de entrar al carrito */}
      <Dialog open={Boolean(pendienteMonto)} onClose={cancelarMontoNuevoItem} maxWidth="xs" fullWidth>
        <DialogTitle>
          <Icon icon="mdi:gas-station" style={{ marginRight: 8, verticalAlign: "middle" }} />
          {pendienteMonto?.producto?.descripcion || pendienteMonto?.producto?.nombre || "Producto"}
        </DialogTitle>
        <DialogContent>
          <Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
            <MonedaInput
              label="¿Cuánto carga?"
              value={montoNuevoItem}
              onChange={setMontoNuevoItem}
              moneda={monedaActiva?.moneda?.codigo || "PYG"}
              size="medium"
              autoFocus
              fullWidth
              onKeyDown={(e) => {
                if (e.key === "Enter") confirmarMontoNuevoItem();
              }}
            />
            <Box sx={{ display: "flex", gap: 1, flexWrap: "wrap" }}>
              {montosRapidos(config).map((m) => (
                <Button key={m} size="small" variant="outlined" onClick={() => setMontoNuevoItem(m)}>
                  {fmtMoney(m)}
                </Button>
              ))}
            </Box>
            <Typography variant="body2" color="text.secondary">
              {`Precio: ${fmtMoney(pendienteMonto?.precioUnitario || 0)}`}
              {montoNuevoItem > 0 && ` — Cantidad: ${cantidadDesdeMonto(montoNuevoItem, pendienteMonto?.precioUnitario, decimalesCantidad(config)) ?? "—"}`}
            </Typography>
          </Box>
        </DialogContent>
        <DialogActions>
          <Button onClick={cancelarMontoNuevoItem}>Cancelar</Button>
          <Button variant="contained" onClick={confirmarMontoNuevoItem} disabled={!(montoNuevoItem > 0)}>
            Agregar
          </Button>
        </DialogActions>
      </Dialog>

      <Dialog open={editItemDialog} onClose={() => setEditItemDialog(false)} maxWidth="xs" fullWidth>
        <DialogTitle>
          <Icon icon="mdi:pencil" style={{ marginRight: 8, verticalAlign: "middle" }} />
          Editar Producto
        </DialogTitle>
        <DialogContent>
          <Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
            {montoEditableEnEdicion && (
              <Box>
                <MonedaInput label="Monto" value={Number(editItemData.monto) || 0} onChange={aplicarMonto} moneda={monedaActiva?.moneda?.codigo || "PYG"} size="medium" autoFocus fullWidth />
                <Typography variant="caption" color="text.secondary">
                  Cargá el monto y la cantidad se calcula sola
                </Typography>
                <Box sx={{ display: "flex", gap: 1, mt: 1, flexWrap: "wrap" }}>
                  {montosRapidos(config).map((m) => (
                    <Button key={m} size="small" variant="outlined" onClick={() => aplicarMonto(m)}>
                      {fmtMoney(m)}
                    </Button>
                  ))}
                </Box>
              </Box>
            )}
            <TextField
              label="Cantidad"
              type="number"
              autoFocus={!montoEditableEnEdicion}
              value={editItemData.cantidad}
              onChange={(e) => setEditItemData({ ...editItemData, cantidad: e.target.value })}
              inputProps={{
                min: 0,
                step: montoEditableEnEdicion ? 10 ** -decimalesCantidad(config) : 1,
              }}
              helperText={montoEditableEnEdicion ? "Se recalcula si cargás un monto distinto" : undefined}
              fullWidth
            />
            <TextField
              label="Precio Unitario"
              type={monedaActiva?.moneda?.codigo === "PYG" ? "text" : "number"}
              value={
                monedaActiva?.moneda?.codigo === "PYG"
                  ? editItemData.precio
                    ? parseInt(editItemData.precio.toString().replace(/\D/g, "") || 0).toLocaleString("es-PY")
                    : ""
                  : editItemData.precio || ""
              }
              onChange={(e) => {
                if (monedaActiva?.moneda?.codigo === "PYG") {
                  const numericValue = e.target.value.replace(/\D/g, "");
                  setEditItemData({ ...editItemData, precio: numericValue });
                } else {
                  setEditItemData({ ...editItemData, precio: e.target.value });
                }
              }}
              fullWidth
              inputProps={monedaActiva?.moneda?.codigo !== "PYG" ? { step: "0.01", min: 0 } : {}}
              InputProps={{
                startAdornment: <Typography sx={{ mr: 1, color: "text.secondary" }}>{monedaActiva?.moneda?.codigo || "Gs."}</Typography>,
              }}
            />
            <Box sx={{ display: "flex", gap: 1 }}>
              <TextField
                select
                label="Tipo Descuento"
                value={editItemData.tipoDescuento}
                onChange={(e) =>
                  setEditItemData({
                    ...editItemData,
                    tipoDescuento: e.target.value,
                    descuento: "0",
                  })
                }
                sx={{ width: 140 }}
                SelectProps={{ native: true }}
              >
                <option value="porcentaje">%</option>
                <option value="monto">{monedaActiva?.moneda?.codigo || "Gs."}</option>
              </TextField>
              <TextField
                label={editItemData.tipoDescuento === "porcentaje" ? "Descuento (%)" : `Descuento (${monedaActiva?.moneda?.codigo || "Gs."})`}
                value={
                  editItemData.tipoDescuento === "monto" && editItemData.descuento
                    ? parseInt(editItemData.descuento.toString().replace(/\D/g, "") || 0).toLocaleString("es-PY")
                    : editItemData.descuento
                }
                onChange={(e) => {
                  if (editItemData.tipoDescuento === "monto") {
                    const numericValue = e.target.value.replace(/\D/g, "");
                    setEditItemData({
                      ...editItemData,
                      descuento: numericValue,
                    });
                  } else {
                    setEditItemData({
                      ...editItemData,
                      descuento: e.target.value,
                    });
                  }
                }}
                inputProps={{
                  min: 0,
                  max: editItemData.tipoDescuento === "porcentaje" ? 100 : undefined,
                }}
                type={editItemData.tipoDescuento === "porcentaje" ? "number" : "text"}
                fullWidth
                helperText={editItemData.tipoDescuento === "porcentaje" ? "0-100%" : `Monto en ${monedaActiva?.moneda?.codigo || "Gs."}`}
              />
            </Box>
          </Box>
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setEditItemDialog(false)}>Cancelar</Button>
          <Button variant="contained" onClick={saveEditedItem}>
            Guardar
          </Button>
        </DialogActions>
      </Dialog>

      {/* Modal de Configuración */}
      <Dialog open={showConfigDialog} onClose={() => setShowConfigDialog(false)} maxWidth="sm" fullWidth>
        <DialogTitle>
          <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
            <Icon icon="mdi:cog" width={24} />
            Configuración POS
            {cajaSeleccionada && (
              <Typography variant="caption" color="text.secondary" sx={{ ml: 1 }}>
                — {cajaSeleccionada.descripcion || "Caja"}
              </Typography>
            )}
          </Box>
        </DialogTitle>
        <DialogContent>
          <Box sx={{ display: "flex", flexDirection: "column", gap: 2.5, pt: 1 }}>
            {/* Moneda */}
            <TextField
              select
              label="Moneda"
              value={monedaActiva?.id || ""}
              onChange={(e) => {
                const selected = monedas.find((m) => m.id === e.target.value);
                setMonedaActiva(selected);
              }}
              fullWidth
              size="small"
              SelectProps={{ native: true }}
              helperText="Moneda para mostrar precios"
            >
              {monedas.map((m) => (
                <option key={m.id} value={m.id}>
                  {m.moneda?.codigo} - {m.moneda?.descripcion}
                </option>
              ))}
            </TextField>

            <Divider />

            {/* Impresora Térmica (QZ Tray) */}
            <Box>
              <Typography variant="body2" fontWeight={600} sx={{ mb: 1.5, display: "flex", alignItems: "center", gap: 1 }}>
                <Icon icon="mdi:printer" width={20} />
                Impresora Térmica (QZ Tray)
              </Typography>

              <Box
                sx={{
                  display: "flex",
                  flexDirection: "column",
                  gap: 2,
                  pl: 0.5,
                }}
              >
                {/* Estado de conexión + Detectar */}
                <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
                  <Chip
                    size="small"
                    label={qzStatus === "connected" ? "Conectado" : qzStatus === "connecting" ? "Conectando..." : qzStatus === "error" ? "No disponible" : "No conectado"}
                    color={qzStatus === "connected" ? "success" : qzStatus === "error" ? "error" : "default"}
                    variant="outlined"
                  />
                  <Button size="small" variant="outlined" onClick={handleDetectPrinters} disabled={loadingPrinters}>
                    {loadingPrinters ? <CircularProgress size={16} sx={{ mr: 1 }} /> : null}
                    {loadingPrinters ? "Detectando..." : "DETECTAR IMPRESORAS"}
                  </Button>
                </Box>

                {/* Selección de impresora detectada */}
                {availablePrinters.length > 0 ? (
                  <TextField
                    select
                    label="Impresora"
                    value={configForm.printer_name}
                    onChange={(e) =>
                      setConfigForm((prev) => ({
                        ...prev,
                        printer_name: e.target.value,
                      }))
                    }
                    fullWidth
                    size="small"
                    SelectProps={{ native: true }}
                    helperText="Seleccione la impresora térmica a utilizar"
                  >
                    <option value="">— Seleccionar —</option>
                    {availablePrinters.map((p) => (
                      <option key={p} value={p}>
                        {p}
                      </option>
                    ))}
                  </TextField>
                ) : (
                  <Typography variant="body2" color="text.secondary" sx={{ fontStyle: "italic" }}>
                    {configForm.printer_name ? `Impresora configurada: ${configForm.printer_name}` : 'Haga clic en "Detectar Impresoras" para listar las impresoras disponibles.'}
                  </Typography>
                )}

                {/* Ancho de Papel */}
                <TextField
                  select
                  label="Ancho de Papel"
                  value={configForm.printer_paper_width}
                  onChange={(e) =>
                    setConfigForm((prev) => ({
                      ...prev,
                      printer_paper_width: parseInt(e.target.value, 10),
                    }))
                  }
                  fullWidth
                  size="small"
                  SelectProps={{ native: true }}
                >
                  <option value={58}>58 mm</option>
                  <option value={80}>80 mm</option>
                </TextField>

                {/* Auto-imprimir ticket */}
                <FormControlLabel
                  control={
                    <Switch
                      checked={configForm.auto_print_ticket}
                      onChange={(e) =>
                        setConfigForm((prev) => ({
                          ...prev,
                          auto_print_ticket: e.target.checked,
                        }))
                      }
                      color="primary"
                    />
                  }
                  label={
                    <Box>
                      <Typography variant="body2" fontWeight={500}>
                        Auto-imprimir ticket después de cada venta
                      </Typography>
                      <Typography variant="caption" color="text.secondary">
                        Imprime automáticamente sin mostrar diálogo de confirmación
                      </Typography>
                    </Box>
                  }
                />
              </Box>
            </Box>

            {updatePosConfigMutation.isPending && (
              <Box sx={{ display: "flex", justifyContent: "center" }}>
                <CircularProgress size={20} />
              </Box>
            )}
          </Box>
        </DialogContent>
        <DialogActions>
          <Button onClick={() => setShowConfigDialog(false)}>Cerrar</Button>
          <Button variant="contained" onClick={handleSaveConfig} disabled={updatePosConfigMutation.isPending}>
            Guardar
          </Button>
        </DialogActions>
      </Dialog>

      {/* Diálogo para crear nuevo cliente */}
      <Dialog
        open={showNuevoClienteDialog}
        onClose={() => {
          setShowNuevoClienteDialog(false);
          setNuevoClienteData(getDefaultClienteData());
          setClienteDuplicado(null);
        }}
        maxWidth="sm"
        fullWidth
      >
        <DialogTitle>
          <Box display="flex" alignItems="center" gap={1}>
            <Icon icon="mdi:account-plus" />
            Nuevo Cliente Rápido
          </Box>
        </DialogTitle>
        <DialogContent>
          <Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
            {/* El documento ya está cargado en el sistema: en vez de dejar que el
                cajero complete todo y choque contra el 409 del backend, se le
                ofrece acá mismo el cliente que estaba buscando. */}
            {clienteDuplicado && (
              <Paper
                variant="outlined"
                sx={{ p: 1.5, borderColor: "warning.main", bgcolor: "warning.50", display: "flex", alignItems: "center", gap: 1.5, flexWrap: "wrap" }}
              >
                <Icon icon="mdi:alert-circle-outline" width={22} />
                <Box sx={{ flex: "1 1 200px", minWidth: 0 }}>
                  <Typography variant="body2" fontWeight={700}>
                    Ya existe un cliente con este documento
                  </Typography>
                  <Typography variant="caption" color="text.secondary">
                    {clienteDuplicado.personas?.razon_social || clienteDuplicado.razon_social}
                    {clienteDuplicado.personas?.ruc
                      ? ` · RUC ${clienteDuplicado.personas.ruc}`
                      : clienteDuplicado.personas?.nro_documento
                        ? ` · CI ${clienteDuplicado.personas.nro_documento}`
                        : ""}
                  </Typography>
                </Box>
                <Button size="small" variant="contained" color="warning" onClick={() => usarClienteExistente(clienteDuplicado)}>
                  Usar este cliente
                </Button>
              </Paper>
            )}

            {/* Naturaleza del receptor */}
            <TextField
              select
              label="Naturaleza *"
              value={nuevoClienteData.naturaleza_id}
              onChange={(e) => {
                const naturalezaId = e.target.value;
                const naturalezas = referencialesData?.naturaleza_receptor || [];
                const naturaleza = naturalezas.find((n) => n.id === naturalezaId);
                const esContribuyente = naturaleza?.codigo === 1;

                // Auto-seleccionar tipo operación según naturaleza
                const tiposOp = referencialesData?.tipo_operacion || [];
                let tipoOpId = "";
                if (esContribuyente) {
                  const b2b = tiposOp.find((t) => t.codigo === 1);
                  tipoOpId = b2b?.id || "";
                } else {
                  const b2c = tiposOp.find((t) => t.codigo === 2);
                  tipoOpId = b2c?.id || "";
                }

                setNuevoClienteData((prev) => ({
                  ...prev,
                  naturaleza_id: naturalezaId,
                  tipo_operacion_id: tipoOpId,
                  // Limpiar campos según tipo
                  ruc: esContribuyente ? prev.ruc : "",
                  dv: esContribuyente ? prev.dv : 0,
                  nro_documento: esContribuyente ? "" : prev.nro_documento,
                  tipo_documento_id: esContribuyente ? "" : prev.tipo_documento_id,
                }));
              }}
              fullWidth
              SelectProps={{ native: true }}
            >
              <option value="">Seleccionar</option>
              {(referencialesData?.naturaleza_receptor || []).map((n) => (
                <option key={n.id} value={n.id}>
                  {n.descripcion}
                </option>
              ))}
            </TextField>

            {/* Campos según naturaleza */}
            {(() => {
              const naturalezas = referencialesData?.naturaleza_receptor || [];
              const naturaleza = naturalezas.find((n) => n.id === nuevoClienteData.naturaleza_id);
              const esContribuyente = naturaleza?.codigo === 1;

              if (esContribuyente) {
                return (
                  <>
                    <Box sx={{ display: "flex", gap: 1 }}>
                      <TextField
                        label="RUC *"
                        value={nuevoClienteData.ruc}
                        onChange={(e) => {
                          const ruc = e.target.value.replace(/\D/g, "");
                          const dv = calcularDV(ruc);
                          // Auto-calcular tipo contribuyente
                          const tiposContrib = referencialesData?.tipo_contribuyente || [];
                          const rucNum = parseInt(ruc, 10);
                          let tipoContribId = "";
                          if (rucNum >= 8000000) {
                            const juridica = tiposContrib.find((t) => t.codigo === 2);
                            tipoContribId = juridica?.id || "";
                          } else if (ruc) {
                            const fisica = tiposContrib.find((t) => t.codigo === 1);
                            tipoContribId = fisica?.id || "";
                          }
                          setNuevoClienteData((prev) => ({
                            ...prev,
                            ruc,
                            dv,
                            tipo_contribuyente_id: tipoContribId,
                          }));
                        }}
                        onBlur={(e) => verificarClienteDuplicado(e.target.value)}
                        sx={{ flex: 1 }}
                        inputProps={{ maxLength: 8 }}
                        InputProps={{
                          endAdornment: loadingDnitPOS ? (
                            <InputAdornment position="end">
                              <CircularProgress size={16} />
                            </InputAdornment>
                          ) : undefined,
                        }}
                        helperText="Se completa solo al reconocer el documento en la DNIT"
                      />
                      <TextField label="DV" value={nuevoClienteData.dv} sx={{ width: 70 }} InputProps={{ readOnly: true }} />
                    </Box>
                  </>
                );
              } else if (nuevoClienteData.naturaleza_id) {
                return (
                  <Box sx={{ display: "flex", gap: 1 }}>
                    <TextField
                      select
                      label="Tipo Doc. *"
                      value={nuevoClienteData.tipo_documento_id}
                      onChange={(e) =>
                        setNuevoClienteData((prev) => ({
                          ...prev,
                          tipo_documento_id: e.target.value,
                        }))
                      }
                      sx={{ width: 150 }}
                      SelectProps={{ native: true }}
                    >
                      <option value="">Seleccionar</option>
                      {(referencialesData?.tipo_documento_identidad || []).map((t) => (
                        <option key={t.id} value={t.id}>
                          {t.descripcion}
                        </option>
                      ))}
                    </TextField>
                    <TextField
                      label="Nro. Documento *"
                      value={nuevoClienteData.nro_documento}
                      onChange={(e) =>
                        setNuevoClienteData((prev) => ({
                          ...prev,
                          nro_documento: e.target.value,
                        }))
                      }
                      onBlur={(e) => verificarClienteDuplicado(e.target.value)}
                      fullWidth
                      helperText="Con cédula, se completa solo el nombre desde el Padrón"
                      InputProps={{
                        endAdornment: loadingDnitPOS ? (
                          <InputAdornment position="end">
                            <CircularProgress size={16} />
                          </InputAdornment>
                        ) : undefined,
                      }}
                    />
                  </Box>
                );
              }
              return null;
            })()}

            <TextField
              label="Razón Social / Nombre *"
              value={nuevoClienteData.razon_social}
              onChange={(e) =>
                setNuevoClienteData((prev) => ({
                  ...prev,
                  razon_social: e.target.value,
                }))
              }
              fullWidth
            />

            <TextField
              select
              label="Tipo Operación *"
              value={nuevoClienteData.tipo_operacion_id}
              onChange={(e) =>
                setNuevoClienteData((prev) => ({
                  ...prev,
                  tipo_operacion_id: e.target.value,
                }))
              }
              fullWidth
              SelectProps={{ native: true }}
            >
              <option value="">Seleccionar</option>
              {(referencialesData?.tipo_operacion || [])
                .filter((t) => {
                  const naturalezas = referencialesData?.naturaleza_receptor || [];
                  const naturaleza = naturalezas.find((n) => n.id === nuevoClienteData.naturaleza_id);
                  const esContribuyente = naturaleza?.codigo === 1;
                  if (esContribuyente) {
                    return t.codigo === 1 || t.codigo === 3; // B2B, B2G
                  } else {
                    return t.codigo === 2 || t.codigo === 4; // B2C, B2F
                  }
                })
                .map((t) => (
                  <option key={t.id} value={t.id}>
                    {t.descripcion}
                  </option>
                ))}
            </TextField>

            <TextField
              label="Celular"
              value={nuevoClienteData.celular}
              onChange={(e) =>
                setNuevoClienteData((prev) => ({
                  ...prev,
                  celular: e.target.value,
                }))
              }
              fullWidth
            />

            <TextField
              label="Correo Electrónico"
              type="email"
              value={nuevoClienteData.email}
              onChange={(e) =>
                setNuevoClienteData((prev) => ({
                  ...prev,
                  email: e.target.value,
                }))
              }
              fullWidth
            />

            <TextField
              label="Dirección"
              value={nuevoClienteData.direccion}
              onChange={(e) =>
                setNuevoClienteData((prev) => ({
                  ...prev,
                  direccion: e.target.value,
                }))
              }
              fullWidth
            />
          </Box>
        </DialogContent>
        <DialogActions sx={{ p: 2 }}>
          <Button
            onClick={() => {
              setShowNuevoClienteDialog(false);
              setNuevoClienteData(getDefaultClienteData());
              setClienteDuplicado(null);
            }}
            disabled={isCreatingCliente}
          >
            Cancelar
          </Button>
          <Button
            variant="contained"
            color="primary"
            disabled={isCreatingCliente}
            startIcon={isCreatingCliente ? <Icon icon="mdi:loading" className="spin" /> : <Icon icon="mdi:check" />}
            onClick={async () => {
              const naturalezas = referencialesData?.naturaleza_receptor || [];
              const naturaleza = naturalezas.find((n) => n.id === nuevoClienteData.naturaleza_id);
              const esContribuyente = naturaleza?.codigo === 1;

              // Validaciones
              if (!nuevoClienteData.naturaleza_id) {
                toast.error("Seleccione la naturaleza del cliente");
                return;
              }
              if (!nuevoClienteData.razon_social) {
                toast.error("Ingrese la razón social o nombre");
                return;
              }
              if (!nuevoClienteData.tipo_operacion_id) {
                toast.error("Seleccione el tipo de operación");
                return;
              }
              if (esContribuyente && !nuevoClienteData.ruc) {
                toast.error("Ingrese el RUC");
                return;
              }
              if (!esContribuyente && (!nuevoClienteData.tipo_documento_id || !nuevoClienteData.nro_documento)) {
                toast.error("Ingrese el tipo y número de documento");
                return;
              }

              setIsCreatingCliente(true);
              try {
                const dataToSend = {
                  razon_social: nuevoClienteData.razon_social,
                  naturaleza_id: nuevoClienteData.naturaleza_id,
                  tipo_operacion_id: nuevoClienteData.tipo_operacion_id,
                  direccion: nuevoClienteData.direccion?.trim() || null,
                  celular: nuevoClienteData.celular?.trim() || null,
                  email: nuevoClienteData.email?.trim() || null,
                };

                if (esContribuyente) {
                  dataToSend.ruc = String(nuevoClienteData.ruc || "").trim();
                  // dv puede venir como string (autocompletado DNIT) → el backend lo valida como entero 0-9.
                  dataToSend.dv = Number(String(nuevoClienteData.dv).replace(/\D/g, "")) || 0;
                  if (nuevoClienteData.tipo_contribuyente_id) {
                    dataToSend.tipo_contribuyente_id = nuevoClienteData.tipo_contribuyente_id;
                  }
                } else {
                  dataToSend.tipo_documento_id = nuevoClienteData.tipo_documento_id;
                  dataToSend.nro_documento = nuevoClienteData.nro_documento;
                }

                const response = await createCliente(dataToSend);
                // El backend responde { message, data: { persona, cliente } }.
                // El id que necesita la factura es el del CLIENTE (cabecera.cliente_id).
                const payload = response?.data || response;
                const clienteObj = payload?.cliente || payload;
                const personaObj = payload?.persona ||
                  clienteObj?.personas || {
                    ruc: dataToSend.ruc,
                    dv: dataToSend.dv,
                    razon_social: dataToSend.razon_social,
                    direccion: dataToSend.direccion,
                    celular: dataToSend.celular,
                    nro_documento: dataToSend.nro_documento,
                  };

                // Seleccionar el cliente creado (con su id UUID real a nivel raíz).
                setClienteSeleccionado({
                  ...clienteObj,
                  id: clienteObj?.id,
                  personas: personaObj,
                  tipo_operacion_id: clienteObj?.tipo_operacion_id || dataToSend.tipo_operacion_id,
                  razon_social: personaObj?.razon_social || dataToSend.razon_social,
                  ruc: personaObj?.ruc || dataToSend.ruc,
                  dv: personaObj?.dv ?? dataToSend.dv,
                });

                toast.success("Cliente creado exitosamente");
                setShowNuevoClienteDialog(false);
                setShowClienteDialog(false);
                setClienteSearch("");
                setClientesResults([]);
                setNuevoClienteData(getDefaultClienteData());
              } catch (err) {
                console.error("Error creando cliente:", err);
                const mensaje = err.response?.data?.message || "Error al crear el cliente";
                // Red de seguridad: si el duplicado se coló igual (otra caja lo
                // creó mientras tanto), mostrar el mismo banner con la salida en
                // vez de dejar al cajero con un toast rojo y sin acción.
                if (err.response?.status === 409) {
                  await verificarClienteDuplicado(esContribuyente ? nuevoClienteData.ruc : nuevoClienteData.nro_documento);
                }
                toast.error(mensaje);
              } finally {
                setIsCreatingCliente(false);
              }
            }}
          >
            {isCreatingCliente ? "Creando..." : "Crear Cliente"}
          </Button>
        </DialogActions>
      </Dialog>

      {/* Diálogo para Abrir Caja */}
      <Dialog open={showAbrirCajaDialog} onClose={() => setShowAbrirCajaDialog(false)} maxWidth="sm" fullWidth>
        <DialogTitle>
          <Box display="flex" alignItems="center" gap={1}>
            <Icon icon="mdi:cash-register" />
            Abrir Caja
          </Box>
        </DialogTitle>
        <DialogContent>
          <Box sx={{ display: "flex", flexDirection: "column", gap: 2, mt: 1 }}>
            <TextField
              select
              label="Seleccionar Caja *"
              value={cajaSeleccionada?.id || ""}
              onChange={(e) => {
                const caja = cajasDisponibles.find((c) => c.id === e.target.value);
                setCajaSeleccionada(caja || null);
              }}
              fullWidth
              SelectProps={{ native: true }}
            >
              <option value="">Seleccionar caja</option>
              {cajasDisponibles.map((caja) => (
                <option key={caja.id} value={caja.id}>
                  {caja.nombre} {caja.sesion_activa ? "(En uso)" : ""}
                </option>
              ))}
            </TextField>

            <TextField
              label="Monto Inicial (Fondo de Caja)"
              value={montoApertura ? parseInt(montoApertura.replace(/\D/g, "") || 0).toLocaleString("es-PY") : ""}
              onChange={(e) => {
                const numericValue = e.target.value.replace(/\D/g, "");
                setMontoApertura(numericValue);
              }}
              fullWidth
              InputProps={{
                startAdornment: <Typography sx={{ mr: 1, color: "text.secondary" }}>Gs.</Typography>,
              }}
              helperText="Ingrese el monto con el que inicia la caja"
            />
          </Box>
        </DialogContent>
        <DialogActions sx={{ p: 2 }}>
          <Button onClick={() => setShowAbrirCajaDialog(false)} disabled={isLoadingCaja}>
            Cancelar
          </Button>
          <Button
            variant="contained"
            color="success"
            onClick={handleAbrirCaja}
            disabled={isLoadingCaja || !cajaSeleccionada}
            startIcon={isLoadingCaja ? <Icon icon="mdi:loading" className="spin" /> : <Icon icon="mdi:lock-open" />}
          >
            {isLoadingCaja ? "Abriendo..." : "Abrir Caja"}
          </Button>
        </DialogActions>
      </Dialog>

      {/* Diálogo de confirmación de impresión de ticket */}
      <Dialog
        open={ticketDialog.open}
        onClose={() => {}}
        maxWidth="sm"
        fullWidth
        /* Enter cierra y vuelve al escáner. Va en el diálogo y no solo en el
           foco del botón porque el <object> del PDF puede quedarse con el foco. */
        onKeyDown={(e) => {
          if (e.key === "Enter") {
            e.preventDefault();
            finalizarTicket();
          }
        }}
        PaperProps={{ sx: { borderRadius: 3, overflow: "hidden" } }}
        TransitionProps={{
          onEntered: () => finalizarBtnRef.current?.focus(),
        }}
      >
        <Box
          sx={{
            display: "flex",
            flexDirection: "column",
            alignItems: "center",
            gap: 0.5,
            pt: 3,
            pb: 2,
            px: 3,
            background: "linear-gradient(180deg, rgba(76,175,80,0.12) 0%, rgba(76,175,80,0) 100%)",
          }}
        >
          <Box
            sx={{
              width: 56,
              height: 56,
              borderRadius: "50%",
              display: "flex",
              alignItems: "center",
              justifyContent: "center",
              bgcolor: "#4caf50",
              mb: 0.5,
            }}
          >
            <Icon icon="mdi:check-bold" width={30} color="#fff" />
          </Box>
          <Typography variant="h6" fontWeight={700}>
            Venta registrada
          </Typography>
          <Typography variant="body2" color="text.secondary" textAlign="center">
            Revisá el ticket antes de continuar
          </Typography>
        </Box>
        <DialogContent sx={{ pt: 0 }}>
          {ticketDialog.pdfUrl ? (
            <Box
              sx={{
                border: "1px solid",
                borderColor: "divider",
                borderRadius: 2,
                overflow: "hidden",
                boxShadow: "0 2px 10px rgba(0,0,0,0.12)",
                bgcolor: "#fff",
              }}
            >
              <object data={ticketDialog.pdfUrl} type="application/pdf" width="100%" height="460" aria-label="Vista previa del ticket">
                <Typography variant="body2" color="text.secondary" sx={{ p: 2 }}>
                  No se pudo previsualizar el ticket. Usá &quot;Reimprimir&quot; para verlo.
                </Typography>
              </object>
            </Box>
          ) : (
            <Typography variant="body2" color="text.secondary">
              ¿El ticket se imprimió correctamente?
            </Typography>
          )}
        </DialogContent>
        {/* Reimprimir a la izquierda, Finalizar a la derecha: son acciones
            opuestas, no una lista de opciones equivalentes. */}
        <DialogActions sx={{ px: 3, pb: 2.5, justifyContent: "space-between" }}>
          <Button
            variant="outlined"
            size="large"
            sx={{ px: 3 }}
            startIcon={<Icon icon="mdi:printer" />}
            onClick={async () => {
              if (ticketDialog.pdfUrl) {
                try {
                  const printerName = config.printer_name || null;
                  const paperWidth = config.printer_paper_width || 80;
                  const response = await fetch(ticketDialog.pdfUrl);
                  const blob = await response.blob();
                  await printTicketWithFallback({
                    pdfBlob: blob,
                    printerName,
                    paperWidth,
                  });
                } catch (err) {
                  console.error("Error al reimprimir:", err);
                  const printWindow = window.open(ticketDialog.pdfUrl, "_blank");
                  if (printWindow) {
                    printWindow.addEventListener("load", () => {
                      printWindow.print();
                    });
                  }
                }
              }
            }}
          >
            Reimprimir
          </Button>
          <Button
            ref={finalizarBtnRef}
            variant="contained"
            size="large"
            startIcon={<Icon icon="mdi:check" />}
            sx={{
              bgcolor: "#4caf50",
              px: 3,
              fontWeight: 700,
              "&:hover": { bgcolor: "#388e3c" },
            }}
            onClick={finalizarTicket}
          >
            Finalizar (Enter)
          </Button>
        </DialogActions>
      </Dialog>

      {/* Diálogo de reintento de impresión */}
      <Dialog open={printRetryDialog.open} onClose={() => setPrintRetryDialog({ open: false, retryFn: null, error: "" })} maxWidth="xs" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
        <DialogTitle sx={{ display: "flex", alignItems: "center", gap: 1 }}>
          <Icon icon="mdi:printer-alert" width={28} color="#f44336" />
          Error de impresión
        </DialogTitle>
        <DialogContent>
          <Typography sx={{ mb: 1 }}>No se pudo imprimir el ticket.</Typography>
          <Typography variant="body2" color="text.secondary">
            {printRetryDialog.error}
          </Typography>
        </DialogContent>
        <DialogActions sx={{ px: 3, pb: 2, gap: 1 }}>
          <Button variant="outlined" color="inherit" onClick={() => setPrintRetryDialog({ open: false, retryFn: null, error: "" })} startIcon={<Icon icon="mdi:close" />}>
            Cancelar
          </Button>
          <Button
            variant="contained"
            color="primary"
            onClick={() => {
              if (printRetryDialog.retryFn) printRetryDialog.retryFn();
            }}
            startIcon={<Icon icon="mdi:printer" />}
          >
            Reintentar
          </Button>
        </DialogActions>
      </Dialog>

      {/* Diálogo de Transacciones Recientes */}
      <Dialog open={showTransaccionesDialog} onClose={() => setShowTransaccionesDialog(false)} maxWidth="md" fullWidth PaperProps={{ sx: { maxHeight: "85vh" } }}>
        <DialogTitle sx={{ display: "flex", alignItems: "center", gap: 1, pb: 1 }}>
          <Icon icon="mdi:receipt-text-outline" width={24} color="#f88533" />
          <Typography variant="h6" fontWeight={700} sx={{ flex: 1 }}>
            Transacciones del Día
          </Typography>
          <IconButton size="small" onClick={() => loadTransacciones(transaccionesPage)}>
            <Icon icon="mdi:refresh" width={20} />
          </IconButton>
          <IconButton size="small" onClick={() => setShowTransaccionesDialog(false)}>
            <Icon icon="mdi:close" width={20} />
          </IconButton>
        </DialogTitle>
        <DialogContent dividers sx={{ p: 0 }}>
          {transaccionesLoading ? (
            <Box display="flex" justifyContent="center" alignItems="center" py={6}>
              <CircularProgress size={32} />
              <Typography sx={{ ml: 2 }} color="text.secondary">
                Cargando transacciones...
              </Typography>
            </Box>
          ) : transacciones.length === 0 ? (
            <Box display="flex" flexDirection="column" alignItems="center" py={6} gap={1}>
              <Icon icon="mdi:receipt-text-remove-outline" width={48} style={{ opacity: 0.3 }} />
              <Typography color="text.secondary">No hay transacciones registradas hoy</Typography>
            </Box>
          ) : (
            <Box sx={{ overflowX: "auto" }}>
              <table style={{ width: "100%", borderCollapse: "collapse" }}>
                <thead>
                  <tr style={{ borderBottom: "2px solid rgba(255,255,255,0.1)" }}>
                    <th
                      style={{
                        padding: "10px 16px",
                        textAlign: "left",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        textTransform: "uppercase",
                        opacity: 0.7,
                      }}
                    >
                      Fecha
                    </th>
                    <th
                      style={{
                        padding: "10px 16px",
                        textAlign: "left",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        textTransform: "uppercase",
                        opacity: 0.7,
                      }}
                    >
                      Nro.
                    </th>
                    <th
                      style={{
                        padding: "10px 16px",
                        textAlign: "left",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        textTransform: "uppercase",
                        opacity: 0.7,
                      }}
                    >
                      Cliente
                    </th>
                    <th
                      style={{
                        padding: "10px 16px",
                        textAlign: "right",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        textTransform: "uppercase",
                        opacity: 0.7,
                      }}
                    >
                      Monto
                    </th>
                    <th
                      style={{
                        padding: "10px 16px",
                        textAlign: "center",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        textTransform: "uppercase",
                        opacity: 0.7,
                      }}
                    >
                      Moneda
                    </th>
                    <th
                      style={{
                        padding: "10px 16px",
                        textAlign: "center",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        textTransform: "uppercase",
                        opacity: 0.7,
                      }}
                    >
                      Estado
                    </th>
                    <th
                      style={{
                        padding: "10px 16px",
                        textAlign: "center",
                        fontSize: "0.75rem",
                        fontWeight: 600,
                        textTransform: "uppercase",
                        opacity: 0.7,
                      }}
                    >
                      Acciones
                    </th>
                  </tr>
                </thead>
                <tbody>
                  {transacciones.map((tx) => {
                    const fecha = tx.dfeemide || tx.created_at;
                    const nroDoc = tx.dnumdoc ? `${tx.dest || ""}-${tx.dpunexp || ""}-${tx.dnumdoc}` : "Sin número";
                    const clienteNombre = tx.clientes?.personas?.razon_social || tx.clientes?.personas?.nro_documento || "Sin nombre";
                    const subtotales = Array.isArray(tx.factura_subtotales) ? tx.factura_subtotales[0] : tx.factura_subtotales;
                    const monto = subtotales?.dtotgralope ?? subtotales?.dtotope ?? 0;
                    const monedaCodigo = tx.moneda?.codigo || "PYG";
                    const estado = tx.estado || "Pendiente";
                    const estadoLower = estado.toLowerCase();
                    const estadoColor = estadoLower === "anulada" ? "#ef4444" : estadoLower === "pagada" || estadoLower === "enviada" ? "#10b981" : "#f59e0b";

                    return (
                      <tr
                        key={tx.id}
                        style={{
                          borderBottom: "1px solid rgba(255,255,255,0.05)",
                        }}
                      >
                        <td
                          style={{
                            padding: "10px 16px",
                            fontSize: "0.82rem",
                            whiteSpace: "nowrap",
                          }}
                        >
                          {fecha
                            ? new Date(fecha).toLocaleString("es-PY", {
                                hour: "2-digit",
                                minute: "2-digit",
                                day: "2-digit",
                                month: "2-digit",
                              })
                            : "-"}
                        </td>
                        <td
                          style={{
                            padding: "10px 16px",
                            fontSize: "0.82rem",
                            fontFamily: "monospace",
                            whiteSpace: "nowrap",
                          }}
                        >
                          {nroDoc}
                        </td>
                        <td
                          style={{
                            padding: "10px 16px",
                            fontSize: "0.82rem",
                            maxWidth: 180,
                            overflow: "hidden",
                            textOverflow: "ellipsis",
                            whiteSpace: "nowrap",
                          }}
                        >
                          {clienteNombre}
                        </td>
                        <td
                          style={{
                            padding: "10px 16px",
                            fontSize: "0.82rem",
                            textAlign: "right",
                            fontWeight: 600,
                            whiteSpace: "nowrap",
                          }}
                        >
                          {FormatearNumeroDinero(monto, monedaCodigo, "PY")}
                        </td>
                        <td
                          style={{
                            padding: "10px 16px",
                            fontSize: "0.75rem",
                            textAlign: "center",
                          }}
                        >
                          <Chip label={monedaCodigo} size="small" variant="outlined" sx={{ fontSize: "0.7rem", height: 22 }} />
                        </td>
                        <td style={{ padding: "10px 16px", textAlign: "center" }}>
                          <Chip
                            label={estado}
                            size="small"
                            sx={{
                              bgcolor: estadoColor + "20",
                              color: estadoColor,
                              fontWeight: 600,
                              fontSize: "0.7rem",
                              height: 22,
                              textTransform: "capitalize",
                            }}
                          />
                        </td>
                        <td
                          style={{
                            padding: "6px 16px",
                            textAlign: "center",
                            whiteSpace: "nowrap",
                          }}
                        >
                          <IconButton size="small" title="Reimprimir ticket" onClick={() => handleReprintTicket(tx.id)} disabled={reprintingId === tx.id} sx={{ color: "#f88533" }}>
                            {reprintingId === tx.id ? <CircularProgress size={16} /> : <Icon icon="mdi:printer" width={18} />}
                          </IconButton>
                          <IconButton size="small" title="Ver ticket" onClick={() => handleViewTicket(tx.id)} disabled={reprintingId === tx.id} sx={{ color: "text.secondary" }}>
                            <Icon icon="mdi:eye-outline" width={18} />
                          </IconButton>
                          <IconButton
                            size="small"
                            title={!puedeAnularFactura ? "No tenés permiso para anular facturas" : "Anular factura"}
                            onClick={() => handleAnularFactura(tx)}
                            disabled={!puedeAnularFactura || estadoLower === "anulada" || anulacionLoading}
                            sx={{
                              color: !puedeAnularFactura || estadoLower === "anulada" ? "text.disabled" : "#ef4444",
                            }}
                          >
                            <Icon icon="mdi:cancel" width={18} />
                          </IconButton>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
              </table>
            </Box>
          )}
        </DialogContent>
        {transaccionesTotalPages > 1 && (
          <DialogActions sx={{ justifyContent: "center", gap: 2 }}>
            <Button size="small" disabled={transaccionesPage <= 1 || transaccionesLoading} onClick={() => loadTransacciones(transaccionesPage - 1)} startIcon={<Icon icon="mdi:chevron-left" />}>
              Anterior
            </Button>
            <Typography variant="body2" color="text.secondary">
              Página {transaccionesPage} de {transaccionesTotalPages}
            </Typography>
            <Button
              size="small"
              disabled={transaccionesPage >= transaccionesTotalPages || transaccionesLoading}
              onClick={() => loadTransacciones(transaccionesPage + 1)}
              endIcon={<Icon icon="mdi:chevron-right" />}
            >
              Siguiente
            </Button>
          </DialogActions>
        )}
      </Dialog>

      {/* Diálogo para Cerrar Caja / Arqueo */}
      <CerrarCajaDialog
        open={showCerrarCajaDialog}
        onClose={() => setShowCerrarCajaDialog(false)}
        sesionCaja={sesionCaja}
        cajaDescripcion={cajaSeleccionada?.descripcion}
        mediosPago={mediosPago}
        montoCierre={montoCierre}
        setMontoCierre={setMontoCierre}
        observacionesCierre={observacionesCierre}
        setObservacionesCierre={setObservacionesCierre}
        onCerrarCaja={handleCerrarCaja}
        isLoading={isLoadingCaja}
        requiereArqueo={!!posConfig?.requiere_arqueo_cierre}
      />

      {/* Fase 2: Diálogo de crédito bloqueado con override */}
      <Dialog
        open={showCreditoBlockDialog}
        onClose={() => {
          setShowCreditoBlockDialog(false);
          setCreditoOverridePin("");
          setCreditoOverrideCallback(null);
        }}
        maxWidth="xs"
        fullWidth
        PaperProps={{ sx: { borderRadius: 3 } }}
      >
        <DialogTitle
          sx={{
            display: "flex",
            alignItems: "center",
            gap: 1,
            color: "error.main",
          }}
        >
          <Icon icon="mdi:credit-card-off-outline" width={24} />
          Crédito Bloqueado
        </DialogTitle>
        <DialogContent>
          <Typography variant="body2" sx={{ mb: 2 }}>
            El cliente <strong>{clienteSeleccionado?.razon_social}</strong> tiene el crédito bloqueado.
          </Typography>
          {creditoInfo && (
            <Box
              sx={{
                bgcolor: "error.50",
                border: "1px solid",
                borderColor: "error.200",
                borderRadius: 2,
                p: 1.5,
                mb: 2,
              }}
            >
              <Typography variant="caption" color="text.secondary">
                Límite: <strong>{textoTope(creditoInfo.limite_credito)}</strong>
              </Typography>
              <br />
              <Typography variant="caption" color="text.secondary">
                Saldo pendiente: <strong style={{ color: "#ef4444" }}>{fmtCredito(creditoInfo.saldo_pendiente || 0)}</strong>
              </Typography>
              {creditoInfo.motivo_bloqueo && (
                <>
                  <br />
                  <Typography variant="caption" color="text.secondary">
                    Motivo: {creditoInfo.motivo_bloqueo}
                  </Typography>
                </>
              )}
            </Box>
          )}
          <Typography variant="body2" sx={{ mb: 1.5, fontWeight: 500 }}>
            Ingrese PIN de supervisor para continuar:
          </Typography>
          <TextField
            type="password"
            fullWidth
            size="small"
            value={creditoOverridePin}
            onChange={(e) => setCreditoOverridePin(e.target.value)}
            onKeyDown={(e) => {
              if (e.key === "Enter" && creditoOverridePin) {
                handleCreditoOverride();
              }
            }}
            placeholder="PIN supervisor"
            autoFocus
            inputProps={{ maxLength: 10 }}
          />
        </DialogContent>
        <DialogActions sx={{ px: 3, pb: 2 }}>
          <Button
            onClick={() => {
              setShowCreditoBlockDialog(false);
              setCreditoOverridePin("");
              setCreditoOverrideCallback(null);
            }}
          >
            Cancelar
          </Button>
          <Button variant="contained" color="warning" onClick={handleCreditoOverride} disabled={!creditoOverridePin} startIcon={<Icon icon="mdi:shield-key-outline" />}>
            Override
          </Button>
        </DialogActions>
      </Dialog>

      {/* Dialog de confirmacion de anulacion */}
      <Dialog open={anulacionDialog.open} onClose={() => !anulacionLoading && setAnulacionDialog({ open: false, factura: null })} maxWidth="sm" fullWidth PaperProps={{ sx: { borderRadius: 3 } }}>
        <DialogTitle sx={{ display: "flex", alignItems: "center", gap: 1 }}>
          <Icon icon="mdi:cancel" width={24} color="#ef4444" />
          Anular Factura
        </DialogTitle>
        <DialogContent>
          {anulacionDialog.factura && (
            <Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}>
              Factura:{" "}
              <strong>
                {anulacionDialog.factura.timbrado_numero_establecimiento || ""}-{anulacionDialog.factura.timbrado_punto_expedicion || ""}-
                {String(anulacionDialog.factura.numero_factura || "").padStart(7, "0")}
              </strong>
            </Typography>
          )}
          <Typography variant="body2" sx={{ mb: 2, color: "warning.main", fontWeight: 500 }}>
            Esta accion no se puede deshacer. Se revertiran los movimientos de caja e inventario.
          </Typography>
          <TextField
            label="Motivo de anulacion"
            fullWidth
            multiline
            rows={3}
            value={anulacionMotivo}
            onChange={(e) => setAnulacionMotivo(e.target.value)}
            disabled={anulacionLoading}
            placeholder="Ingrese el motivo de la anulacion..."
            autoFocus
          />
        </DialogContent>
        <DialogActions sx={{ px: 3, pb: 2 }}>
          <Button onClick={() => setAnulacionDialog({ open: false, factura: null })} disabled={anulacionLoading}>
            Cancelar
          </Button>
          <Button
            variant="contained"
            color="error"
            onClick={handleConfirmAnulacion}
            disabled={anulacionLoading || !anulacionMotivo.trim()}
            startIcon={anulacionLoading ? <CircularProgress size={16} color="inherit" /> : <Icon icon="mdi:cancel" />}
          >
            {anulacionLoading ? "Anulando..." : "Confirmar Anulacion"}
          </Button>
        </DialogActions>
      </Dialog>

      {/* Fase 1: Modal PIN Supervisor */}
      <ModalPinSupervisor
        open={showPinModal}
        onClose={() => setShowPinModal(false)}
        onSuccess={handlePinSuccess}
        tipoOperacion={pinOperacion || "apertura_caja"}
        contexto={{ caja_id: cajaSeleccionada?.id || sesionCaja?.caja_id }}
      />
    </Container>
  );
}

// Styled Components
const Container = styled.div`
  display: flex;
  flex-direction: column;
  height: 100vh;
  background: ${({ theme }) => theme.bg};
  overflow: hidden;
`;

const CotizacionAlert = styled.div`
  background: ${({ theme }) => theme.bg2};
  border: 1px solid #5c6bc0;
  border-left: 4px solid #5c6bc0;
  border-radius: 8px;
  padding: 14px 18px;
  margin: 8px 16px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 16px;
  flex-wrap: wrap;

  .alert-content {
    display: flex;
    align-items: flex-start;
    gap: 12px;
    color: ${({ theme }) => theme.text};

    svg {
      flex-shrink: 0;
      margin-top: 2px;
      color: #5c6bc0;
    }

    .alert-text {
      strong {
        display: block;
        font-size: 0.9rem;
        margin-bottom: 4px;
        color: #5c6bc0;
      }

      p {
        margin: 0;
        font-size: 0.8rem;
        opacity: 0.8;
        line-height: 1.5;
      }
    }
  }

  .alert-actions {
    display: flex;
    gap: 8px;
    flex-shrink: 0;

    button {
      display: flex;
      align-items: center;
      gap: 6px;
      padding: 8px 14px;
      border-radius: 6px;
      font-size: 0.8rem;
      font-weight: 500;
      cursor: pointer;
      transition: all 0.2s;
      border: none;

      &.accept {
        background: #5c6bc0;
        color: white;
        &:hover {
          background: #3f51b5;
        }
      }

      &.reject {
        background: transparent;
        border: 1px solid ${({ theme }) => theme.color2};
        color: ${({ theme }) => theme.text};
        &:hover {
          background: ${({ theme }) => theme.bg3};
        }
      }
    }
  }
`;

const TopInfoBar = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 6px 12px;
  background: ${({ theme }) => theme.bg3};
  border-bottom: 1px solid ${({ theme }) => theme.color2};
  min-height: 44px;
  flex-shrink: 0;

  .info-left,
  .info-right {
    display: flex;
    align-items: center;
    gap: 16px;
  }

  .info-item {
    display: flex;
    align-items: center;
    gap: 6px;
    font-size: 13px;
    color: ${({ theme }) => theme.textSecondary};

    svg {
      font-size: 18px;
      color: ${({ theme }) => theme.primary};
    }

    &.user {
      font-weight: 600;
      color: ${({ theme }) => theme.text};
    }

    &.time {
      font-family: monospace;
      font-size: 14px;
    }
  }

  @media (max-width: 768px) {
    padding: 4px 8px;
    gap: 8px;

    .info-right {
      display: none;
    }

    .info-left {
      gap: 8px;
      overflow-x: auto;
      flex-wrap: nowrap;

      &::-webkit-scrollbar {
        display: none;
      }
    }

    .info-item {
      font-size: 11px;
      white-space: nowrap;

      &.user,
      &.time {
        display: none;
      }
    }
  }
`;

const MainContent = styled.div`
  display: flex;
  flex: 1;
  overflow: hidden;

  @media (max-width: 768px) {
    flex-direction: column;
  }
`;

const LeftPanel = styled.div`
  flex: 1;
  display: flex;
  flex-direction: column;
  padding: 16px;
  gap: 12px;
  overflow: hidden;
  min-width: 0;

  @media (max-width: 768px) {
    padding: 8px;
    gap: 8px;
    flex: 0 0 auto;
  }
`;

const SearchBar = styled.div`
  /* Es el motor real del POS: casi toda venta entra por escáner o código, así
     que tiene que ser el elemento más prominente del área de productos. */
  display: flex;
  align-items: center;
  gap: 14px;
  padding: 16px 20px;
  background: ${({ theme }) => theme.bg2};
  border-radius: 14px;
  border: 2px solid ${({ theme }) => theme.borderColor};
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
  transition: all 0.2s;
  flex-shrink: 0;

  &:focus-within {
    border-color: ${({ theme }) => theme.primary};
    box-shadow: 0 0 0 4px ${({ theme }) => `color-mix(in srgb, ${theme.primary} 18%, transparent)`};
  }

  .icon {
    font-size: 28px;
    color: ${({ theme }) => theme.primary};
    flex-shrink: 0;
  }

  input {
    flex: 1;
    border: none;
    background: transparent;
    font-size: 20px;
    font-weight: 500;
    color: ${({ theme }) => theme.text};
    outline: none;
    min-width: 0;

    &::placeholder {
      color: ${({ theme }) => theme.textSecondary};
      font-weight: 400;
    }
  }

  @media (max-width: 768px) {
    padding: 12px 14px;
    gap: 10px;

    .icon {
      font-size: 22px;
    }

    input {
      font-size: 16px;
    }
  }
`;

const UltimoAgregadoStrip = styled.div`
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 12px;
  border-radius: 10px;
  background: ${({ theme }) => `color-mix(in srgb, #22c55e 12%, ${theme.bg2})`};
  border: 1px solid ${({ theme }) => `color-mix(in srgb, #22c55e 35%, ${theme.borderColor})`};
  flex-shrink: 0;

  .ok {
    font-size: 20px;
    color: #22c55e;
    flex-shrink: 0;
  }

  .name {
    font-weight: 700;
    font-size: 14px;
    color: ${({ theme }) => theme.text};
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
  }

  /* Contador de escaneos repetidos del mismo producto: es la señal de que el
     segundo beep entró, que antes daba el toast. */
  .cantidad {
    font-size: 13px;
    font-weight: 800;
    color: #22c55e;
    background: ${({ theme }) => `color-mix(in srgb, #22c55e 18%, ${theme.bg2})`};
    border-radius: 999px;
    padding: 1px 8px;
    flex-shrink: 0;
  }

  .oferta {
    font-size: 11px;
    font-weight: 600;
    color: #f59e0b;
    white-space: nowrap;
  }

  .precio {
    margin-left: auto;
    font-weight: 700;
    font-size: 15px;
    color: ${({ theme }) => theme.primary};
    white-space: nowrap;
  }
`;

const SearchResultsPanel = styled.div`
  background: ${({ theme }) => theme.bg2};
  border: 1px solid ${({ theme }) => theme.borderColor};
  border-radius: 12px;
  box-shadow: 0 8px 24px rgba(0, 0, 0, 0.15);
  overflow: hidden;
`;

const SearchResultsHeader = styled.div`
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 14px;
  background: ${({ theme }) => `color-mix(in srgb, ${theme.primary} 12%, ${theme.bg2})`};
  border-bottom: 1px solid ${({ theme }) => theme.borderColor};
  font-size: 12.5px;
  font-weight: 600;
  color: ${({ theme }) => theme.primary};

  svg {
    font-size: 15px;
    flex-shrink: 0;
  }

  kbd {
    margin-left: auto;
    display: inline-flex;
    align-items: center;
    padding: 1px 6px;
    border-radius: 4px;
    background: ${({ theme }) => theme.bg4};
    color: ${({ theme }) => theme.text};
    font-family: monospace;
    font-size: 10.5px;
    font-weight: 600;
  }

  .hint {
    color: ${({ theme }) => theme.textSecondary};
    font-weight: 500;
  }

  @media (max-width: 768px) {
    .hint {
      display: none;
    }
  }
`;

const SearchResults = styled.div`
  display: flex;
  flex-direction: column;
  gap: 6px;
  max-height: 300px;
  overflow-y: auto;
  padding: 8px;

  @media (max-width: 768px) {
    max-height: 45vh;
  }
`;

const ProductCard = styled.div`
  position: relative;
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px;
  padding-right: ${({ $primary }) => ($primary ? "76px" : "12px")};
  background: ${({ $primary, theme }) => ($primary ? `color-mix(in srgb, ${theme.primary} 10%, ${theme.bg})` : theme.bg)};
  border: 1px solid ${({ $primary, theme }) => ($primary ? theme.primary : "transparent")};
  border-radius: 8px;
  cursor: pointer;
  transition: all 0.2s;

  &:hover {
    background: ${({ theme }) => theme.bg4};
    transform: translateX(4px);
  }

  .enter-chip {
    position: absolute;
    top: 50%;
    right: 10px;
    transform: translateY(-50%);
    display: flex;
    align-items: center;
    gap: 3px;
    padding: 3px 8px;
    border-radius: 12px;
    background: ${({ theme }) => theme.primary};
    color: #fff;
    font-size: 10px;
    font-weight: 700;
    white-space: nowrap;

    svg {
      font-size: 12px;
    }
  }

  .product-image {
    width: 48px;
    height: 48px;
    border-radius: 8px;
    overflow: hidden;
    flex-shrink: 0;
    display: flex;
    align-items: center;
    justify-content: center;

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    &.placeholder {
      background: ${({ theme }) => theme.bg4};
      color: ${({ theme }) => theme.textSecondary};
    }
  }

  .info {
    flex: 1;
    display: flex;
    flex-direction: column;
    min-width: 0;

    .name {
      font-weight: 600;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }

    .code {
      font-size: 12px;
      color: ${({ theme }) => theme.textSecondary};
    }
  }

  .details {
    display: flex;
    flex-direction: column;
    align-items: flex-end;
    gap: 4px;

    .price {
      font-weight: 700;
      color: ${({ theme }) => theme.primary};
      white-space: nowrap;
    }

    .stock {
      font-size: 11px;
      padding: 2px 8px;
      border-radius: 4px;
      background: ${({ theme }) => theme.success || "#22c55e"};
      color: white;

      &.out {
        background: ${({ theme }) => theme.error || "#ef4444"};
      }
    }
  }
`;

const CategoriesBar = styled.div`
  display: flex;
  gap: 6px;
  overflow-x: auto;
  padding: 10px 8px;
  background: ${({ theme }) => theme.bg2};
  border-top: 1px solid ${({ theme }) => theme.color2};
  margin: 0 -16px -16px -16px;
  flex-shrink: 0;

  &::-webkit-scrollbar {
    height: 3px;
  }

  @media (max-width: 768px) {
    display: none;
  }
`;

const CategoryButton = styled.button`
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  padding: 10px 16px;
  min-width: 90px;
  border: none;
  border-radius: 20px;
  background: ${({ $active, $color, theme }) => ($active ? $color || theme.primary : theme.bg3)};
  color: ${({ $active }) => ($active ? "#fff" : "inherit")};
  cursor: pointer;
  transition: all 0.2s;
  font-size: 13px;
  font-weight: 500;
  white-space: nowrap;

  &:hover {
    background: ${({ $active, $color, theme }) => ($active ? $color || theme.primary : theme.bg4)};
    transform: scale(1.02);
  }

  svg {
    font-size: 18px;
  }
`;

const LoadMoreSentinel = styled.div`
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: 40px;
  font-size: 13px;
  color: ${({ theme }) => theme.textSecondary};

  .spin {
    animation: spin 1s linear infinite;
  }

  @keyframes spin {
    to {
      transform: rotate(360deg);
    }
  }
`;

const EmptyProducts = styled.div`
  grid-column: 1 / -1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 12px;
  padding: 60px 20px;
  color: ${({ theme }) => theme.textSecondary};

  svg {
    font-size: 64px;
    opacity: 0.3;
  }

  span {
    font-size: 18px;
    font-weight: 600;
  }

  small {
    font-size: 14px;
    opacity: 0.7;
  }
`;

const ProductsGrid = styled.div`
  flex: 1;
  display: grid;
  grid-template-columns: ${({ $columns, $tileSize }) => ($columns > 0 ? `repeat(${$columns}, ${$tileSize}px)` : `repeat(auto-fill, minmax(${$tileSize}px, 1fr))`)};
  grid-auto-rows: ${({ $tileSize }) => $tileSize}px;
  gap: 8px;
  overflow-y: auto;
  padding: 8px;
  align-content: start;

  @media (max-width: 768px) {
    display: none;
  }
`;

const ProductTile = styled.div`
  position: relative;
  display: flex;
  flex-direction: column;
  /* Color neutro único: el catálogo es soporte, no protagonista. El acento de
     categoría (si existe) queda solo como una línea fina superior, suficiente
     para agrupar visualmente sin convertir la grilla en un mosaico de colores. */
  /* theme.bg3 en vez de bg2: en modo oscuro es un negro un poco menos cerrado
     (#1a1a1a vs #141414), que contrasta menos crudo contra el texto casi-blanco
     — el bg2 puro + texto blanco 700 se sentía como negro-sobre-blanco invertido
     y "chocaba". En claro la diferencia es imperceptible. */
  background: ${({ theme }) => theme.bg3};
  border: 1px solid ${({ theme }) => theme.borderColor};
  border-top: ${({ $accent, theme }) => ($accent ? `3px solid ${$accent}` : `1px solid ${theme.borderColor}`)};
  border-radius: 10px;
  overflow: hidden;
  cursor: pointer;
  transition: all 0.15s;
  min-height: ${({ $hasImage }) => ($hasImage ? "140px" : "100px")};
  /* Texto ligeramente atenuado (92%) en vez de blanco/negro puro: baja la
     dureza del contraste sin perder legibilidad. */
  color: ${({ $hasImage, theme }) => ($hasImage ? "#fff" : `color-mix(in srgb, ${theme.text} 92%, ${theme.bg3})`)};

  &:hover {
    transform: translateY(-2px);
    box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
    border-color: ${({ theme }) => theme.primary};
  }

  &:active {
    transform: scale(0.97);
  }

  .tile-image {
    position: absolute;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    z-index: 0;

    img {
      width: 100%;
      height: 100%;
      object-fit: cover;
    }

    &::after {
      content: "";
      position: absolute;
      top: 0;
      left: 0;
      right: 0;
      bottom: 0;
      background: linear-gradient(to bottom, rgba(0, 0, 0, 0.1) 0%, rgba(0, 0, 0, 0.5) 100%);
    }
  }

  .tile-header {
    padding: 6px 8px;
    display: flex;
    justify-content: flex-end;
    position: relative;
    z-index: 1;

    .code {
      font-size: 10px;
      opacity: ${({ $hasImage }) => ($hasImage ? 0.8 : 1)};
      background: ${({ $hasImage, theme }) => ($hasImage ? "rgba(0, 0, 0, 0.2)" : theme.bg4)};
      color: ${({ $hasImage, theme }) => ($hasImage ? "inherit" : theme.textSecondary)};
      padding: 2px 6px;
      border-radius: 4px;
    }
  }

  .tile-content {
    flex: 1;
    padding: 0 10px;
    display: flex;
    align-items: center;
    justify-content: center;
    position: relative;
    z-index: 1;

    .name {
      font-size: 13px;
      font-weight: 600;
      line-height: 1.3;
      text-align: center;
      display: -webkit-box;
      -webkit-line-clamp: 3;
      -webkit-box-orient: vertical;
      overflow: hidden;
      text-shadow: ${({ $hasImage }) => ($hasImage ? "0 1px 2px rgba(0, 0, 0, 0.3)" : "none")};
    }
  }

  .tile-footer {
    padding: 8px 10px;
    display: flex;
    justify-content: space-between;
    align-items: center;
    background: ${({ $hasImage }) => ($hasImage ? "rgba(0, 0, 0, 0.15)" : "transparent")};
    position: relative;
    z-index: 1;

    .price {
      font-weight: 700;
      font-size: 14px;
    }

    .stock {
      font-size: 11px;
      font-weight: 600;
      padding: 2px 6px;
      border-radius: 4px;
      /* Colores semánticos constantes (ver docs/ui-standards.md): verde
         disponible / rojo sin stock — con fondo al 20% para que se lea bien
         tanto sobre la foto (footer con scrim oscuro) como sobre el pastel. */
      background: ${({ $hasImage }) => ($hasImage ? "rgba(34, 197, 94, 0.3)" : "#22c55e26")};
      color: ${({ $hasImage }) => ($hasImage ? "#fff" : "#16803c")};

      &.out {
        background: ${({ $hasImage }) => ($hasImage ? "rgba(239, 68, 68, 0.55)" : "#ef444426")};
        color: ${({ $hasImage }) => ($hasImage ? "#fff" : "#dc2626")};
      }
    }
  }
`;

const RightPanel = styled.div`
  width: 480px;
  display: flex;
  flex-direction: column;
  /* El carrito es el protagonista de la pantalla: se diferencia del área de
     productos con fondo propio + sombra para que se lea como un panel elevado,
     no como una continuación del catálogo. */
  background: ${({ theme }) => theme.bg2};
  border-left: 2px solid ${({ theme }) => theme.borderColor};
  box-shadow: -4px 0 16px rgba(0, 0, 0, 0.06);
  flex-shrink: 0;

  @media (max-width: 768px) {
    width: 100%;
    border-left: none;
    border-top: 2px solid ${({ theme }) => theme.color2};
    flex: 1;
    overflow: hidden;
  }
`;

const CartHeader = styled.div`
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 10px 14px;
  border-bottom: 1px solid ${({ theme }) => theme.color2};
  flex-shrink: 0;

  @media (max-width: 768px) {
    padding: 8px 12px;
  }
`;

const ClienteSection = styled.div`
  display: flex;
  align-items: center;
  gap: 12px;
  padding: 12px 16px;
  background: ${({ theme }) => theme.bg4};
  cursor: pointer;
  transition: background 0.2s;

  &:hover {
    background: ${({ theme }) => theme.bg2};
  }

  .icon {
    font-size: 28px;
    color: ${({ theme }) => theme.primary};
  }

  .info {
    flex: 1;
    display: flex;
    flex-direction: column;

    .name {
      font-weight: 600;
      font-size: 14px;
    }

    .ruc {
      font-size: 12px;
      color: ${({ theme }) => theme.textSecondary};
    }

    .badges {
      display: flex;
      flex-wrap: wrap;
      gap: 4px;
      margin-top: 4px;

      .badge {
        display: inline-flex;
        align-items: center;
        gap: 3px;
        padding: 2px 8px;
        border-radius: 10px;
        font-size: 10px;
        font-weight: 500;

        &.blue {
          background: #2196f315;
          color: #2196f3;
          border: 1px solid #2196f330;
        }
        &.orange {
          background: #ff980015;
          color: #ff9800;
          border: 1px solid #ff980030;
        }
        &.purple {
          background: #9c27b015;
          color: #9c27b0;
          border: 1px solid #9c27b030;
        }
        &.green {
          background: #4caf5015;
          color: #4caf50;
          border: 1px solid #4caf5030;
        }
        &.red {
          background: #f4433615;
          color: #f44336;
          border: 1px solid #f4433630;
        }
      }
    }
  }

  .arrow {
    font-size: 20px;
    color: ${({ theme }) => theme.textSecondary};
  }
`;

const CartItems = styled.div`
  flex: 1;
  overflow-y: auto;
  padding: 8px;
  -webkit-overflow-scrolling: touch;
`;

const EmptyCart = styled.div`
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  height: 100%;
  color: ${({ theme }) => theme.textSecondary};

  svg {
    font-size: 64px;
    margin-bottom: 16px;
  }

  span {
    font-size: 18px;
    font-weight: 500;
  }

  small {
    margin-top: 8px;
  }
`;

const CartItem = styled.div`
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  background: ${({ theme }) => theme.bg};
  border-radius: 8px;
  margin-bottom: 8px;

  .product-name {
    display: flex;
    align-items: center;
    flex-wrap: wrap;

    .name {
      font-weight: 600;
      font-size: 15px;
      flex: 1;
    }
  }

  .price-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 4px 0;

    .unit-price {
      font-size: 13px;
      color: ${({ theme }) => theme.textSecondary};
    }

    .total-price {
      font-weight: 700;
      font-size: 16px;
      color: ${({ theme }) => theme.primary};
    }
  }

  .controls-row {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding-top: 8px;
    border-top: 1px solid ${({ theme }) => theme.color2};

    .quantity-controls {
      display: flex;
      align-items: center;
      gap: 8px;
    }

    .action-buttons {
      display: flex;
      gap: 8px;
    }
  }
`;

const CartFooter = styled.div`
  /* Zona de cierre de venta: descuento, total, cobro rápido y COBRAR juntos en
     un solo bloque fijo, en vez de controles sueltos repartidos por el panel. */
  padding: 14px 16px 16px;
  border-top: 2px solid ${({ theme }) => theme.borderColor};
  background: ${({ theme }) => theme.bg3};
  flex-shrink: 0;

  @media (max-width: 768px) {
    padding: 10px 12px 12px;
  }
`;

const QuickPayRow = styled.div`
  display: flex;
  gap: 8px;
  margin-bottom: 10px;

  button {
    flex: 1;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 5px;
    padding: 12px 4px;
    border-radius: 10px;
    border: 1px solid ${({ theme }) => theme.borderColor};
    background: ${({ theme }) => theme.bg2};
    color: ${({ theme }) => theme.text};
    cursor: pointer;
    font-size: 13.5px;
    font-weight: 700;
    transition: all 0.15s;

    svg {
      font-size: 24px;
      color: ${({ theme }) => theme.primary};
    }

    &:hover {
      border-color: ${({ theme }) => theme.primary};
      background: ${({ theme }) => `color-mix(in srgb, ${theme.primary} 8%, ${theme.bg2})`};
      transform: translateY(-1px);
    }

    &:active {
      transform: translateY(0);
    }
  }

  @media (max-width: 768px) {
    button {
      padding: 10px 4px;
      font-size: 12px;
    }
  }
`;

const TotalsSection = styled.div`
  margin-bottom: 12px;

  .row {
    display: flex;
    justify-content: space-between;
    align-items: baseline;
    padding: 6px 0;
    font-size: 14px;
    color: ${({ theme }) => theme.textSecondary};

    /* El total tiene que leerse desde el otro lado del mostrador: es el dato
       que el cajero canta y el cliente verifica. Por eso rompe la escala del
       resto del panel en vez de ser "una fila más" del listado de importes. */
    &.total {
      margin-top: 4px;
      padding-top: 12px;
      font-size: 40px;
      line-height: 1.1;
      font-weight: 800;
      letter-spacing: -0.5px;
      color: ${({ theme }) => theme.text};

      span:first-child {
        font-size: 13px;
        font-weight: 700;
        letter-spacing: 1px;
        color: ${({ theme }) => theme.textSecondary};
        align-self: flex-end;
        padding-bottom: 6px;
      }

      span:last-child {
        color: ${({ theme }) => theme.primary};
      }
    }
  }

  @media (max-width: 768px) {
    margin-bottom: 8px;

    .row {
      padding: 4px 0;

      &.total {
        font-size: 30px;
      }
    }
  }
`;

const ActionButtonsSection = styled.div`
  display: flex;
  gap: 8px;

  @media (max-width: 768px) {
    gap: 6px;
  }
`;
