"use client";

import "./pdv.css";

import Image from "next/image";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { HelpCircle, MonitorSmartphone, ReceiptText, UserCheck, Wallet } from "lucide-react";
import QRCode from "qrcode";
import { CustomSelect } from "@/components/ui/custom-select";
import { CashIcon, CreditCardIcon, PixIcon } from "@/components/icons/payment-icons";
import { Vitrine } from "@/components/pdv/vitrine";
import { Cupom } from "@/components/pdv/cupom";
import { Receber, type CobrancaPix, type DisponibilidadeDoPagamento, type FaseDoPix, type NotaDaVenda } from "@/components/pdv/receber";
import { MenuDoCaixa, ModalDoCaixa, ModalDoHistorico, type AcaoDoCaixa } from "@/components/pdv/caixa";
import { ModalDoCupom, ModalDoVendedor } from "@/components/pdv/modais";
import { MenuDoCupom } from "@/components/pdv/menu-do-cupom";
import { useCupomNoBalcao } from "@/components/fiscal/nota-do-cupom";
import { PixPendente } from "@/components/pdv/pix-pendente";
import { getSupabaseClient } from "@/lib/supabase";
import { getCompanyContext } from "@/lib/company-context";
import { usePermissoes } from "@/lib/permissoes-do-usuario";
import { useConferenciaDoEstoque, useEsgotadosDaLista, useEstoqueV2Ligado } from "@/lib/use-estoque-da-venda";
import { quantidadeDoItem, resumoDaMedida } from "@/lib/orders/quantidade-do-item";
import { printPdvReceipt, type PdvReceiptPayload } from "@/lib/orders/pdv-receipt-print";
import { splitLiquidationAccounts } from "@/lib/liquidation-accounts";
import { formatMoney, formatMoneyInput, parseMoneyInput } from "@/utils/formatters";
import { abrirPonte, type EstadoDaTela, type FaseDaTela } from "@/lib/pdv/tela-do-cliente";
import {
  buildDefaultSelectedOptions,
  buildSelectedOptionsPayload,
  getItemVariationMap,
  getProductMinQuantity,
  getSelectedOptionsSummary,
  makeId,
  resolveItemUnitPrice,
  minimoDaLinhaDoPdv,
  minimoDoProdutoNoPdv,
  resolveNextQuantityForProduct,
  sanitizeQty,
  subtotalDoCarrinho,
  totalDaLinha,
  type BankAccountRow,
  type CartItem,
  type CashMovement,
  type CashSession,
  type CashSessionTotals,
  type CategoryRow,
  type ClientRow,
  type PaymentMethod,
  type PaymentSplit,
  type ProductAttributeValue,
  type ProductRow,
  type ProfileRow,
  type SellerAllocationDraft,
  type SellerOption,
} from "@/lib/pdv/carrinho";

/*
  O PDV.

  Dois passos, duas folhas. Na esquerda a vitrine; na direita o cupom — e,
  quando o caixa fecha a conta, o painel de receber no lugar do cupom. A barra
  de cima guarda o que muda pouco: cliente, vendedor, caixa físico e a tela do
  cliente.

  A conta (preço por faixa, variação, metro quadrado) mora em
  `lib/pdv/carrinho.ts`. Esta página é a orquestra: estado, atalhos e o que
  acontece quando a venda fecha.
*/

const GUARDA_DO_MERCADO_PAGO = "ktalogu:pdv:mercado-pago";
/* Imprimir sozinho vale por computador: a impressora é deste balcão. */
const GUARDA_DA_IMPRESSAO = "ktalogu:pdv:imprimir-sozinho";
const AVISO_DO_CLIENTE_DO_DIGITAL =
  "Produto digital: escolha o cliente da venda (no campo de cliente, na barra de cima). O acesso vai para ele, no e-mail e pelo QR code.";
const passthroughLoader = ({ src }: { src: string }) => src;
const esperar = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));

const PixSelectIcon = ({ size = 16, className }: { size?: number; className?: string }) => <PixIcon size={size} className={className} />;
const CashSelectIcon = ({ size = 16, className }: { size?: number; className?: string }) => <CashIcon size={size} className={className} />;
const CardSelectIcon = ({ size = 16, className }: { size?: number; className?: string }) => (
  <CreditCardIcon size={size} className={className} />
);

export default function PdvPage() {
  const { pode } = usePermissoes();
  /* Abrir o PDV e uma permissao; fechar a venda sao outras duas, porque a
     rota de checkout grava um pedido e um lancamento no financeiro. Sem elas
     a venda batia em 403 depois de o cliente ja estar no balcao. */
  const podeFecharVenda = pode("orders.write") && pode("financial.write");

  const [loading, setLoading] = useState(true);
  const [products, setProducts] = useState<ProductRow[]>([]);
  /* O dono da empresa: o estoque é conferido no banco pela empresa. */
  const [donoDaEmpresa, setDonoDaEmpresa] = useState<string | null>(null);
  const [categories, setCategories] = useState<CategoryRow[]>([]);
  const [clients, setClients] = useState<ClientRow[]>([]);
  const [bankAccounts, setBankAccounts] = useState<BankAccountRow[]>([]);
  const [sellers, setSellers] = useState<SellerOption[]>([]);
  const [storeName, setStoreName] = useState("Minha empresa");
  const [companyLogoUrl, setCompanyLogoUrl] = useState("");
  const [companyCnpj, setCompanyCnpj] = useState("");
  const [companyWhatsapp, setCompanyWhatsapp] = useState("");
  const [companyAddress, setCompanyAddress] = useState("");
  const [selectedCategory, setSelectedCategory] = useState("all");
  const [query, setQuery] = useState("");
  const [currentPage, setCurrentPage] = useState(1);
  const [cart, setCart] = useState<CartItem[]>([]);
  const [activeItemId, setActiveItemId] = useState<string | null>(null);
  const [discountInput, setDiscountInput] = useState("");
  const [selectedClientId, setSelectedClientId] = useState("");
  const [selectedBankAccountId, setSelectedBankAccountId] = useState("");
  const [selectedElectronicBankAccountId, setSelectedElectronicBankAccountId] = useState("");
  const [sellerMode, setSellerMode] = useState<"single" | "split">("single");
  const [primarySellerUserId, setPrimarySellerUserId] = useState("");
  const [sellerAllocations, setSellerAllocations] = useState<SellerAllocationDraft[]>([
    { id: makeId(), seller_user_id: "", allocation_percent: "100,00" },
  ]);
  const [showFreeItemForm, setShowFreeItemForm] = useState(false);
  const [freeItemName, setFreeItemName] = useState("");
  const [freeItemPriceInput, setFreeItemPriceInput] = useState("");
  const [payments, setPayments] = useState<PaymentSplit[]>([{ id: makeId(), method: "pix", amount_input: "" }]);
  const [quickMethod, setQuickMethod] = useState<PaymentMethod>("pix");
  const [cashReceivedInput, setCashReceivedInput] = useState("");
  const [saleMessage, setSaleMessage] = useState("");
  const [saleMessageTom, setSaleMessageTom] = useState<"ok" | "erro">("erro");
  const [isFinalizing, setIsFinalizing] = useState(false);
  const [justAddedCartItemId, setJustAddedCartItemId] = useState<string | null>(null);
  const [cashSession, setCashSession] = useState<CashSession | null>(null);
  const [cashTotals, setCashTotals] = useState<CashSessionTotals | null>(null);
  const [cashModalAction, setCashModalAction] = useState<AcaoDoCaixa | null>(null);
  const [cashAmountInput, setCashAmountInput] = useState("");
  const [cashCountedInput, setCashCountedInput] = useState("");
  const [cashNotesInput, setCashNotesInput] = useState("");
  const [cashActionLoading, setCashActionLoading] = useState(false);
  const [cashRecentSessions, setCashRecentSessions] = useState<CashSession[]>([]);
  const [cashHistoryOpen, setCashHistoryOpen] = useState(false);
  const [cashHistoryLoading, setCashHistoryLoading] = useState(false);
  const [cashHistorySession, setCashHistorySession] = useState<CashSession | null>(null);
  const [cashHistoryTotals, setCashHistoryTotals] = useState<CashSessionTotals | null>(null);
  const [cashHistoryMovements, setCashHistoryMovements] = useState<CashMovement[]>([]);
  const [lastReceipt, setLastReceipt] = useState<PdvReceiptPayload | null>(null);
  const [receiptPromptOpen, setReceiptPromptOpen] = useState(false);
  const [lastOrderId, setLastOrderId] = useState("");
  /* NFC-e: se está ligada, se sai em toda venda, e o CPF desta venda. */
  const [notaFiscal, setNotaFiscalEstado] = useState<NotaDaVenda>({ disponivel: false, teste: false, emitir: false, documento: "" });
  const [modoDaNota, setModoDaNota] = useState<"perguntar" | "sempre" | "nunca">("nunca");
  const [motivoDaNota, setMotivoDaNota] = useState<string | null>(null);
  const [podeConfigurarNota, setPodeConfigurarNota] = useState(false);
  const [salvandoModoDaNota, setSalvandoModoDaNota] = useState(false);
  const [erroDoModoDaNota, setErroDoModoDaNota] = useState<string | null>(null);
  const [imprimirSozinho, setImprimirSozinhoEstado] = useState(false);
  const setNotaFiscal = useCallback((atualizar: (antes: NotaDaVenda) => NotaDaVenda) => setNotaFiscalEstado(atualizar), []);

  type SituacaoDaNota = {
    disponivel?: boolean;
    modo?: "perguntar" | "sempre" | "nunca";
    teste?: boolean;
    motivo?: string | null;
    podeConfigurar?: boolean;
    podeEmitir?: boolean;
  };
  const aplicarSituacaoDaNota = useCallback((json: SituacaoDaNota) => {
    const modo = json.modo ?? "nunca";
    /* Marcar o cupom por escolha gasta nota do pacote: é de quem pode emitir.
       Com "emitir em toda venda" ligado, a escolha foi da empresa, e o cupom
       sai para qualquer caixa. */
    const disponivel = Boolean(json.disponivel) && (json.podeEmitir !== false || modo === "sempre");
    setModoDaNota(modo);
    setMotivoDaNota(json.motivo ?? null);
    setPodeConfigurarNota(Boolean(json.podeConfigurar));
    setNotaFiscalEstado((antes) => ({ ...antes, disponivel, teste: Boolean(json.teste), emitir: disponivel && modo === "sempre" }));
  }, []);

  useEffect(() => {
    let vivo = true;
    try {
      setImprimirSozinhoEstado(window.localStorage.getItem(GUARDA_DA_IMPRESSAO) === "1");
    } catch {
      /* sem memória local, começa desligado */
    }
    fetch("/api/fiscal/pdv", { cache: "no-store" })
      .then((resposta) => (resposta.ok ? resposta.json() : null))
      .then((json: SituacaoDaNota | null) => {
        if (vivo && json) aplicarSituacaoDaNota(json);
      })
      .catch(() => undefined);
    return () => {
      vivo = false;
    };
  }, [aplicarSituacaoDaNota]);
  const [sellerModalOpen, setSellerModalOpen] = useState(false);
  const [measureDrafts, setMeasureDrafts] = useState<Record<string, { width_m: string; height_m: string }>>({});

  /* O que é novo nesta cara do PDV. */
  const [passo, setPasso] = useState<"vender" | "receber">("vender");
  const [menuAberto, setMenuAberto] = useState<"caixa" | "cupom" | "atalhos" | null>(null);
  const [mp, setMp] = useState<DisponibilidadeDoPagamento | null>(null);
  const [mpLigado, setMpLigadoEstado] = useState(false);
  const [cobranca, setCobranca] = useState<CobrancaPix | null>(null);
  const [faseDoPix, setFaseDoPix] = useState<FaseDoPix>("parado");
  const [erroDoPix, setErroDoPix] = useState<string | null>(null);
  const [telaTenantId, setTelaTenantId] = useState<string | null>(null);
  /* A cor da loja segura a logo na barra: a logo do cabeçalho costuma ser
     branca, e branco em branco some. */
  const [corDaLoja, setCorDaLoja] = useState<string | null>(null);
  const [vendaNaTela, setVendaNaTela] = useState<{ numero: number | null; troco: number; pago: number; entrega?: { qr: string } | null } | null>(null);
  /* Produto digital vendido no balcão: o link da entrega e o QR code dele. */
  const [entregaDigital, setEntregaDigital] = useState<{ link: string; qr: string } | null>(null);
  /* Pix que caiu e venda que não gravou — o que a faixa amarela lembra. */
  const [pendentes, setPendentes] = useState<CobrancaPix[]>([]);
  const [pendentesOcultas, setPendentesOcultas] = useState(false);
  const [concluindoPendenteId, setConcluindoPendenteId] = useState<string | null>(null);

  const searchInputRef = useRef<HTMLInputElement | null>(null);
  const categoryButtonRefs = useRef<Record<string, HTMLButtonElement | null>>({});
  const ponteRef = useRef<ReturnType<typeof abrirPonte> | null>(null);
  const seqRef = useRef(0);
  /* Este balcão, entre os que podem estar falando com a tela; e a hora da
     última ação do operador aqui (0 até a primeira). */
  const origemRef = useRef(makeId());
  const ativoEmRef = useRef(0);
  const ultimoEstadoVistoRef = useRef<string | null>(null);
  const estadoDaTelaRef = useRef<EstadoDaTela | null>(null);
  const fechandoComPixRef = useRef(false);

  useEffect(() => {
    let cancelled = false;
    async function load() {
      try {
        const supabase = getSupabaseClient();
        const {
          data: { user },
        } = await supabase.auth.getUser();
        if (!user || cancelled) {
          if (!cancelled) setLoading(false);
          return;
        }

        const ctx = await getCompanyContext(supabase, user);
        const tenantIds = ctx.tenantIds?.length ? ctx.tenantIds : [user.id];
        if (!cancelled) setDonoDaEmpresa(ctx.ownerId || user.id);
        const profileLookup = Array.from(new Set([ctx.ownerId, user.id].filter(Boolean))) as string[];

        const sellerOptionsRequest = fetch("/api/company/users?mode=sales-options", {
          method: "GET",
          cache: "no-store",
        }).then(async (response) => {
          const json = (await response.json().catch(() => null)) as
            | { users?: SellerOption[]; currentUserId?: string; error?: string }
            | null;
          if (!response.ok) {
            throw new Error(json?.error || "Falha ao carregar vendedores.");
          }
          return json;
        });

        const [productsRes, categoriesRes, clientsRes, profilesRes, bankAccountsRes, sellersRes] = await Promise.all([
          supabase
            .from("products")
            .select("id,name,price,unit_type,use_variation_price_only,image_url,category_id,active,inventory_scope,inventory_attribute_name,product_attributes(name,allow_multiple,is_required,position,options),product_price_tiers(min_qty,max_qty,unit_price,sort_order,active),min_sale_qty_enabled,min_sale_qty,max_sale_qty_enabled,max_sale_qty,allowed_sale_qty_enabled,allowed_sale_quantities,sku,barcode,product_kind,price_on_request,valor_minimo_item,valor_minimo_pdv")
            .in("user_id", tenantIds)
            .eq("active", true)
            .order("name", { ascending: true }),
          supabase
            .from("categories")
            .select("id,name")
            .in("user_id", tenantIds)
            .order("name", { ascending: true }),
          supabase
            .from("clients")
            .select("id,name,company_name,is_deleted")
            .in("user_id", tenantIds)
            .or("active.is.null,active.eq.true")
            .not("is_deleted", "is", true)
            .order("company_name", { ascending: true })
            .order("name", { ascending: true }),
          supabase
            .from("profiles")
            .select("id,store_name,catalog_name,logo_url,catalog_logo_url,cnpj,address,company_phone,phone,company_whatsapp,whatsapp")
            .in("id", profileLookup.length > 0 ? profileLookup : [user.id]),
          supabase
            .from("bank_accounts")
            .select("id,name,bank_name,active,type")
            .in("company_id", tenantIds)
            .eq("active", true)
            .order("name", { ascending: true }),
          sellerOptionsRequest,
        ]);

        if (cancelled) return;

        const nextProducts = (productsRes.data as ProductRow[] | null) ?? [];
        setProducts(nextProducts);

        const dbCategories = (categoriesRes.data as CategoryRow[] | null) ?? [];
        if (dbCategories.length > 0) {
          setCategories(dbCategories);
        } else {
          const fallback = Array.from(
            new Set(nextProducts.map((product) => product.category_id).filter(Boolean))
          ).map((id) => ({ id: String(id), name: "Categoria" }));
          setCategories(fallback);
        }

        const nextClients = ((clientsRes.data as ClientRow[] | null) ?? []).filter((client) => !client.is_deleted);
        setClients(nextClients);
        const nextBankAccounts = ((bankAccountsRes.data as BankAccountRow[] | null) ?? []).filter((account) => account.id);
        setBankAccounts(nextBankAccounts);
        const { cashAccounts: nextCashAccounts, bankAccounts: nextElectronicAccounts } = splitLiquidationAccounts(nextBankAccounts);
        setSellers(sellersRes?.users || []);
        setPrimarySellerUserId((prev) => prev || sellersRes?.currentUserId || "");
        setSellerAllocations((prev) =>
          prev.map((allocation, index) =>
            index === 0 && !allocation.seller_user_id
              ? { ...allocation, seller_user_id: sellersRes?.currentUserId || "" }
              : allocation
          )
        );
        if (nextCashAccounts.length > 0) {
          setSelectedBankAccountId((prev) => prev || nextCashAccounts[0].id);
        }
        if (nextElectronicAccounts.length > 0) {
          setSelectedElectronicBankAccountId((prev) => prev || nextElectronicAccounts[0].id);
        }

        const profiles = (profilesRes.data as ProfileRow[] | null) ?? [];
        const profile =
          profiles.find((entry) => entry.logo_url || entry.catalog_logo_url) ||
          profiles.find((entry) => entry.catalog_name || entry.store_name) ||
          profiles[0] ||
          null;
        setCompanyLogoUrl(profile?.logo_url || profile?.catalog_logo_url || "");
        setStoreName(profile?.catalog_name || profile?.store_name || "Minha empresa");
        setCompanyCnpj(profile?.cnpj || "");
        setCompanyWhatsapp(profile?.company_whatsapp || profile?.whatsapp || profile?.company_phone || profile?.phone || "");
        setCompanyAddress(profile?.address || "");
      } finally {
        if (!cancelled) setLoading(false);
      }
    }
    load();
    return () => {
      cancelled = true;
    };
  }, []);

  /* O provedor de pagamento está pronto para o balcão? E o operador deixou ligado
     nesta máquina? */
  useEffect(() => {
    let cancelado = false;
    try {
      setMpLigadoEstado(window.localStorage.getItem(GUARDA_DO_MERCADO_PAGO) === "1");
    } catch {
      /* sem memória local, começa desligado */
    }
    fetch("/api/pdv/pagamento", { cache: "no-store" })
      .then(async (resposta) => {
        const dados = (await resposta.json().catch(() => null)) as DisponibilidadeDoPagamento | null;
        if (!cancelado) setMp(dados && resposta.ok ? dados : { disponivel: false, motivo: "Não foi possível consultar o provedor de pagamento." });
      })
      .catch(() => {
        if (!cancelado) setMp({ disponivel: false, motivo: "Não foi possível consultar o provedor de pagamento." });
      });
    void carregarPendentesRef.current();
    fetch("/api/pdv/tela", { cache: "no-store" })
      .then(async (resposta) => {
        const dados = (await resposta.json().catch(() => null)) as { tenantId?: string; loja?: { cor?: string } } | null;
        if (!cancelado && resposta.ok && dados?.tenantId) {
          setTelaTenantId(dados.tenantId);
          if (dados.loja?.cor) setCorDaLoja(dados.loja.cor);
        }
      })
      .catch(() => undefined);
    return () => {
      cancelado = true;
    };
  }, []);

  const carregarPendentes = useCallback(async () => {
    /* A lista de Pix pendentes so serve para quem fecha venda, e a rota dela
       exige as mesmas duas chaves. Sem esta linha, quem tem o PDV so para
       consultar levava um 403 no carregamento da tela. */
    if (!podeFecharVenda) return;
    try {
      const resposta = await fetch("/api/pdv/pix?pendentes=1", { cache: "no-store" });
      const dados = (await resposta.json().catch(() => null)) as { cobrancas?: CobrancaPix[] } | null;
      if (resposta.ok && Array.isArray(dados?.cobrancas)) setPendentes(dados.cobrancas);
    } catch {
      /* sem lista agora; tenta de novo na próxima venda */
    }
  }, [podeFecharVenda]);
  const carregarPendentesRef = useRef(carregarPendentes);
  carregarPendentesRef.current = carregarPendentes;

  const setMpLigado = useCallback((valor: boolean) => {
    setMpLigadoEstado(valor);
    try {
      window.localStorage.setItem(GUARDA_DO_MERCADO_PAGO, valor ? "1" : "0");
    } catch {
      /* fica só nesta sessão */
    }
  }, []);

  /**
   * Relê a sessão do caixa e DEVOLVE o que leu.
   *
   * Devolver importa: quem precisa do saldo esperado agora (o fechamento) não
   * pode esperar o estado do React chegar. Ler `cashTotals` logo depois de
   * chamar aqui devolveria o valor velho, que foi exatamente o defeito que
   * gravou diferença de caixa que não existiu.
   */
  const fetchCashSession = useCallback(async (bankAccountId: string) => {
    if (!bankAccountId) {
      setCashSession(null);
      setCashTotals(null);
      setCashRecentSessions([]);
      return null;
    }
    try {
      const response = await fetch(`/api/pdv/cash-session?bank_account_id=${encodeURIComponent(bankAccountId)}`);
      const result = (await response.json().catch(() => null)) as
        | {
            session?: CashSession | null;
            totals?: CashSessionTotals | null;
            movements?: CashMovement[] | null;
            recent_sessions?: CashSession[] | null;
          }
        | null;
      if (!response.ok) {
        setCashSession(null);
        setCashTotals(null);
        setCashRecentSessions([]);
        return null;
      }
      const sessao = result?.session || null;
      const totais = result?.totals || null;
      setCashSession(sessao);
      setCashTotals(totais);
      setCashRecentSessions(result?.recent_sessions || []);
      return { sessao, totais };
    } catch {
      setCashSession(null);
      setCashTotals(null);
      setCashRecentSessions([]);
      return null;
    }
  }, []);

  useEffect(() => {
    fetchCashSession(selectedBankAccountId);
  }, [selectedBankAccountId, fetchCashSession]);

  function avisar(texto: string, tom: "ok" | "erro" = "erro") {
    setSaleMessage(texto);
    setSaleMessageTom(tom);
  }

  /* O cupom fiscal de cada venda: emitido na hora e acompanhado aqui, mesmo com a janela da venda fechada. */
  const cupomNoBalcao = useCupomNoBalcao({ imprimirSozinho, pedidoNaTela: receiptPromptOpen ? lastOrderId : null, avisar });
  const iniciarCupomFiscal = cupomNoBalcao.iniciar;

  const setImprimirSozinho = useCallback((valor: boolean) => {
    setImprimirSozinhoEstado(valor);
    try {
      window.localStorage.setItem(GUARDA_DA_IMPRESSAO, valor ? "1" : "0");
    } catch {
      /* fica só nesta sessão */
    }
  }, []);

  const mudarCupomEmTodaVenda = useCallback(
    async (emTodaVenda: boolean) => {
      setSalvandoModoDaNota(true);
      setErroDoModoDaNota(null);
      try {
        const resposta = await fetch("/api/fiscal/pdv", {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify({ emTodaVenda }),
          cache: "no-store",
        });
        const json = (await resposta.json().catch(() => null)) as (SituacaoDaNota & { error?: string }) | null;
        if (!resposta.ok || !json) throw new Error(json?.error || "Não foi possível salvar agora.");
        aplicarSituacaoDaNota(json);
      } catch (erro) {
        setErroDoModoDaNota(erro instanceof Error ? erro.message : "Não foi possível salvar agora.");
      } finally {
        setSalvandoModoDaNota(false);
      }
    },
    [aplicarSituacaoDaNota]
  );

  async function submitCashAction() {
    if (!selectedBankAccountId) {
      avisar("Escolha o caixa físico para operar o caixa.");
      return;
    }
    if (!cashModalAction) return;

    const amount = parseMoneyInput(cashAmountInput);
    const countedAmount = parseMoneyInput(cashCountedInput);
    const body =
      cashModalAction === "open"
        ? {
            action: "open",
            bank_account_id: selectedBankAccountId,
            amount,
            notes: cashNotesInput || null,
          }
        : cashModalAction === "close"
          ? {
              action: "close",
              session_id: cashSession?.id,
              counted_amount: countedAmount,
              notes: cashNotesInput || null,
            }
          : {
              action: cashModalAction,
              session_id: cashSession?.id,
              amount,
              notes: cashNotesInput || null,
            };

    setCashActionLoading(true);
    setSaleMessage("");
    try {
      const response = await fetch("/api/pdv/cash-session", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(body),
      });
      const result = (await response.json().catch(() => null)) as
        | {
            ok?: boolean;
            error?: string;
            session?: CashSession | null;
            totals?: CashSessionTotals | null;
          }
        | null;
      if (!response.ok) {
        avisar(result?.error || "Falha ao processar operação de caixa.");
        return;
      }
      setCashSession(result?.session || null);
      setCashTotals(result?.totals || null);
      setCashModalAction(null);
      setCashAmountInput("");
      setCashCountedInput("");
      setCashNotesInput("");
      await fetchCashSession(selectedBankAccountId);
    } catch {
      avisar("Falha ao processar operação de caixa.");
    } finally {
      setCashActionLoading(false);
    }
  }

  async function openCashHistory(targetSessionId?: string) {
    const sessionId = targetSessionId || cashSession?.id || cashRecentSessions[0]?.id || "";
    if (!sessionId) {
      avisar("Nenhuma sessão de caixa encontrada para exibir histórico.");
      return;
    }

    setCashHistoryLoading(true);
    setCashHistoryOpen(true);
    try {
      const response = await fetch(`/api/pdv/cash-session?session_id=${encodeURIComponent(sessionId)}&movements_limit=200`);
      const result = (await response.json().catch(() => null)) as
        | {
            error?: string;
            session?: CashSession | null;
            totals?: CashSessionTotals | null;
            movements?: CashMovement[] | null;
            recent_sessions?: CashSession[] | null;
          }
        | null;
      if (!response.ok) {
        avisar(result?.error || "Falha ao carregar histórico da sessão.");
        setCashHistoryOpen(false);
        return;
      }
      setCashHistorySession(result?.session || null);
      setCashHistoryTotals(result?.totals || null);
      setCashHistoryMovements(result?.movements || []);
      /* A rota já devolve as sessões do mesmo caixa desta sessão, e a lista da
         esquerda vinha de outra chamada. Com o caixa físico não escolhido, o
         histórico abria mostrando os movimentos de uma sessão e "Nenhuma sessão
         ainda" ao lado. */
      if (Array.isArray(result?.recent_sessions) && result.recent_sessions.length > 0) {
        setCashRecentSessions(result.recent_sessions);
      }
    } catch {
      avisar("Falha ao carregar histórico da sessão.");
      setCashHistoryOpen(false);
    } finally {
      setCashHistoryLoading(false);
    }
  }

  const categoryItems = useMemo(
    () => [{ id: "all", name: "Todos" } as CategoryRow, ...categories.filter((category) => category.id !== "all")],
    [categories]
  );
  const selectedCategoryIndex = useMemo(
    () => categoryItems.findIndex((category) => category.id === selectedCategory),
    [categoryItems, selectedCategory]
  );
  const paymentMethodOptions = useMemo(
    () => [
      { value: "pix", label: "PIX", icon: PixSelectIcon },
      { value: "card", label: "Cartão", icon: CardSelectIcon },
      { value: "cash", label: "Dinheiro", icon: CashSelectIcon },
    ],
    []
  );
  const clientOptions = useMemo(
    () => [
      { value: "", label: "Venda sem cliente" },
      ...clients.map((client) => ({
        value: client.id,
        label: client.company_name || client.name,
      })),
    ],
    [clients]
  );
  const bankAccountOptions = useMemo(
    () => [
      { value: "", label: "Selecione caixa físico" },
      ...splitLiquidationAccounts(bankAccounts).cashAccounts.map((account) => ({
        value: account.id,
        label: account.name || account.bank_name || "Caixa físico",
      })),
    ],
    [bankAccounts]
  );
  const electronicBankAccountOptions = useMemo(
    () => [
      { value: "", label: "Selecione conta bancária" },
      ...splitLiquidationAccounts(bankAccounts).bankAccounts.map((account) => ({
        value: account.id,
        label: account.name || account.bank_name || "Conta bancária",
      })),
    ],
    [bankAccounts]
  );
  const sellerOptions = useMemo(
    () =>
      sellers.map((seller) => ({
        value: seller.user_id,
        label: `${seller.email} • ${seller.role}`,
      })),
    [sellers]
  );
  const normalizedSellerAllocations = useMemo(
    () =>
      sellerAllocations
        .map((allocation, index) => ({
          ...allocation,
          allocationPercent: parseMoneyInput(allocation.allocation_percent),
          is_primary: index === 0,
        }))
        .filter((allocation) => allocation.seller_user_id && allocation.allocationPercent > 0),
    [sellerAllocations]
  );

  const filteredProducts = useMemo(() => {
    const normalized = query.trim().toLowerCase();
    return products.filter((product) => {
      if (selectedCategory !== "all" && product.category_id !== selectedCategory) return false;
      if (!normalized) return true;
      const haystack = `${product.name} ${product.id} ${product.sku || ""} ${product.barcode || ""}`.toLowerCase();
      return haystack.includes(normalized);
    });
  }, [products, query, selectedCategory]);

  const pageSize = 20;
  const totalPages = Math.max(1, Math.ceil(filteredProducts.length / pageSize));
  const safePage = Math.min(currentPage, totalPages);
  const visibleProducts = useMemo(() => {
    const start = (safePage - 1) * pageSize;
    return filteredProducts.slice(start, start + pageSize);
  }, [filteredProducts, safePage]);
  const productImageById = useMemo(
    () => new Map(products.map((product) => [product.id, product.image_url ?? ""])),
    [products]
  );

  /*
    O estoque do cupom, conferido no banco a cada mudança (estoque_conferir_itens,
    a MESMA conta da loja e do orçamento: soma por célula no estoque v2, regra
    de sempre fora dele). Produto que não controla ou que permite negativo
    nunca aparece. A frase vai na linha ("Acabou o estoque de "Camiseta lisa
    (G / Preta)" para "Camisetas": tem 0, o pedido precisa de 3.") e a venda
    não segue para receber — antes, o Pix podia ser pago e a venda recusada
    pelo banco depois.
  */
  const supabaseDoPdv = useMemo(() => getSupabaseClient(), []);
  const estoqueV2 = useEstoqueV2Ligado(supabaseDoPdv, donoDaEmpresa);
  const linhasParaOEstoque = useMemo(
    () =>
      cart.map((item) => ({
        product_id: item.is_free_item ? null : item.product_id,
        quantity: item.quantity,
        selected_options: item.selected_options,
        width_m: item.width_m ?? null,
        height_m: item.height_m ?? null,
        title: item.title,
      })),
    [cart]
  );
  const conferenciaDoEstoque = useConferenciaDoEstoque({
    supabase: supabaseDoPdv,
    dono: donoDaEmpresa,
    linhas: linhasParaOEstoque,
    ativo: Boolean(donoDaEmpresa),
  });
  const faltasDeEstoque = useMemo(() => {
    const porItem = new Map<string, string[]>();
    conferenciaDoEstoque.faltas.forEach((frases, indice) => {
      const item = cart[indice];
      if (item) porItem.set(item.id, frases);
    });
    return porItem;
  }, [conferenciaDoEstoque.faltas, cart]);
  /* O aviso "Acabou o estoque…" do Receber some quando o cupom muda (ex.:
     tirou o item que faltava); se ainda faltar, a linha do item continua
     dizendo. */
  const avisoDeEstoqueMostrado = useRef<string | null>(null);
  useEffect(() => {
    const aviso = avisoDeEstoqueMostrado.current;
    if (!aviso || conferenciaDoEstoque.frase === aviso) return;
    avisoDeEstoqueMostrado.current = null;
    setSaleMessage((atual) => (atual === aviso ? "" : atual));
  }, [conferenciaDoEstoque.frase]);
  /* Estoque v2: o selo "Esgotado" nos produtos da página da vitrine. */
  const idsVisiveis = useMemo(() => visibleProducts.map((produto) => produto.id), [visibleProducts]);
  const esgotadosDaLista = useEsgotadosDaLista({
    supabase: supabaseDoPdv,
    dono: donoDaEmpresa,
    ids: idsVisiveis,
    ativo: estoqueV2 === true,
  });
  const esquecerEsgotados = esgotadosDaLista.esquecer;

  useEffect(() => {
    setCurrentPage(1);
  }, [query, selectedCategory]);

  const subtotal = useMemo(() => subtotalDoCarrinho(cart), [cart]);
  const discountValue = parseMoneyInput(discountInput);
  const total = Math.max(0, subtotal - discountValue);
  const paidTotal = useMemo(
    () => payments.reduce((sum, payment) => sum + parseMoneyInput(payment.amount_input), 0),
    [payments]
  );
  const missingPayment = Math.max(0, total - paidTotal);
  const overPayment = Math.max(0, paidTotal - total);
  const hasCashPayment = payments.some((payment) => payment.method === "cash");
  const hasPixPayment = payments.some((payment) => payment.method === "pix");
  const hasCardPayment = payments.some((payment) => payment.method === "card");
  const hasElectronicPayment = hasPixPayment || hasCardPayment;
  const cashReceived = parseMoneyInput(cashReceivedInput);
  const cashUsed = payments
    .filter((payment) => payment.method === "cash")
    .reduce((sum, payment) => sum + parseMoneyInput(payment.amount_input), 0);
  const change = Math.max(0, cashReceived - cashUsed);
  const pixTotal = payments
    .filter((payment) => payment.method === "pix")
    .reduce((sum, payment) => sum + parseMoneyInput(payment.amount_input), 0);
  const requiresOpenCashSession = hasCashPayment;
  const hasOpenCashSession = Boolean(cashSession?.id && cashSession.status === "open");

  /* Com o provedor ligado, o Pix cai na conta que a integração aponta;
     sem ela, na conta bancária escolhida aqui — como sempre foi. */
  const usaMercadoPago = mpLigado && Boolean(mp?.disponivel) && pixTotal > 0.0001;
  const contaDoPix = usaMercadoPago && mp?.conta?.id ? mp.conta.id : selectedElectronicBankAccountId;
  const contaDoCartao = selectedElectronicBankAccountId;

  const canFinalize =
    podeFecharVenda &&
    cart.length > 0 &&
    missingPayment <= 0.0001 &&
    overPayment <= 0.0001 &&
    (!hasCashPayment || Boolean(selectedBankAccountId)) &&
    (!hasCardPayment || Boolean(contaDoCartao)) &&
    (!hasPixPayment || Boolean(contaDoPix)) &&
    (!requiresOpenCashSession || hasOpenCashSession);

  function addSellerAllocation() {
    setSellerAllocations((prev) => [...prev, { id: makeId(), seller_user_id: "", allocation_percent: "0,00" }]);
  }

  function updateSellerAllocation(id: string, patch: Partial<SellerAllocationDraft>) {
    setSellerAllocations((prev) => prev.map((allocation) => (allocation.id === id ? { ...allocation, ...patch } : allocation)));
  }

  function removeSellerAllocation(id: string) {
    setSellerAllocations((prev) => (prev.length > 2 ? prev.filter((allocation) => allocation.id !== id) : prev));
  }

  function addProduct(product: ProductRow) {
    setSaleMessage("");
    const minimumQty = getProductMinQuantity(product);
    setCart((prev) => {
      const selectedOptions = buildDefaultSelectedOptions(product);
      const serializedSelection = JSON.stringify(selectedOptions || null);
      const existing = prev.find(
        (item) =>
          item.product_id === product.id &&
          !item.is_free_item &&
          JSON.stringify(item.selected_options || null) === serializedSelection
      );
      if (existing) {
        setJustAddedCartItemId(existing.id);
        return prev.map((item) => {
          if (item.id !== existing.id) return item;
          const nextQty = resolveNextQuantityForProduct(product, item.quantity, 1);
          return {
            ...item,
            quantity: nextQty,
            unit_price: resolveItemUnitPrice(product, item.selected_options, nextQty),
            valor_minimo_item: minimoDoProdutoNoPdv(product),
          };
        });
      }
      const isSquareMeter = product.unit_type === "square_meter";
      const itemId = makeId();
      setJustAddedCartItemId(itemId);
      return [
        ...prev,
        {
          id: itemId,
          product_id: product.id,
          title: product.name,
          quantity: isSquareMeter ? 1 : minimumQty,
          unit_price: resolveItemUnitPrice(product, selectedOptions, isSquareMeter ? 1 : minimumQty),
          unit_type: product.unit_type || null,
          width_m: isSquareMeter ? 1 : null,
          height_m: isSquareMeter ? 1 : null,
          sku: product.sku || null,
          barcode: product.barcode || null,
          note: "",
          is_free_item: false,
          selected_options: selectedOptions,
          valor_minimo_item: minimoDoProdutoNoPdv(product),
        },
      ];
    });
  }

  function addFreeItem() {
    const name = freeItemName.trim();
    const price = parseMoneyInput(freeItemPriceInput);
    if (!name || price <= 0) return;
    const itemId = makeId();
    setSaleMessage("");
    setJustAddedCartItemId(itemId);
    setCart((prev) => [
      ...prev,
      { id: itemId, product_id: null, title: name, quantity: 1, unit_price: price, note: "", is_free_item: true, selected_options: null },
    ]);
    setFreeItemName("");
    setFreeItemPriceInput("");
    setShowFreeItemForm(false);
  }

  function updateItem(itemId: string, patch: Partial<CartItem>) {
    setCart((prev) => prev.map((item) => (item.id === itemId ? { ...item, ...patch } : item)));
  }

  function removeItem(itemId: string) {
    setCart((prev) => prev.filter((item) => item.id !== itemId));
    if (activeItemId === itemId) setActiveItemId(null);
  }

  function changeQuantity(itemId: string, delta: number) {
    setCart((prev) =>
      prev.map((item) => {
        if (item.id !== itemId) return item;
        const product = item.product_id ? products.find((entry) => entry.id === item.product_id) || null : null;
        const minQty = getProductMinQuantity(product);
        const nextQty = product
          ? resolveNextQuantityForProduct(product, item.quantity, delta)
          : sanitizeQty(item.quantity + delta);
        const resolvedQty = Math.max(minQty, nextQty);
        const nextPrice = product ? resolveItemUnitPrice(product, item.selected_options, resolvedQty) : item.unit_price;
        /* O preço volta a ser o do cadastro, e com ele o valor mínimo do item. */
        return {
          ...item,
          quantity: resolvedQty,
          unit_price: nextPrice,
          valor_minimo_item: product ? minimoDoProdutoNoPdv(product) : item.valor_minimo_item ?? null,
        };
      })
    );
  }

  function updateItemVariationSelection(itemId: string, product: ProductRow, attribute: ProductAttributeValue, optionLabel: string) {
    setCart((prev) =>
      prev.map((item) => {
        if (item.id !== itemId) return item;
        const currentMap = getItemVariationMap(item.selected_options);
        const currentValues = currentMap[attribute.name] || [];
        const allowMultiple = Boolean(attribute.allow_multiple);
        const nextValues = allowMultiple
          ? currentValues.includes(optionLabel)
            ? currentValues.filter((value) => value !== optionLabel)
            : [...currentValues, optionLabel]
          : currentValues.includes(optionLabel)
            ? []
            : [optionLabel];
        const nextMap = { ...currentMap };
        if (nextValues.length > 0) {
          nextMap[attribute.name] = nextValues;
        } else {
          delete nextMap[attribute.name];
        }
        const payload = buildSelectedOptionsPayload(product, nextMap);
        return {
          ...item,
          selected_options: payload,
          unit_price: resolveItemUnitPrice(product, payload, item.quantity),
          valor_minimo_item: minimoDoProdutoNoPdv(product),
        };
      })
    );
  }

  const applyQuickPayment = useCallback((method: PaymentMethod) => {
    setQuickMethod(method);
    setPayments((prev) => {
      const remaining = Math.max(0, total - prev.reduce((sum, payment) => sum + parseMoneyInput(payment.amount_input), 0));
      const nextAmount = remaining > 0 ? formatMoneyInput(String(Math.round(remaining * 100))) : "";
      const firstEmpty = prev.find((payment) => parseMoneyInput(payment.amount_input) <= 0.0001);
      if (firstEmpty) {
        return prev.map((payment) =>
          payment.id === firstEmpty.id ? { ...payment, method, amount_input: nextAmount || payment.amount_input } : payment
        );
      }
      if (remaining <= 0.0001) {
        /* Nada falta. Com uma parte só, o clique troca o meio dela ("é cartão,
           não Pix"); com várias, não inventa linha vazia. */
        const comValor = prev.filter((payment) => parseMoneyInput(payment.amount_input) > 0.0001);
        if (comValor.length === 1) {
          return prev.map((payment) => (payment.id === comValor[0].id ? { ...payment, method } : payment));
        }
        return prev;
      }
      return [...prev, { id: makeId(), method, amount_input: nextAmount }];
    });
  }, [total]);

  const rotateQuickMethod = useCallback(() => {
    const order: PaymentMethod[] = ["pix", "card", "cash"];
    const index = order.indexOf(quickMethod);
    const next = order[(index + 1) % order.length];
    applyQuickPayment(next);
  }, [quickMethod, applyQuickPayment]);

  const navigateCategory = useCallback((delta: number) => {
    if (!categoryItems.length) return;
    const baseIndex = selectedCategoryIndex >= 0 ? selectedCategoryIndex : 0;
    const nextIndex = (baseIndex + delta + categoryItems.length) % categoryItems.length;
    const nextCategoryId = categoryItems[nextIndex]?.id;
    if (!nextCategoryId) return;
    setSelectedCategory(nextCategoryId);
    const nextButton = categoryButtonRefs.current[nextCategoryId];
    nextButton?.focus();
    nextButton?.scrollIntoView({ block: "nearest", inline: "nearest" });
  }, [categoryItems, selectedCategoryIndex]);

  /* A busca com Enter: código de barras ou SKU exato entra na hora; senão,
     o primeiro da lista. */
  const confirmarBusca = useCallback(
    (bruto: string) => {
      const val = bruto.trim().toLowerCase();
      if (!val) return;
      const exactMatch = products.find(
        (p) => (p.barcode && p.barcode.toLowerCase() === val) || (p.sku && p.sku.toLowerCase() === val)
      );
      if (exactMatch) {
        addProduct(exactMatch);
        setQuery("");
        return;
      }
      if (visibleProducts[0]) {
        addProduct(visibleProducts[0]);
        setQuery("");
      }
    },
    [products, visibleProducts]
  );

  /* Produto digital no carrinho: a venda precisa de um cliente (o acesso é dele). */
  const temDigitalNoCarrinho = useMemo(
    () => cart.some((item) => item.product_id && products.find((p) => p.id === item.product_id)?.product_kind === "digital"),
    [cart, products]
  );
  const validarClienteDoDigital = useCallback(() => {
    if (!temDigitalNoCarrinho || selectedClientId) return true;
    avisar(AVISO_DO_CLIENTE_DO_DIGITAL);
    return false;
  }, [temDigitalNoCarrinho, selectedClientId]);

  /* A venda com produto digital mostra o QR code do link da entrega (aqui e na tela do cliente). */
  const mostrarEntregaDigital = useCallback((link: string | null | undefined) => {
    if (!link) {
      setEntregaDigital(null);
      return;
    }
    void QRCode.toDataURL(link, { margin: 1, width: 360 })
      .then((qr) => {
        setEntregaDigital({ link, qr });
        setVendaNaTela((atual) => (atual ? { ...atual, entrega: { qr } } : atual));
      })
      .catch(() => setEntregaDigital({ link, qr: "" }));
  }, []);

  /* Quem vendeu precisa estar certo antes de qualquer cobrança. */
  const validarVendedor = useCallback(() => {
    if (sellerMode === "single" && !primarySellerUserId) {
      avisar("Escolha o vendedor da venda (botão Vendedor, na barra de cima).");
      return false;
    }
    if (sellerMode === "split") {
      if (normalizedSellerAllocations.length < 2) {
        avisar("Informe ao menos dois vendedores para o rateio.");
        return false;
      }
      const allocationTotal = normalizedSellerAllocations.reduce((sum, allocation) => sum + allocation.allocationPercent, 0);
      if (Math.abs(allocationTotal - 100) > 0.01) {
        avisar("O rateio da venda deve totalizar exatamente 100%.");
        return false;
      }
      const uniqueSellerCount = new Set(normalizedSellerAllocations.map((allocation) => allocation.seller_user_id)).size;
      if (uniqueSellerCount !== normalizedSellerAllocations.length) {
        avisar("O mesmo vendedor não pode aparecer mais de uma vez no rateio.");
        return false;
      }
    }
    return true;
  }, [sellerMode, primarySellerUserId, normalizedSellerAllocations]);

  /*
    A venda como o servidor a recebe — usada pelo ensaio (antes de cobrar o
    Pix), pela foto guardada na cobrança e pelo fechamento de verdade. Com uma
    cobrança Pix aprovada, o pagamento em Pix nasce **dela**: vale o que foi
    pago, não o que está digitado.
  */
  const montarVenda = useCallback(
    (pixChargeId: string | null) => {
      const selectedClient = selectedClientId ? clients.find((client) => client.id === selectedClientId) : null;
      const fallbackClientName = selectedClient?.company_name || selectedClient?.name || "Cliente balcão";
      const itemCount = cart.reduce((sum, item) => sum + item.quantity, 0);
      const soldItemsSnapshot = cart.map((item) => {
        /* No cupom, item por m² sai como peças × preço da peça ("2 x R$ 62,93"),
           com a medida no nome. Antes saía "0,7 x R$ 89,90". */
        const isSqM = item.unit_type === "square_meter";
        const area = isSqM ? (item.width_m || 1) * (item.height_m || 1) : 1;
        const pecas = Math.max(1, item.quantity || 1);
        const medida = quantidadeDoItem(item);
        /* Com o valor mínimo do item, o cupom mostra a linha no mínimo. */
        const minimo = minimoDaLinhaDoPdv(item);
        return {
          title: isSqM && medida.medida ? `${item.title} (${medida.medida})` : item.title,
          quantity: pecas,
          unit_price: minimo ? minimo.minimo / pecas : item.unit_price * area,
          line_total: minimo ? minimo.minimo : pecas * area * item.unit_price,
          sku: item.sku || null,
          barcode: item.barcode || null,
          note: item.note || null,
        };
      });
      const contaDe = (method: PaymentMethod) =>
        method === "cash" ? selectedBankAccountId || null : method === "pix" ? contaDoPix || null : contaDoCartao || null;
      const partesSemPix = payments
        .filter((payment) => payment.method !== "pix")
        .map((payment) => ({ method: payment.method, amount: parseMoneyInput(payment.amount_input), bank_account_id: contaDe(payment.method) }));
      const partesPix =
        pixChargeId && cobranca
          ? [{ method: "pix" as PaymentMethod, amount: cobranca.valor, bank_account_id: contaDoPix || null, pix_charge_id: pixChargeId }]
          : payments
              .filter((payment) => payment.method === "pix")
              .map((payment) => ({ method: payment.method, amount: parseMoneyInput(payment.amount_input), bank_account_id: contaDe("pix") }));
      const soldPaymentsSnapshot = [...partesSemPix, ...partesPix].filter((payment) => payment.amount > 0 && payment.bank_account_id);
      const subtotalSnapshot = soldItemsSnapshot.reduce((sum, item) => sum + item.line_total, 0);
      const discountSnapshot = parseMoneyInput(discountInput);
      const totalSnapshot = Math.max(0, subtotalSnapshot - discountSnapshot);
      const paidTotalSnapshot = soldPaymentsSnapshot.reduce((sum, payment) => sum + payment.amount, 0);

      const payload = {
        client_id: selectedClientId || null,
        bank_account_id:
          hasCashPayment && !hasElectronicPayment
            ? selectedBankAccountId || null
            : !hasCashPayment && hasElectronicPayment
              ? (hasCardPayment ? contaDoCartao : contaDoPix) || null
              : null,
        seller_mode: sellerMode,
        primary_seller_user_id:
          sellerMode === "single" ? primarySellerUserId : normalizedSellerAllocations[0]?.seller_user_id || null,
        allocations:
          sellerMode === "split"
            ? normalizedSellerAllocations.map((allocation) => ({
                seller_user_id: allocation.seller_user_id,
                allocation_percent: allocation.allocationPercent,
                is_primary: allocation.is_primary,
              }))
            : [
                {
                  seller_user_id: primarySellerUserId,
                  allocation_percent: 100,
                  is_primary: true,
                },
              ],
        discount: discountSnapshot,
        cash_received: parseMoneyInput(cashReceivedInput),
        items: cart.map((item) => {
          /* Peças e medida vão separados: o servidor calcula a área. */
          const isSqM = item.unit_type === "square_meter";
          return {
            product_id: item.product_id,
            title: item.title,
            quantity: Math.max(1, item.quantity || 1),
            unit_type: isSqM ? "square_meter" : item.unit_type || "unit",
            width_m: isSqM ? item.width_m || 1 : null,
            height_m: isSqM ? item.height_m || 1 : null,
            unit_price: item.unit_price,
            note: item.note || null,
            is_free_item: item.is_free_item,
            selected_options: item.selected_options,
            /* O servidor confere o mínimo com o cadastro antes de aceitar. */
            valor_minimo_aplicado: minimoDaLinhaDoPdv(item)?.minimo ?? null,
            valor_sem_minimo: minimoDaLinhaDoPdv(item)?.semMinimo ?? null,
          };
        }),
        payments: soldPaymentsSnapshot,
        nota_fiscal: notaFiscal.disponivel && notaFiscal.emitir ? { emitir: true, documento: notaFiscal.documento.replace(/\D/g, "") || null } : null,
      };

      return {
        payload,
        soldItemsSnapshot,
        soldPaymentsSnapshot,
        subtotalSnapshot,
        discountSnapshot,
        totalSnapshot,
        paidTotalSnapshot,
        fallbackClientName,
        itemCount,
      };
    },
    [
      selectedClientId,
      clients,
      cart,
      payments,
      cobranca,
      selectedBankAccountId,
      contaDoPix,
      contaDoCartao,
      discountInput,
      cashReceivedInput,
      hasCashPayment,
      hasElectronicPayment,
      hasCardPayment,
      sellerMode,
      primarySellerUserId,
      normalizedSellerAllocations,
      notaFiscal,
    ]
  );

  /* Fecha a venda: depois de uma venda concluída, o balcão volta ao zero e a
     tela do cliente agradece. */
  const encerrarVenda = useCallback(
    (
      result: { orderId?: string; orderNumber?: number | null; clientName?: string; change?: number; entregaDigital?: { link?: string } | null } | null,
      venda: ReturnType<typeof montarVenda>,
      totalDaVenda: number
    ) => {
      const orderLabel = result?.orderNumber ? `Pedido #${result.orderNumber}` : "Pedido";
      const clientName = result?.clientName || venda.fallbackClientName;
      const changeText =
        typeof result?.change === "number" && result.change > 0.009 ? ` • Troco ${formatMoney(result.change)}` : "";

      avisar(`${orderLabel} finalizado: ${venda.itemCount} itens • ${formatMoney(totalDaVenda)} • ${clientName}${changeText}`, "ok");
      const receiptPayload: PdvReceiptPayload = {
        company: {
          name: storeName || "Minha empresa",
          logo_url: companyLogoUrl || null,
          cnpj: companyCnpj || null,
          whatsapp: companyWhatsapp || null,
          address: companyAddress || null,
        },
        order: {
          id: result?.orderId || "",
          number: result?.orderNumber ?? null,
          created_at: new Date().toISOString(),
        },
        client: {
          name: clientName,
        },
        items: venda.soldItemsSnapshot,
        payments: venda.soldPaymentsSnapshot.map(({ method, amount }) => ({ method, amount })),
        totals: {
          subtotal: venda.subtotalSnapshot,
          discount: venda.discountSnapshot,
          total: venda.totalSnapshot,
          paid_total: venda.paidTotalSnapshot,
          change: typeof result?.change === "number" ? result.change : 0,
        },
      };
      setLastReceipt(receiptPayload);
      setLastOrderId(result?.orderId || "");
      const pedidoDeNota = venda.payload.nota_fiscal as { emitir?: boolean; documento?: string | null } | null | undefined;
      if (pedidoDeNota?.emitir && result?.orderId) {
        /* O cupom fiscal sai na hora; com a impressão automática, vai para a impressora assim que a SEFAZ autorizar. */
        void iniciarCupomFiscal(result.orderId, { documento: pedidoDeNota.documento ?? null, numeroDoPedido: result.orderNumber ?? null });
      } else if (imprimirSozinho) {
        printPdvReceipt(receiptPayload);
      }
      setNotaFiscalEstado((antes) => ({ ...antes, emitir: antes.disponivel && modoDaNota === "sempre", documento: "" }));
      setReceiptPromptOpen(true);
      /* A tela do cliente agradece por alguns segundos antes de voltar à vitrine. */
      setVendaNaTela({
        numero: result?.orderNumber ?? null,
        troco: typeof result?.change === "number" ? result.change : 0,
        pago: venda.paidTotalSnapshot,
      });
      mostrarEntregaDigital(result?.entregaDigital?.link);
      /* O saldo mudou: o selo "Esgotado" da vitrine é perguntado de novo. */
      esquecerEsgotados();
      setCart([]);
      setDiscountInput("");
      setSelectedClientId("");
      setSelectedBankAccountId((prev) => prev || "");
      setSelectedElectronicBankAccountId((prev) => prev || "");
      setSellerMode("single");
      setPrimarySellerUserId((prev) => prev || "");
      setSellerAllocations([
        {
          id: makeId(),
          seller_user_id: primarySellerUserId,
          allocation_percent: "100,00",
        },
      ]);
      setPayments([{ id: makeId(), method: quickMethod, amount_input: "" }]);
      setCashReceivedInput("");
      setActiveItemId(null);
      setShowFreeItemForm(false);
      setCobranca(null);
      setFaseDoPix("parado");
      setErroDoPix(null);
      setPasso("vender");
      void carregarPendentesRef.current();
      /* O caixa físico só era relido ao trocar de conta ou ao operar a gaveta.
         Uma venda em dinheiro entrava no banco e o saldo do topo continuava o
         mesmo até recarregar a página — e era esse número velho que o
         fechamento usava para propor o valor contado. */
      void fetchCashSession(selectedBankAccountId);
    },
    [
      storeName,
      companyLogoUrl,
      companyCnpj,
      companyWhatsapp,
      companyAddress,
      primarySellerUserId,
      quickMethod,
      fetchCashSession,
      selectedBankAccountId,
      modoDaNota,
      iniciarCupomFiscal,
      mostrarEntregaDigital,
      imprimirSozinho,
      esquecerEsgotados,
    ]
  );

  const finalizeSale = useCallback(async (pixChargeId: string | null = null) => {
    if (!canFinalize || isFinalizing) return;
    if (!validarVendedor()) return;
    if (!pixChargeId && !validarClienteDoDigital()) return;

    const venda = montarVenda(pixChargeId);

    setIsFinalizing(true);
    setSaleMessage("");

    try {
      const response = await fetch("/api/pdv/checkout", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify(venda.payload),
      });

      const result = (await response.json().catch(() => null)) as
        | {
            error?: string;
            orderId?: string;
            orderNumber?: number | null;
            clientName?: string;
            change?: number;
            entregaDigital?: { link?: string } | null;
          }
        | null;

      if (!response.ok) {
        throw new Error(result?.error || "Falha ao concluir a venda no PDV.");
      }

      encerrarVenda(result, venda, total);
    } catch (error) {
      const message = error instanceof Error ? error.message : "Falha ao concluir a venda no PDV.";
      if (pixChargeId) {
        /* O Pix já caiu; a venda é que não gravou. Nada de perder o dinheiro
           de vista: a cobrança fica presa a esta venda, o painel destrava e o
           caixa ajusta o que faltar antes de concluir de novo. */
        setErroDoPix(`O Pix foi pago, mas a venda não foi registrada: ${message}`);
      } else {
        avisar(message);
      }
    } finally {
      setIsFinalizing(false);
      fechandoComPixRef.current = false;
    }
  }, [canFinalize, isFinalizing, validarVendedor, validarClienteDoDigital, montarVenda, encerrarVenda, total]);

  /* ---------- Pix pelo provedor configurado ---------- */

  const gerarPix = useCallback(async () => {
    if (!canFinalize || isFinalizing || faseDoPix !== "parado") return;
    if (!validarVendedor()) return;
    if (!validarClienteDoDigital()) return;

    setErroDoPix(null);
    setSaleMessage("");
    setFaseDoPix("preparando");

    /*
      O ensaio: o servidor confere a venda inteira (vendedor, quantidades,
      contas, caixa) sem gravar nada. Regra que recusaria a venda aparece
      agora — nunca depois de o cliente ter pago.
    */
    const venda = montarVenda(null);
    try {
      const ensaio = await fetch("/api/pdv/checkout", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({ ...venda.payload, simular: true }),
      });
      const resultado = (await ensaio.json().catch(() => null)) as { error?: string } | null;
      if (!ensaio.ok) {
        setFaseDoPix("parado");
        avisar(resultado?.error || "A venda não passou na conferência. Ajuste e tente de novo.");
        return;
      }
    } catch {
      setFaseDoPix("parado");
      avisar("Não foi possível conferir a venda antes de cobrar. Tente de novo.");
      return;
    }

    /* Uma cobrança, um pagamento: as partes em Pix viram uma só, do valor
       cobrado. Dividir Pix com Pix não tem sentido quando quem cobra é um QR. */
    const valorDoPix = Math.round(pixTotal * 100) / 100;
    setPayments((antes) => [
      ...antes.filter((parte) => parte.method !== "pix"),
      { id: makeId(), method: "pix", amount_input: formatMoneyInput(String(Math.round(valorDoPix * 100))) },
    ]);

    await esperar(600);
    setFaseDoPix("gerando");
    const comecou = Date.now();
    try {
      const resposta = await fetch("/api/pdv/pix", {
        method: "POST",
        headers: { "content-type": "application/json" },
        body: JSON.stringify({
          amount: valorDoPix,
          client_id: selectedClientId || null,
          /* A foto da venda vai junto: se a aba cair depois do Pix pago, a
             venda é refeita daqui. */
          snapshot: {
            payload: venda.payload,
            resumo: {
              itens: cart.map((item) => `${Math.max(1, item.quantity || 1)}× ${item.title}`),
              total: venda.totalSnapshot,
              cliente: venda.fallbackClientName,
            },
          },
        }),
      });
      const dados = (await resposta.json().catch(() => null)) as { error?: string; cobranca?: CobrancaPix } | null;
      if (!resposta.ok || !dados?.cobranca) {
        throw new Error(dados?.error || "Não foi possível gerar o Pix.");
      }
      const falta = 900 - (Date.now() - comecou);
      if (falta > 0) await esperar(falta);
      setCobranca(dados.cobranca);
      if (dados.cobranca.status === "approved") {
        setFaseDoPix("aprovado");
      } else {
        setFaseDoPix("aguardando");
      }
    } catch (erro) {
      setFaseDoPix("falhou");
      setErroDoPix(erro instanceof Error ? erro.message : "Não foi possível gerar o Pix.");
      window.setTimeout(() => setFaseDoPix("parado"), 300);
    }
  }, [canFinalize, isFinalizing, faseDoPix, validarVendedor, validarClienteDoDigital, montarVenda, pixTotal, selectedClientId, cart]);

  const cancelarPix = useCallback(async () => {
    if (!cobranca) return;
    const id = cobranca.id;
    setFaseDoPix("parado");
    setCobranca(null);
    setErroDoPix(null);
    try {
      const resposta = await fetch(`/api/pdv/pix?id=${encodeURIComponent(id)}`, { method: "DELETE" });
      const dados = (await resposta.json().catch(() => null)) as { cobranca?: CobrancaPix } | null;
      /* Cancelou no mesmo instante em que o cliente pagou: o dinheiro venceu.
         A cobrança volta para a tela como paga e a venda segue. */
      if (dados?.cobranca?.status === "approved") {
        const paga = dados.cobranca;
        setCobranca(paga);
        setPayments((antes) => [
          ...antes.filter((parte) => parte.method !== "pix"),
          { id: makeId(), method: "pix", amount_input: formatMoneyInput(String(Math.round(paga.valor * 100))) },
        ]);
        setFaseDoPix("aprovado");
      }
    } catch {
      /* o registro fecha na próxima conferência */
    }
  }, [cobranca]);

  /* Enquanto o QR está na tela, o balcão pergunta ao servidor de 4 em 4 s. */
  useEffect(() => {
    if (faseDoPix !== "aguardando" || !cobranca?.id) return;
    const id = cobranca.id;
    let parado = false;
    const conferir = async () => {
      try {
        const resposta = await fetch(`/api/pdv/pix?id=${encodeURIComponent(id)}`, { cache: "no-store" });
        const dados = (await resposta.json().catch(() => null)) as { cobranca?: CobrancaPix } | null;
        if (parado || !dados?.cobranca) return;
        const atual = dados.cobranca;
        if (atual.status === "approved") {
          setCobranca(atual);
          setPayments((antes) => [
            ...antes.filter((parte) => parte.method !== "pix"),
            { id: makeId(), method: "pix", amount_input: formatMoneyInput(String(Math.round(atual.valor * 100))) },
          ]);
          setFaseDoPix("aprovado");
        } else if (atual.status === "expired" || atual.status === "cancelled" || atual.status === "rejected") {
          setCobranca(atual);
          setFaseDoPix("falhou");
          setErroDoPix(
            atual.status === "expired"
              ? "O Pix venceu sem pagamento. Gere outro ou receba de outro jeito."
              : atual.status === "rejected"
                ? `O ${mp?.provedorNome || "provedor"} recusou o pagamento.`
                : "A cobrança foi cancelada."
          );
          window.setTimeout(() => {
            if (!parado) {
              setCobranca(null);
              setFaseDoPix("parado");
            }
          }, 5000);
        }
      } catch {
        /* tenta de novo no próximo tique */
      }
    };
    void conferir();
    const relogio = window.setInterval(() => void conferir(), 4000);
    return () => {
      parado = true;
      window.clearInterval(relogio);
    };
  }, [faseDoPix, cobranca?.id]);

  /* Pix confirmado: a venda fecha sozinha, uma vez só. */
  useEffect(() => {
    if (faseDoPix !== "aprovado" || !cobranca?.id || fechandoComPixRef.current || isFinalizing) return;
    if (erroDoPix) return;
    fechandoComPixRef.current = true;
    void finalizeSale(cobranca.id);
  }, [faseDoPix, cobranca?.id, isFinalizing, erroDoPix, finalizeSale]);

  const concluir = useCallback(() => {
    if (faseDoPix === "aprovado" && cobranca?.id) {
      setErroDoPix(null);
      void finalizeSale(cobranca.id);
      return;
    }
    if (usaMercadoPago) {
      void gerarPix();
      return;
    }
    void finalizeSale(null);
  }, [faseDoPix, cobranca?.id, usaMercadoPago, gerarPix, finalizeSale]);

  /*
    A venda que ficou para trás: refeita da foto guardada na cobrança, com o
    pagamento em Pix apontando para ela. Sem foto (cobrança anterior a isto),
    entra como venda avulsa do valor pago — o caixa bate, o pedido existe.
  */
  const concluirPendente = useCallback(
    async (pendente: CobrancaPix) => {
      if (concluindoPendenteId || isFinalizing) return;
      if (!podeFecharVenda) return;
      setConcluindoPendenteId(pendente.id);
      setSaleMessage("");
      try {
        const base = pendente.foto?.payload as Record<string, unknown> | undefined;
        const contaPix = mp?.conta?.id || selectedElectronicBankAccountId || null;
        const payload: Record<string, unknown> = base
          ? {
              ...base,
              payments: (Array.isArray(base.payments) ? (base.payments as Array<Record<string, unknown>>) : []).map((parte) =>
                parte.method === "pix" ? { ...parte, amount: pendente.valor, pix_charge_id: pendente.id } : parte
              ),
            }
          : {
              client_id: null,
              bank_account_id: contaPix,
              seller_mode: "single",
              primary_seller_user_id: primarySellerUserId || null,
              allocations: [{ seller_user_id: primarySellerUserId, allocation_percent: 100, is_primary: true }],
              discount: 0,
              cash_received: 0,
              items: [
                {
                  product_id: null,
                  title: `Pix pago no balcão (${mp?.provedorNome || "Pix"})`,
                  quantity: 1,
                  unit_type: "unit",
                  width_m: null,
                  height_m: null,
                  unit_price: pendente.valor,
                  note: `Cobrança ${pendente.id}`,
                  is_free_item: true,
                  selected_options: null,
                },
              ],
              payments: [{ method: "pix", amount: pendente.valor, bank_account_id: contaPix, pix_charge_id: pendente.id }],
            };
        /* Se a foto guardou mais de uma parte em Pix, elas viram uma só do
           valor pago — é o que o servidor exige. */
        const partes = Array.isArray(payload.payments) ? (payload.payments as Array<Record<string, unknown>>) : [];
        const pixPartes = partes.filter((parte) => parte.method === "pix");
        if (pixPartes.length > 1) {
          payload.payments = [
            ...partes.filter((parte) => parte.method !== "pix"),
            { ...pixPartes[0], amount: pendente.valor, pix_charge_id: pendente.id },
          ];
        }
        const partesFinais = Array.isArray(payload.payments) ? (payload.payments as Array<Record<string, unknown>>) : [];

        const response = await fetch("/api/pdv/checkout", {
          method: "POST",
          headers: { "content-type": "application/json" },
          body: JSON.stringify(payload),
        });
        const result = (await response.json().catch(() => null)) as
          | { error?: string; orderId?: string; orderNumber?: number | null; clientName?: string; change?: number; entregaDigital?: { link?: string } | null }
          | null;
        if (!response.ok) throw new Error(result?.error || "Não foi possível concluir a venda pendente.");
        mostrarEntregaDigital(result?.entregaDigital?.link);

        const itens = Array.isArray(payload.items) ? (payload.items as Array<Record<string, unknown>>) : [];
        const itensDoCupom = itens.map((item) => {
          const pecas = Math.max(1, Number(item.quantity) || 1);
          const area =
            item.unit_type === "square_meter" ? (Number(item.width_m) || 1) * (Number(item.height_m) || 1) : 1;
          const preco = Number(item.unit_price) || 0;
          const minimo = Number(item.valor_minimo_aplicado) > 0 ? Number(item.valor_minimo_aplicado) : null;
          return {
            title: String(item.title || "Item"),
            quantity: pecas,
            unit_price: minimo ? minimo / pecas : preco * area,
            line_total: minimo ?? pecas * area * preco,
            sku: null,
            barcode: null,
            note: typeof item.note === "string" ? item.note : null,
          };
        });
        const pagamentos = partesFinais.map((parte) => ({ method: String(parte.method || "pix"), amount: Number(parte.amount) || 0 }));
        const subtotalDaVenda = itensDoCupom.reduce((soma, item) => soma + item.line_total, 0);
        const descontoDaVenda = Number(payload.discount) || 0;
        setLastReceipt({
          company: {
            name: storeName || "Minha empresa",
            logo_url: companyLogoUrl || null,
            cnpj: companyCnpj || null,
            whatsapp: companyWhatsapp || null,
            address: companyAddress || null,
          },
          order: { id: result?.orderId || "", number: result?.orderNumber ?? null, created_at: new Date().toISOString() },
          client: { name: result?.clientName || "Cliente balcão" },
          items: itensDoCupom,
          payments: pagamentos,
          totals: {
            subtotal: subtotalDaVenda,
            discount: descontoDaVenda,
            total: Math.max(0, subtotalDaVenda - descontoDaVenda),
            paid_total: pagamentos.reduce((soma, parte) => soma + parte.amount, 0),
            change: typeof result?.change === "number" ? result.change : 0,
          },
        });
        setLastOrderId(result?.orderId || "");
        const notaPedida = payload.nota_fiscal as { emitir?: boolean; documento?: string | null } | null | undefined;
        if (notaPedida?.emitir && result?.orderId) {
          void iniciarCupomFiscal(result.orderId, { documento: notaPedida.documento ?? null, numeroDoPedido: result.orderNumber ?? null });
        }
        avisar(`${result?.orderNumber ? `Pedido #${result.orderNumber}` : "Pedido"} registrado a partir do Pix de ${formatMoney(pendente.valor)}.`, "ok");
        setReceiptPromptOpen(true);
        setPendentes((antes) => antes.filter((item) => item.id !== pendente.id));
        void fetchCashSession(selectedBankAccountId);
      } catch (erro) {
        avisar(erro instanceof Error ? erro.message : "Não foi possível concluir a venda pendente.");
      } finally {
        setConcluindoPendenteId(null);
      }
    },
    [
      concluindoPendenteId,
      isFinalizing,
      podeFecharVenda,
      mp?.conta?.id,
      selectedElectronicBankAccountId,
      primarySellerUserId,
      storeName,
      companyLogoUrl,
      companyCnpj,
      companyWhatsapp,
      companyAddress,
      fetchCashSession,
      selectedBankAccountId,
      iniciarCupomFiscal,
      mostrarEntregaDigital,
    ]
  );

  /**
   * Abre a janela de operação do caixa.
   *
   * O fechamento relê o saldo ANTES de propor o valor contado. Antes ele usava
   * `cashTotals` da memória da tela, que só era atualizado ao abrir a página ou
   * ao mexer na gaveta: quem vendeu e foi fechar via a proposta do saldo de
   * quando abriu o PDV. Como o servidor calcula a diferença com o saldo de
   * verdade, o fechamento gravava uma falta que nunca existiu.
   */
  async function abrirOperacaoDeCaixa(acao: AcaoDoCaixa) {
    setMenuAberto(null);
    setCashAmountInput("");
    setCashNotesInput("");
    setCashCountedInput("");
    setCashModalAction(acao);
    if (acao !== "close") return;

    const lido = await fetchCashSession(selectedBankAccountId);
    const saldo = lido?.totais?.expected_balance ?? cashTotals?.expected_balance ?? 0;
    setCashCountedInput(formatMoneyInput(String(Math.round(saldo * 100))));
  }

  const abrirReceber = useCallback(async () => {
    if (!cart.length || !podeFecharVenda) return;
    /* Sem estoque para o cupom: diz o que falta antes de receber. */
    const falta = conferenciaDoEstoque.frase ?? (await conferenciaDoEstoque.conferirAgora());
    if (falta) {
      avisoDeEstoqueMostrado.current = falta;
      avisar(falta);
      return;
    }
    setSaleMessage("");
    setPasso("receber");
    if (paidTotal <= 0.0001) applyQuickPayment(quickMethod);
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [cart.length, podeFecharVenda, paidTotal, applyQuickPayment, quickMethod, conferenciaDoEstoque]);

  const voltarParaVender = useCallback(() => {
    if (faseDoPix === "preparando" || faseDoPix === "gerando" || faseDoPix === "aguardando") return;
    setPasso("vender");
  }, [faseDoPix]);

  useEffect(() => {
    if (cart.length === 0 && passo === "receber" && faseDoPix === "parado") setPasso("vender");
  }, [cart.length, passo, faseDoPix]);

  useEffect(() => {
    if (!justAddedCartItemId) return;
    const timeout = window.setTimeout(() => setJustAddedCartItemId(null), 320);
    return () => window.clearTimeout(timeout);
  }, [justAddedCartItemId]);

  useEffect(() => {
    function onKeyDown(event: KeyboardEvent) {
      const target = event.target as HTMLElement | null;
      const isTypingField =
        target instanceof HTMLInputElement ||
        target instanceof HTMLTextAreaElement ||
        target instanceof HTMLSelectElement;
      if (event.key === "F2") {
        event.preventDefault();
        searchInputRef.current?.focus();
        searchInputRef.current?.select();
        return;
      }
      if (event.altKey && event.key.toLowerCase() === "n") {
        event.preventDefault();
        setShowFreeItemForm((prev) => !prev);
        return;
      }
      if (event.key === "F8") {
        event.preventDefault();
        rotateQuickMethod();
        return;
      }
      if (event.key === "F9") {
        event.preventDefault();
        if (passo === "vender") abrirReceber();
        else concluir();
        return;
      }
      if (event.key === "Escape") {
        setMenuAberto(null);
        if (passo === "receber") voltarParaVender();
        setActiveItemId(null);
      }
      if (!isTypingField && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
        event.preventDefault();
        navigateCategory(event.key === "ArrowDown" ? 1 : -1);
        return;
      }
      if (event.key === "Enter" && !isTypingField) {
        event.preventDefault();
        if (passo === "vender") abrirReceber();
        else concluir();
      }
    }
    window.addEventListener("keydown", onKeyDown);
    return () => window.removeEventListener("keydown", onKeyDown);
  }, [rotateQuickMethod, navigateCategory, passo, abrirReceber, concluir, voltarParaVender]);

  /* Clique fora fecha o menu da barra. */
  useEffect(() => {
    if (!menuAberto) return;
    const fechar = () => setMenuAberto(null);
    window.addEventListener("click", fechar);
    return () => window.removeEventListener("click", fechar);
  }, [menuAberto]);

  /* ---------- a tela do cliente ---------- */

  useEffect(() => {
    if (!vendaNaTela) return;
    /* Com produto digital, o QR code fica mais tempo: o cliente precisa apontar o celular. */
    const relogio = window.setTimeout(() => setVendaNaTela(null), vendaNaTela.entrega ? 120000 : 9000);
    return () => window.clearTimeout(relogio);
  }, [vendaNaTela]);

  const clienteNaTela = useMemo(() => {
    if (!selectedClientId) return null;
    const cliente = clients.find((entrada) => entrada.id === selectedClientId);
    return cliente ? cliente.company_name || cliente.name : null;
  }, [clients, selectedClientId]);

  const faseDaTela: FaseDaTela = vendaNaTela
    ? "aprovado"
    : faseDoPix === "preparando"
      ? "preparando"
      : faseDoPix === "gerando"
        ? "gerando"
        : faseDoPix === "aguardando"
          ? "pagar"
          : faseDoPix === "aprovado"
            ? "aprovado"
            : faseDoPix === "falhou" && cobranca
              ? cobranca.status === "expired"
                ? "expirado"
                : "cancelado"
              : cart.length
                ? "conta"
                : "ocioso";

  const estadoDaTelaSerializado = useMemo(() => {
    const estado: Omit<EstadoDaTela, "seq" | "em" | "origem" | "ativoEm"> = {
      fase: faseDaTela,
      itens: cart.map((item) => ({
        id: item.id,
        titulo: item.title,
        quantidade: Math.max(1, item.quantity || 1),
        medida: item.unit_type === "square_meter" ? resumoDaMedida(item) : null,
        detalhe:
          [item.selected_options ? getSelectedOptionsSummary(item.selected_options) : null, minimoDaLinhaDoPdv(item) ? "Valor mínimo" : null]
            .filter(Boolean)
            .join(" · ") || null,
        total: totalDaLinha(item),
      })),
      subtotal,
      desconto: discountValue,
      total,
      cliente: clienteNaTela,
      pix: cobranca
        ? { valor: cobranca.valor, qrCode: cobranca.qrCode, qrCodeBase64: cobranca.qrCodeBase64, expiraEm: cobranca.expiraEm }
        : null,
      venda: vendaNaTela,
    };
    return JSON.stringify(estado);
  }, [faseDaTela, cart, subtotal, discountValue, total, clienteNaTela, cobranca, vendaNaTela]);

  const mandarParaATelaRef = useRef<() => void>(() => undefined);

  const mandarParaATela = useCallback(() => {
    const ponte = ponteRef.current;
    if (!ponte) return;
    seqRef.current += 1;
    const estado: EstadoDaTela = {
      ...(JSON.parse(estadoDaTelaSerializado) as Omit<EstadoDaTela, "seq" | "em" | "origem" | "ativoEm">),
      seq: seqRef.current,
      em: new Date().toISOString(),
      origem: origemRef.current,
      ativoEm: ativoEmRef.current,
    };
    estadoDaTelaRef.current = estado;
    ponte.enviar({ evento: "estado", estado });
  }, [estadoDaTelaSerializado]);

  mandarParaATelaRef.current = mandarParaATela;

  useEffect(() => {
    if (!telaTenantId) return;
    const ponte = abrirPonte(telaTenantId, (mensagem) => {
      /* A tela acabou de abrir e pediu o estado: manda o de agora. */
      if (mensagem.evento === "ola") mandarParaATelaRef.current();
    });
    ponteRef.current = ponte;
    return () => {
      ponte.fechar();
      ponteRef.current = null;
    };
  }, [telaTenantId]);

  /* Mudou o estado por ação de quem está no balcão: marca a hora (a primeira
     passada é só a montagem, não conta) e manda. */
  useEffect(() => {
    if (ultimoEstadoVistoRef.current !== null && ultimoEstadoVistoRef.current !== estadoDaTelaSerializado) {
      ativoEmRef.current = Date.now();
    }
    ultimoEstadoVistoRef.current = estadoDaTelaSerializado;
    mandarParaATela();
  }, [mandarParaATela, estadoDaTelaSerializado, telaTenantId]);

  useEffect(() => {
    const relogio = window.setInterval(() => mandarParaATelaRef.current(), 10000);
    return () => window.clearInterval(relogio);
  }, []);

  function abrirTelaDoCliente() {
    window.open("/pdv/tela", "ktalogu-pdv-tela", "popup=yes,width=1280,height=800");
  }

  const vendedorAtual = useMemo(() => {
    if (sellerMode === "split") return `Rateio · ${normalizedSellerAllocations.length}`;
    const vendedor = sellers.find((entrada) => entrada.user_id === primarySellerUserId);
    return vendedor ? vendedor.email.split("@")[0] : "Escolher";
  }, [sellerMode, normalizedSellerAllocations.length, sellers, primarySellerUserId]);

  const itemCount = cart.reduce((sum, item) => sum + Math.max(1, item.quantity || 1), 0);
  const totalDoRateio = normalizedSellerAllocations.reduce((sum, a) => sum + a.allocationPercent, 0);

  return (
    <div className="pdv">
      <header className="pdv-barra">
        <div className="pdv-marca">
          {companyLogoUrl ? (
            <span className="pdv-marca-logo" style={corDaLoja ? { background: corDaLoja } : undefined}>
              <Image src={companyLogoUrl} alt={storeName} width={120} height={30} sizes="120px" loader={passthroughLoader} unoptimized />
            </span>
          ) : (
            <span className="pdv-marca-inicial">{storeName?.[0]?.toUpperCase() || "E"}</span>
          )}
          <span className="pdv-marca-nome">{storeName}</span>
        </div>

        <div className="pdv-seg-select">
          <CustomSelect
            options={clientOptions}
            value={selectedClientId}
            onChange={(valor) => {
              setSelectedClientId(valor);
              /* Escolheu o cliente: o aviso que pedia o cliente do produto digital sai da tela. */
              if (valor && saleMessage === AVISO_DO_CLIENTE_DO_DIGITAL) setSaleMessage("");
            }}
            searchable
            placeholder="Venda sem cliente"
          />
        </div>

        <button
          type="button"
          className={`pdv-seg ${primarySellerUserId || sellerMode === "split" ? "" : "pdv-seg-ativo"}`}
          onClick={() => setSellerModalOpen(true)}
          title="Quem vendeu"
        >
          <UserCheck size={15} />
          <span className="pdv-seg-rotulo">Vendedor</span>
          <span className="pdv-seg-valor">{vendedorAtual}</span>
        </button>

        <div className="pdv-barra-direita">
          <div className="pdv-seg-envelope">
          <button
            type="button"
            className={`pdv-seg ${menuAberto === "caixa" ? "pdv-seg-ativo" : ""}`}
            onClick={(evento) => {
              evento.stopPropagation();
              setMenuAberto((antes) => (antes === "caixa" ? null : "caixa"));
            }}
            title="Caixa físico e contas"
          >
            <Wallet size={15} />
            <span className={`pdv-ponto ${hasOpenCashSession ? "pdv-ponto-verde" : selectedBankAccountId ? "pdv-ponto-ambar" : ""}`} />
            <span className="pdv-seg-valor">
              {hasOpenCashSession
                ? `Caixa aberto · ${formatMoney(cashTotals?.expected_balance || 0)}`
                : selectedBankAccountId
                  ? "Caixa fechado"
                  : "Sem caixa físico"}
            </span>
          </button>
            {menuAberto === "caixa" ? (
              <MenuDoCaixa
                cashSession={cashSession}
                cashTotals={cashTotals}
                cashRecentSessions={cashRecentSessions}
                bankAccountOptions={bankAccountOptions}
                selectedBankAccountId={selectedBankAccountId}
                setSelectedBankAccountId={setSelectedBankAccountId}
                electronicBankAccountOptions={electronicBankAccountOptions}
                selectedElectronicBankAccountId={selectedElectronicBankAccountId}
                setSelectedElectronicBankAccountId={setSelectedElectronicBankAccountId}
                aoAgir={(acao) => {
                  void abrirOperacaoDeCaixa(acao);
                }}
                aoHistorico={(sessionId) => {
                  setMenuAberto(null);
                  void openCashHistory(sessionId);
                }}
              />
            ) : null}
          </div>

          <div className="pdv-seg-envelope">
            <button
              type="button"
              className={`pdv-seg ${menuAberto === "cupom" ? "pdv-seg-ativo" : ""}`}
              onClick={(evento) => {
                evento.stopPropagation();
                setMenuAberto((antes) => (antes === "cupom" ? null : "cupom"));
              }}
              title="Cupom fiscal e impressão"
            >
              <ReceiptText size={15} />
              <span className={`pdv-ponto ${notaFiscal.disponivel && modoDaNota === "sempre" ? "pdv-ponto-verde" : ""}`} />
              <span className="pdv-seg-valor">Cupom</span>
            </button>
            {menuAberto === "cupom" ? (
              <MenuDoCupom
                notaDisponivel={notaFiscal.disponivel}
                notaEmTeste={notaFiscal.teste}
                modoDaNota={modoDaNota}
                motivoDaNota={motivoDaNota}
                podeConfigurar={podeConfigurarNota}
                salvando={salvandoModoDaNota}
                erro={erroDoModoDaNota}
                aoMudarEmTodaVenda={(valor) => void mudarCupomEmTodaVenda(valor)}
                imprimirSozinho={imprimirSozinho}
                aoMudarImprimirSozinho={setImprimirSozinho}
              />
            ) : null}
          </div>

          <button type="button" className="pdv-seg" onClick={abrirTelaDoCliente} title="Abre em outra janela: arraste para o segundo monitor">
            <MonitorSmartphone size={15} />
            Tela do cliente
          </button>

          <div className="pdv-seg-envelope">
          <button
            type="button"
            className={`pdv-seg ${menuAberto === "atalhos" ? "pdv-seg-ativo" : ""}`}
            onClick={(evento) => {
              evento.stopPropagation();
              setMenuAberto((antes) => (antes === "atalhos" ? null : "atalhos"));
            }}
            aria-label="Atalhos do teclado"
            title="Atalhos"
          >
            <HelpCircle size={15} />
          </button>
            {menuAberto === "atalhos" ? (
              <div className="pdv-menu" onClick={(evento) => evento.stopPropagation()}>
                <div className="pdv-menu-rotulo">Atalhos</div>
                <div className="pdv-atalhos">
                  <div className="pdv-atalho">
                    <span>Buscar / ler código</span>
                    <kbd>F2</kbd>
                  </div>
                  <div className="pdv-atalho">
                    <span>Item livre</span>
                    <kbd>Alt+N</kbd>
                  </div>
                  <div className="pdv-atalho">
                    <span>Trocar categoria</span>
                    <kbd>↑ ↓</kbd>
                  </div>
                  <div className="pdv-atalho">
                    <span>Trocar meio de pagamento</span>
                    <kbd>F8</kbd>
                  </div>
                  <div className="pdv-atalho">
                    <span>Receber / confirmar</span>
                    <kbd>F9 · Enter</kbd>
                  </div>
                  <div className="pdv-atalho">
                    <span>Voltar / fechar</span>
                    <kbd>Esc</kbd>
                  </div>
                </div>
              </div>
            ) : null}
          </div>
        </div>
      </header>

      <div className="pdv-faixa-slot">
        {!pendentesOcultas && podeFecharVenda ? (
          <PixPendente
            cobrancas={pendentes}
            ocupadaId={concluindoPendenteId}
            aoConcluir={(pendente) => void concluirPendente(pendente)}
            aoDepois={() => setPendentesOcultas(true)}
          />
        ) : null}
      </div>

      <div className="pdv-corpo">
        <Vitrine
          query={query}
          setQuery={setQuery}
          searchInputRef={searchInputRef}
          aoConfirmarBusca={confirmarBusca}
          categoryItems={categoryItems}
          selectedCategory={selectedCategory}
          setSelectedCategory={setSelectedCategory}
          categoryButtonRefs={categoryButtonRefs}
          loading={loading}
          visibleProducts={visibleProducts}
          totalFiltrado={filteredProducts.length}
          safePage={safePage}
          totalPages={totalPages}
          setCurrentPage={setCurrentPage}
          addProduct={addProduct}
          showFreeItemForm={showFreeItemForm}
          setShowFreeItemForm={setShowFreeItemForm}
          freeItemName={freeItemName}
          setFreeItemName={setFreeItemName}
          freeItemPriceInput={freeItemPriceInput}
          setFreeItemPriceInput={setFreeItemPriceInput}
          addFreeItem={addFreeItem}
          esgotado={esgotadosDaLista.esgotado}
        />

        {passo === "vender" ? (
          <Cupom
            cart={cart}
            products={products}
            productImageById={productImageById}
            activeItemId={activeItemId}
            setActiveItemId={setActiveItemId}
            justAddedCartItemId={justAddedCartItemId}
            measureDrafts={measureDrafts}
            setMeasureDrafts={setMeasureDrafts}
            updateItem={updateItem}
            removeItem={removeItem}
            changeQuantity={changeQuantity}
            updateItemVariationSelection={updateItemVariationSelection}
            subtotal={subtotal}
            discountInput={discountInput}
            setDiscountInput={setDiscountInput}
            total={total}
            aoReceber={abrirReceber}
            podeFecharVenda={podeFecharVenda}
            aviso={saleMessage && saleMessageTom === "erro" ? saleMessage : null}
            pixPago={faseDoPix === "aprovado" && cobranca ? { valor: cobranca.valor, aprovadaEm: cobranca.aprovadaEm ?? null } : null}
            faltasDeEstoque={faltasDeEstoque}
          />
        ) : (
          <Receber
            total={total}
            itemCount={itemCount}
            payments={payments}
            setPayments={setPayments}
            paymentMethodOptions={paymentMethodOptions}
            quickMethod={quickMethod}
            applyQuickPayment={applyQuickPayment}
            cashReceivedInput={cashReceivedInput}
            setCashReceivedInput={setCashReceivedInput}
            change={change}
            paidTotal={paidTotal}
            missingPayment={missingPayment}
            overPayment={overPayment}
            hasCashPayment={hasCashPayment}
            hasPixPayment={hasPixPayment}
            hasCardPayment={hasCardPayment}
            selectedBankAccountId={selectedBankAccountId}
            setSelectedBankAccountId={setSelectedBankAccountId}
            bankAccountOptions={bankAccountOptions}
            selectedElectronicBankAccountId={selectedElectronicBankAccountId}
            setSelectedElectronicBankAccountId={setSelectedElectronicBankAccountId}
            electronicBankAccountOptions={electronicBankAccountOptions}
            requiresOpenCashSession={requiresOpenCashSession}
            hasOpenCashSession={hasOpenCashSession}
            saleMessage={saleMessage}
            saleMessageTom={saleMessageTom}
            canFinalize={canFinalize}
            isFinalizing={isFinalizing}
            aoConcluir={concluir}
            aoVoltar={voltarParaVender}
            mp={mp}
            mpLigado={mpLigado}
            setMpLigado={setMpLigado}
            usaMercadoPago={usaMercadoPago}
            pixTotal={pixTotal}
            cobranca={cobranca}
            faseDoPix={faseDoPix}
            erroDoPix={erroDoPix}
            aoCancelarPix={() => void cancelarPix()}
            notaFiscal={notaFiscal}
            setNotaFiscal={setNotaFiscal}
          />
        )}
      </div>

      <ModalDoCaixa
        acao={cashModalAction}
        aoFechar={() => setCashModalAction(null)}
        saldoEsperado={cashTotals?.expected_balance || 0}
        cashAmountInput={cashAmountInput}
        setCashAmountInput={setCashAmountInput}
        cashCountedInput={cashCountedInput}
        setCashCountedInput={setCashCountedInput}
        cashNotesInput={cashNotesInput}
        setCashNotesInput={setCashNotesInput}
        ocupado={cashActionLoading}
        aoConfirmar={() => void submitCashAction()}
      />

      <ModalDoHistorico
        aberto={cashHistoryOpen}
        aoFechar={() => setCashHistoryOpen(false)}
        carregando={cashHistoryLoading}
        sessao={cashHistorySession}
        totais={cashHistoryTotals}
        movimentos={cashHistoryMovements}
        sessoesRecentes={cashRecentSessions}
        aoEscolherSessao={(sessionId) => void openCashHistory(sessionId)}
      />

      <ModalDoCupom
        aberto={receiptPromptOpen}
        aoFechar={() => setReceiptPromptOpen(false)}
        cupom={lastReceipt}
        pedidoId={lastOrderId}
        resumo={saleMessage}
        cupomFiscal={lastOrderId ? cupomNoBalcao.cupons[lastOrderId] ?? null : null}
        aoImprimirCupomFiscal={() => void cupomNoBalcao.imprimir(lastOrderId)}
        aoTentarCupomDeNovo={() => cupomNoBalcao.tentarDeNovo(lastOrderId)}
        aoConsultarCupomDeNovo={() => cupomNoBalcao.consultarDeNovo(lastOrderId)}
        entregaDigital={entregaDigital}
      />

      <ModalDoVendedor
        aberto={sellerModalOpen}
        aoFechar={() => setSellerModalOpen(false)}
        sellerMode={sellerMode}
        setSellerMode={setSellerMode}
        sellerOptions={sellerOptions}
        primarySellerUserId={primarySellerUserId}
        setPrimarySellerUserId={setPrimarySellerUserId}
        sellerAllocations={sellerAllocations}
        updateSellerAllocation={updateSellerAllocation}
        removeSellerAllocation={removeSellerAllocation}
        addSellerAllocation={addSellerAllocation}
        totalDoRateio={totalDoRateio}
      />
    </div>
  );
}
