// js/components/pages/Orcamentos.jsx
// Gestão de orçamentos (pipeline + conversão VND)
// Extraído de index.html em Fase 6 (2026-04-29): L3691-L4523
// Deps runtime: fmt, fmtDate, today, saleFinalTotal, itemNet, nid, genIdUUID, toast, Modal, Icon, ConfirmDeleteModal, OrcPipeline, znxGuard, znxCounter, QuoteFormBody
(function() {
  'use strict';
  const {useState, useMemo, useEffect, useRef} = React;

  // [REFACTOR Wave 3 + Wave 4 KIMI + Backlog #579 + v224.55 2026-05-28] vars+check MOVED to component body (preventivo)
  // regra_validacao_helpers_runtime_quando_ordem_scripts_uncertain

  // Inflight guard para doConvert — evita double-submit entre renders
  let _inFlightConvert=false;
  // OB-006: inflight guards para saveNew/saveEdit/doDelete
  let _inFlightSaveNew=false;
  let _inFlightSaveEdit=false;
  let _inFlightDoDelete=false;

  // [UX 20260505 sistema 4] Helper centralizado de status terminal de orçamento
  // Usado pra esconder botões de Editar/Cancelar/Converter em orçamentos
  // que já estão em status final. Backend (trigger guard_quote_terminal)
  // bloqueia mas UX deve prevenir clique pra evitar erro técnico no toast.
  const TERMINAL_QUOTE_STATUS=['Convertido','Cancelado','Cancelada','Recusado','recusado'];
  function isTerminalQuoteStatus(status){
    return TERMINAL_QUOTE_STATUS.includes(status);
  }

function Orcamentos({user,quotes,setQuotes,products,setProducts,clients,sales,setSales,setReceivables,allUsers,discountRequests,setDiscountRequests}){
  // [v224.55 FIX-PREV-7 2026-05-28] vars+check em render time
  const Tabs = window.ZNX?.widgets?.Tabs;
  const ExitConfirmModal = window.ZNX?.widgets?.ExitConfirmModal;
  const ConcurrentEditModal = window.ZNX?.widgets?.orcamentos?.ConcurrentEditModal;
  const InsufficientStockConflictModal = window.ZNX?.widgets?.orcamentos?.InsufficientStockConflictModal;
  const QuoteViewModal = window.ZNX?.widgets?.orcamentos?.QuoteViewModal;
  const QuoteEditHeader = window.ZNX?.widgets?.orcamentos?.QuoteEditHeader;
  const QuoteEditFooter = window.ZNX?.widgets?.orcamentos?.QuoteEditFooter;
  // regra_falha_silenciosa_proibida — fail loud agregado (7 widgets)
  if (!Tabs || !ExitConfirmModal || !ConcurrentEditModal || !InsufficientStockConflictModal ||
      !QuoteViewModal || !QuoteEditHeader || !QuoteEditFooter) {
    const _msg = `[Orcamentos] widgets faltando: Tabs=${!!Tabs}, ExitConfirmModal=${!!ExitConfirmModal}, ConcurrentEditModal=${!!ConcurrentEditModal}, InsufficientStockConflictModal=${!!InsufficientStockConflictModal}, QuoteViewModal=${!!QuoteViewModal}, QuoteEditHeader=${!!QuoteEditHeader}, QuoteEditFooter=${!!QuoteEditFooter}`;
    console.error(_msg);
    window.Sentry?.captureMessage?.(_msg, 'error');
  }
  const vendedores=useMemo(()=>(allUsers||[]).filter(u=>u.role==='vendedor'),[allUsers]);
  // [v224.91 PERF 2026-05-31] Index products por id pra lookup O(1) em inner loops.
  // Antes: form.items.reduce → productsById[it.productId] = O(N×M) · 30 items × 1000 produtos = 30k lookups.
  // Agora: 1 build O(M) no useMemo + N lookups O(1) = 1030 ops vs 30000.
  // nid() def confirmada (js/lib/nid.js): String(a??'')===String(b??'') · sem normalização case/trim.
  // Index direto por id é SAFE pra UUIDs string (caso prático: productId sempre presente em form.items).
  // Refs: regra_extract_refactor_overhead_real · regra_ler_pg_get_functiondef_antes_de_spec_coalesce (leu nid)
  const productsById=useMemo(function(){
    const idx=Object.create(null);
    (products||[]).forEach(function(p){ if(p&&p.id) idx[p.id]=p; });
    return idx;
  },[products]);
  const isReadOnly=user.role==='financeiro';
  const EMPTY_FORM={clientId:'',items:[],validity:'',status:'Rascunho',notes:'',sellerName:user.name,frete:'Retirada',embalagem:'',enderecoEntrega:{nome:'',cep:'',endereco:'',numero:'',complemento:'',bairro:'',cidade:'',estado:''},obsTransportadora:'',nfEnabled:false,notaFiscal:{numero:'',serie:'',chave:'',emissao:'',cnpj:'',obs:''},globalDiscountType:'',globalDiscountValue:0,paymentMethod:'',paymentPixValue:0,paymentCashValue:0};
  const EMPTY_ITEM={productId:'',qty:1,price:0,discountPct:0,discountVal:0};

  const[modal,setModal]=useState(null); // null | 'new' | 'edit' | 'view'
  const[activeId,setActiveId]=useState(null);
  const[form,setForm]=useState(EMPTY_FORM);
  const[itemForm,setItemForm]=useState(EMPTY_ITEM);
  const[prodSearch,setProdSearch]=useState('');
  const[showProdDrop,setShowProdDrop]=useState(false);
  const[clientSearch,setClientSearch]=useState('');
  const[showClientDrop,setShowClientDrop]=useState(false);
  const[filterStatus,setFilterStatus]=useState('Todos');
  const[conflict,setConflict]=useState(null);
  const[pendingConvert,setPendingConvert]=useState(null);
  // [ONDA-E E3 2026-05-11] Sentry ZAYNEX-ERP-R (58 events): saveEdit batia client_modified_concurrently
  // e mostrava erro técnico crú no toast. Agora: 1 retry silencioso com timestamp fresco; se ainda
  // falhar, abre modal amigável com diff visual + botão "Recarregar dados".
  // { quoteId, freshQuote, attemptedFormSnapshot } | null
  const[concurrentEditModal,setConcurrentEditModal]=useState(null);
  const[deleteConfirm,setDeleteConfirm]=useState(null);
  const[searchOrc,setSearchOrc]=useState('');

  const activeQuote=useMemo(()=>quotes.find(q=>q.id===activeId),[quotes,activeId]);
  const[pendingNumber,setPendingNumber]=useState(null);
  const[exitConfirm,setExitConfirm]=useState(false);
  // [Onda V1 20260506 sistema 4] sistema de abas Lista / Pipeline / Insights / 360
  const[mainTab,setMainTab]=useState('lista');
  const[selected360,setSelected360]=useState(null);
  // [Onda V2 20260506 sistema 4] modal de cancelamento com motivo obrigatório (R$133k)
  const[cancelMotivoModal,setCancelMotivoModal]=useState(null); // { id, number, total, clientId } | null
  // [Onda V6 20260506 sistema 4] modal de motivo do desconto (vendedora justifica antes do admin ver)
  const[discountReasonModal,setDiscountReasonModal]=useState(null); // { totalDesconto, totalLiquido, client, formSnapshot } | null
  // [ONDA1-A 2026-05-11] regra_loading_state_obrigatorio — disabled feedback no botão.
  // Module-level vars (_inFlightSaveNew/Edit/Convert) já existem mas não triggam re-render.
  // Mantemos vars (compat) e espelhamos em state pra UI.
  const[isSavingQuote,setIsSavingQuote]=useState(false);
  const convertInflightRef=useRef(false);
  const[convertingQuoteId,setConvertingQuoteId]=useState(null);
  // [BUG-DUP-ORC 2026-05-11] idempotency key persiste enquanto modal aberto.
  // Reset SÓ em sucesso confirmado (após RPC voltar ok) ou ao fechar/abrir novo modal.
  // Se rede falhar mid-flight e user clicar Salvar de novo, MESMA key → RPC detecta replay.
  const saveIdemKeyRef=useRef(null);
  // [ONDA-A #9 2026-05-11] saveEdit também precisa idem estável (separado do saveNew).
  // TODO: updateQuoteAtomic ainda não aceita idempotencyKey — passar quando RPC for atualizada.
  const editIdemKeyRef=useRef(null);

  function confirmExit(){
    // If form has items or client selected, ask confirmation
    if(form.clientId||form.items.length>0){
      setExitConfirm(true);
    }else{
      setModal(null);setPendingNumber(null);
    }
  }
  function doExit(){setExitConfirm(false);setModal(null);setPendingNumber(null);}

  function resetForm(base={}){
    setForm({...EMPTY_FORM,...base});
    setItemForm(EMPTY_ITEM);
    setProdSearch('');setShowProdDrop(false);
    setClientSearch('');setShowClientDrop(false);
  }

  // D1 (OB-006): número gerado pelo banco em create_quote_v2 — reserveORCAsync removido
  async function openNew(presetClient){
    if(presetClient&&presetClient.id){
      resetForm({clientId:presetClient.id,sellerName:user.name});
      setClientSearch(presetClient.name||'');
    }else{
      resetForm();
    }
    setPendingNumber(null);
    setActiveId(null);
    // [BUG-DUP-ORC 2026-05-11] reset idem key a cada NOVO orçamento (não a cada retry)
    saveIdemKeyRef.current=null;
    setModal('new');
  }

  // [WIRE Novo Orçamento] dispatch via event bus do 360º vendas/clientes
  // abre modal pré-preenchido com cliente. Lê window.__znxPendingQuoteClient
  // setado por Clientes.jsx ANTES do dispatch do event.
  useEffect(()=>{
    const pending=window.__znxPendingQuoteClient;
    if(!pending||!pending.id)return;
    window.__znxPendingQuoteClient=null; // consume
    if(isReadOnly){toast('⚠ Perfil financeiro não pode criar orçamentos.');return;}
    openNew(pending);
  },[]); // mount-only — App.jsx desmonta/remonta ao trocar de página

  function openEdit(q){
    // Fix 2.2 — Guard: block editing converted quotes
    if(q.status==='Convertido'){
      toast('Este orçamento já foi convertido em venda '+(q.saleNumber||'')+' e não pode ser editado.');
      return;
    }
    const c=clients.find(x=>x.id===q.clientId);
    // [v224.67 FIX 2026-05-29] Bug família v224.57: openEdit espera camelCase mas quote vem snake_case do banco.
    // Resultado: 254 ORCs danificados (Sedex/Transportadora/PAC/Motoboy) com endereco_entrega vazio porque
    // vendedor reabriu pra editar e salvou sem perceber que form carregou vazio. Fix: fallback p/ snake_case.
    resetForm({clientId:q.clientId,items:[...(q.items||[])],validity:q.validity||'',status:q.status,notes:q.notes||'',sellerName:q.sellerName||user.name,
      frete:q.frete||q.frete_type||'Retirada',
      embalagem:q.embalagem||q.embalagem_tipo||'',
      enderecoEntrega:q.enderecoEntrega||q.endereco_entrega||{nome:'',cep:'',endereco:'',numero:'',complemento:'',bairro:'',cidade:'',estado:''},
      obsTransportadora:q.obsTransportadora||q.obs_transportadora||'',
      trackingCode:q.trackingCode||q.tracking_code||'',
      globalDiscountType:q.globalDiscountType||'',globalDiscountValue:q.globalDiscountValue||0,paymentMethod:q.paymentMethod||'',paymentPixValue:Number(q.paymentPixValue||q.payment_pix_value||0),paymentCashValue:Number(q.paymentCashValue||q.payment_cash_value||0)});
    setClientSearch(c?.name||'');
    setActiveId(q.id);
    setModal('edit');
  }

  function openView(q){
    setActiveId(q.id);
    setModal('view');
  }

  // OB-006: saveNew → grava no relacional via create_quote_v2 (número gerado pelo banco)
  async function saveNew(){
    // [BUG-FIX 20260506] adicionado 'financeiro' — RPC create_quote_v2 sempre permitiu, frontend bloqueava errado
    // [BUG-DUP-ORC 2026-05-11] allowOfflineRoleFallback=true — criação de orçamento é idempotente
    // (idempotency_key gerado abaixo + RPC create_quote_v2 detecta replay). Se rede falhar
    // no role check, NÃO bloqueia ação — banco ainda garante idempotência.
    if(!await znxGuard(['admin','vendedor','financeiro'],{allowOfflineRoleFallback:true}))return;
    if(_inFlightSaveNew){toast('⏳ Salvando orçamento...');return;}
    if(!form.clientId){toast('Selecione um cliente.');return;}
    if(form.items.length===0){toast('Adicione pelo menos um produto.');return;}
    if(!form.paymentMethod){toast('Selecione a forma de pagamento (Pix, Dinheiro ou Misto).');return;}
    // [FEAT 20260504] Validação Misto: soma deve bater com total exato
    if(form.paymentMethod==='Misto'){
      const tot=Number(saleFinalTotal(form)||0);
      const pix=Number(form.paymentPixValue||0);
      const cash=Number(form.paymentCashValue||0);
      if(pix<=0||cash<=0){toast('Pagamento Misto exige Pix > 0 E Dinheiro > 0.');return;}
      const sum=Number((pix+cash).toFixed(2));
      if(Math.abs(sum-Number(tot.toFixed(2)))>=0.01){toast(`Soma Pix(${fmt(pix)}) + Dinheiro(${fmt(cash)}) = ${fmt(sum)} não bate com total ${fmt(tot)}.`);return;}
    }
    if(form.frete==='Retirada'&&!form.embalagem){toast('Selecione a embalagem (Sacola ou Caixa) para retirada presencial.');return;}
    // [BUG-FRETE v212 20260512] Bloqueia salvar Sedex/PAC/Motoboy sem endereço de entrega.
    // Mesmo bug do save de venda — estoque reclamou que VND-0480 era Sedex mas PDF mostrou Retirada
    // porque endereço vazio cai no fallback "Retirada". Bloqueia antes de salvar pra ter dados certos.
    if(['Sedex','PAC','Motoboy'].includes(form.frete)){
      const ee=form.enderecoEntrega||{};
      const cli=clients.find(c=>c.id===form.clientId);
      const fallbackRua=ee.endereco||cli?.address||'';
      const fallbackCidade=ee.cidade||cli?.city||'';
      if(!fallbackRua||!fallbackCidade){
        toast('⚠️ Preencha endereço de entrega (rua + cidade) antes de salvar orçamento '+form.frete+'.','warning');
        if(typeof Sentry!=='undefined')try{Sentry.captureMessage('[ZNX v212] saveNew quote BLOCKED — frete='+form.frete+' sem endereço',{level:'warning',extra:{clientId:form.clientId,clientName:cli?.name,hasEE:!!ee.endereco,hasClientAddr:!!cli?.address}});}catch(_){}
        return;
      }
    }
    // [v224.42 + v224.59 RELAXED 2026-05-28] Orçamento Transportadora SEM data/hora = OK (não bloqueia)
    // VENDA mantém obrigatório em NovaVendaPage.jsx L181. Decisão Jamal: orçamento ainda não é firme.
    // Sentry breadcrumb info-level mantido pra observabilidade · medir % orçamentos sem data/hora.
    if(form.frete==='Transportadora'){
      const ee=form.enderecoEntrega||{};
      if(!ee.delivery_date || !ee.delivery_time){
        // [v224.72] addBreadcrumb em vez de captureMessage · evita ruído ERP-5Y no Sentry
        if(typeof Sentry!=='undefined')try{Sentry.addBreadcrumb({category:'znx-info',message:'[v224.59 ENTREGA] saveNew quote OK sem data/hora — Transportadora orçamento',level:'info',data:{clientId:form.clientId,hasDate:!!ee.delivery_date,hasTime:!!ee.delivery_time}});}catch(_){}
      }
    }
    // NF ativada: campos opcionais — não bloqueia salvar sem preencher
    // Fix 2.4 — STATUS POSSÍVEIS DE ORÇAMENTO:
    // 'Rascunho', 'Aguardando', 'Em Negociação', 'Aprovado' — orçamento aberto, pode ser editado e convertido
    // 'Convertido' — virou venda (BLOQUEADO para edição e reconversão, PERMANENTE)
    // 'Recusado' — recusado (BLOQUEADO para conversão)
    // 'Vencido' — passou da validade (opcional)
    const hasDiscount=form.items.some(it=>(it.discountPct||0)>0)||(form.globalDiscountValue||0)>0;
    const hasPriceBelow=form.items.some(it=>{
      const cat=productsById[it.productId];
      return cat&&Number(it.price)<Number(cat.salePrice);
    });
    if(user.role==='vendedor'&&(hasDiscount||hasPriceBelow)){
      const client=clients.find(c=>c.id===form.clientId);
      const orcSubtotal=form.items.reduce((s,it)=>s+it.qty*(it.price||0),0);
      const orcItemDisc=form.items.reduce((s,it)=>s+it.qty*(it.price*(it.discountPct||0)/100),0);
      const orcAfterItem=orcSubtotal-orcItemDisc;
      const orcGlobalDiscAmt=form.globalDiscountType==='pct'?orcAfterItem*((form.globalDiscountValue||0)/100):form.globalDiscountType==='val'?(form.globalDiscountValue||0):0;
      const totalLiq=orcAfterItem-orcGlobalDiscAmt;
      const totalDesc=orcItemDisc+orcGlobalDiscAmt+(hasPriceBelow?form.items.reduce((s,it)=>{const cat=productsById[it.productId];return s+(cat&&Number(it.price)<Number(cat.salePrice)?it.qty*(Number(cat.salePrice)-Number(it.price)):0);},0):0);
      // [V6 20260506] Motivo do desconto OBRIGATÓRIO — abre DiscountReasonModal antes de criar
      // Admin precisa ver motivo pra decidir aprovar/recusar
      setDiscountReasonModal({
        totalDesconto: totalDesc,
        totalLiquido: totalLiq,
        client,
        formSnapshot: { form: {...form}, hasPriceBelow }
      });
      return;
    }
    const quoteId=genIdUUID();
    // [BUG-DUP-ORC 2026-05-11] gerar idem key SÓ na primeira tentativa.
    // Se retry após erro de rede/role, mantém mesma key → backend faz replay (não duplica).
    if(!saveIdemKeyRef.current){
      saveIdemKeyRef.current = (crypto?.randomUUID?.()) || genIdUUID();
    }
    const idemKey=saveIdemKeyRef.current;
    _inFlightSaveNew=true;
    setIsSavingQuote(true);
    try{
      const result=await createQuoteAtomic({form,quoteId,idempotencyKey:idemKey});
      if(!result.success){
        // [FURO #515 FIX v223.6 2026-05-17 + v224.58 enriched] Reserva firme — toast detalhado com lista ORCs
        if(result.errorCode==='insufficient_stock_for_reserve'){
          let toastMsg = null;
          if(typeof window.ZNX?.lib?.enrichInsufficientStockToast === 'function'){
            toastMsg = await window.ZNX.lib.enrichInsufficientStockToast(result.errorMessage, products, quoteId, sb);
          }
          toast(toastMsg || ('⚠️ '+(result.errorMessage||'Estoque insuficiente — outro vendedor reservou este produto primeiro. Atualize a página.')),'warning');
          if(typeof refreshProducts==='function')await refreshProducts(setProducts);
          if(typeof window.znxCaptureRpcError==='function'){
            window.znxCaptureRpcError('[ZNX] saveNew insufficient_stock_for_reserve',result.errorCode,{quoteId,errorMessage:result.errorMessage,idemKey,enriched:!!toastMsg});
          }
          return;
        }
        // [v223.37 BUG-MONA-UX] tenta parsear pra toast amigável c/ nome do produto (invalid_item/insufficient_stock)
        const friendly=window.ZNX?.lib?.parseQuoteError?.(result.errorMessage,products);
        toast(friendly||('❌ '+(mapErrorToUX(result.errorCode,result.errorMessage))),friendly?'warning':'error');
        // [ONDA-S B 2026-05-13] znxCaptureRpcError classifica expected vs unexpected
        if(typeof window.znxCaptureRpcError==='function'){
          window.znxCaptureRpcError('[ZNX] saveNew RPC error',result.errorCode,{quoteId,errorMessage:result.errorMessage,idemKey,friendly_match:!!friendly});
        } else if(typeof Sentry!=='undefined'){
          Sentry.captureException(new Error('[ZNX] saveNew RPC error: '+result.errorCode),{extra:{quoteId,errorMessage:result.errorMessage,idemKey,friendly_match:!!friendly}});
        }
        return;
      }
      // [BUG-DUP-ORC] sucesso confirmado → reset pra próxima criação
      saveIdemKeyRef.current=null;
      const cliObj=clients.find(x=>x.id===form.clientId);
      // [BUG-FIX 20260504] Usa updatedAt RETORNADO PELO BANCO (não gera local) — assim
      // próxima edição não dispara optimistic lock por divergência de timestamp.
      // status também vem do banco ('Aberto') — antes vinha do form e divergia do banco.
      const dbUpdatedAt=result.updatedAt||new Date().toISOString();
      const dbStatus=result.status||'Aberto';
      // [FEAT 20260504] Normaliza splits Pix/Dinheiro/Misto pra ficar consistente com banco
      const totalQ=Number(result.total||saleFinalTotal(form)||0);
      let pixV=Number(form.paymentPixValue||0), cashV=Number(form.paymentCashValue||0);
      if(form.paymentMethod==='Pix'){pixV=totalQ; cashV=0;}
      else if(form.paymentMethod==='Dinheiro Vivo'){cashV=totalQ; pixV=0;}
      else if(form.paymentMethod!=='Misto'){pixV=0; cashV=0;}
      const nq={...form,id:result.quoteId||quoteId,number:result.quoteNumber,date:today(),status:dbStatus,createdAt:dbUpdatedAt,updatedAt:dbUpdatedAt,clientId:form.clientId,clientName:cliObj?.name||form.clientName||'',sellerName:form.sellerName||user?.name||'Admin',paymentPixValue:pixV,paymentCashValue:cashV};
      setQuotes(prev=>[...prev,nq]);
      // [REFACTOR 20260422] ORC não muta stock físico — reserva calculada em runtime por getAvailableStockForSeller
      setModal(null);setPendingNumber(null);
      toast(result.replayConfirmed ? '✅ Orçamento '+result.quoteNumber+' salvo · servidor demorou mas concluiu!' : '✅ Orçamento '+result.quoteNumber+' salvo!');
    }catch(e){
      console.error('[ZNX] saveNew error:',e);
      // [v223.37 BUG-MONA-UX] tenta parsear pra toast amigável c/ nome do produto
      const friendly=window.ZNX?.lib?.parseQuoteError?.(e,products);
      if(friendly){toast(friendly,'warning');}else{toast('❌ Erro inesperado ao salvar orçamento. Recarregue a página.');}
      if(typeof Sentry!=='undefined')Sentry.captureException(e,{extra:{quoteId,friendly_match:!!friendly}});
    }finally{
      _inFlightSaveNew=false;
      setIsSavingQuote(false);
    }
  }

  // OB-006: saveEdit → grava no relacional via updateQuoteAtomic (legacy-safe)
  async function saveEdit(){
    // [BUG-FIX 20260506] adicionado 'financeiro' — bug Shaimaa: ORC-0190 com credito R$1449,96 nao editava. Backend RPC update_quote_v2 sempre permitiu financeiro.
    if(!await znxGuard(['admin','vendedor','financeiro']))return;
    if(_inFlightSaveEdit){toast('⏳ Salvando alterações...');return;}
    if(!form.clientId){toast('Selecione um cliente.');return;}
    if(form.items.length===0){toast('Adicione pelo menos um produto.');return;}
    if(!form.paymentMethod){toast('Selecione a forma de pagamento (Pix, Dinheiro ou Misto).');return;}
    // [FEAT 20260504] Validação Misto: soma deve bater com total exato
    if(form.paymentMethod==='Misto'){
      const tot=Number(saleFinalTotal(form)||0);
      const pix=Number(form.paymentPixValue||0);
      const cash=Number(form.paymentCashValue||0);
      if(pix<=0||cash<=0){toast('Pagamento Misto exige Pix > 0 E Dinheiro > 0.');return;}
      const sum=Number((pix+cash).toFixed(2));
      if(Math.abs(sum-Number(tot.toFixed(2)))>=0.01){toast(`Soma Pix(${fmt(pix)}) + Dinheiro(${fmt(cash)}) = ${fmt(sum)} não bate com total ${fmt(tot)}.`);return;}
    }
    if(form.frete==='Retirada'&&!form.embalagem){toast('Selecione a embalagem (Sacola ou Caixa) para retirada presencial.');return;}
    // [BUG-FRETE v212 20260512] Bloqueia EDITAR orçamento Sedex/PAC/Motoboy sem endereço.
    // Vendedora pode ter criado ORC antes do v212 e agora editar pra trocar pra Sedex sem endereço.
    if(['Sedex','PAC','Motoboy'].includes(form.frete)){
      const ee=form.enderecoEntrega||{};
      const cli=clients.find(c=>c.id===form.clientId);
      const fallbackRua=ee.endereco||cli?.address||'';
      const fallbackCidade=ee.cidade||cli?.city||'';
      if(!fallbackRua||!fallbackCidade){
        toast('⚠️ Preencha endereço de entrega (rua + cidade) antes de salvar edição '+form.frete+'.','warning');
        if(typeof Sentry!=='undefined')try{Sentry.captureMessage('[ZNX v212] saveEdit quote BLOCKED — frete='+form.frete+' sem endereço',{level:'warning',extra:{quoteId:activeId,clientId:form.clientId,clientName:cli?.name}});}catch(_){}
        return;
      }
    }
    // [v224.42 + v224.59 RELAXED 2026-05-28] Orçamento Transportadora SEM data/hora = OK (não bloqueia)
    // VENDA mantém obrigatório em NovaVendaPage.jsx L181. Sentry info pra observabilidade.
    if(form.frete==='Transportadora'){
      const ee=form.enderecoEntrega||{};
      if(!ee.delivery_date || !ee.delivery_time){
        // [v224.72] addBreadcrumb em vez de captureMessage · evita ruído ERP-5W no Sentry
        if(typeof Sentry!=='undefined')try{Sentry.addBreadcrumb({category:'znx-info',message:'[v224.59 ENTREGA] saveEdit quote OK sem data/hora — Transportadora orçamento',level:'info',data:{quoteId:activeId,clientId:form.clientId}});}catch(_){}
      }
    }
    // [BUG-FIX 20260504] Vendedor edita orçamento e aplica desconto direto sem aprovação.
    // saveNew tinha o check, saveEdit não → vendedora burlava criando ORC sem desconto e
    // depois editando pra aplicar o desconto. Replicar mesmo flow do saveNew aqui.
    const hasDiscountE=form.items.some(it=>(it.discountPct||0)>0)||(form.globalDiscountValue||0)>0;
    const hasPriceBelowE=form.items.some(it=>{
      const cat=productsById[it.productId];
      return cat&&Number(it.price)<Number(cat.salePrice);
    });
    if(user.role==='vendedor'&&(hasDiscountE||hasPriceBelowE)){
      // [BUG-FIX 20260507] saveEdit chamava createDiscountRequestAtomic SEM request_reason → RPC sempre falhava com 'Motivo é obrigatório' (regra V6 2026-05-06).
      // Agora abre DiscountReasonModal igual saveNew faz, e submitDiscountWithReason ramifica em isEdit pra ATUALIZAR o orçamento existente (não criar novo).
      const clientE=clients.find(c=>c.id===form.clientId);
      const sub=form.items.reduce((s,it)=>s+it.qty*(it.price||0),0);
      const itemDisc=form.items.reduce((s,it)=>s+it.qty*(it.price*(it.discountPct||0)/100),0);
      const afterItem=sub-itemDisc;
      const globalDiscAmt=form.globalDiscountType==='pct'?afterItem*((form.globalDiscountValue||0)/100):form.globalDiscountType==='val'?(form.globalDiscountValue||0):0;
      const totalLiqE=afterItem-globalDiscAmt;
      const totalDescE=itemDisc+globalDiscAmt+(hasPriceBelowE?form.items.reduce((s,it)=>{const cat=productsById[it.productId];return s+(cat&&Number(it.price)<Number(cat.salePrice)?it.qty*(Number(cat.salePrice)-Number(it.price)):0);},0):0);
      setDiscountReasonModal({
        totalDesconto: totalDescE,
        totalLiquido: totalLiqE,
        client: clientE,
        formSnapshot: { form: {...form}, hasPriceBelow: hasPriceBelowE, isEdit: true, quoteId: activeId }
      });
      return;
    }
    _inFlightSaveEdit=true;
    setIsSavingQuote(true);
    const currentQuote=quotes.find(q=>q.id===activeId);
    // [BUG-FIX 20260504] Bloqueia edição de status terminal (Convertido/Cancelado/Recusado)
    // Defesa em profundidade — botão deveria estar escondido (ver isTerminalQuoteStatus),
    // mas reforço aqui caso usuário consiga abrir modal por outro caminho.
    if(currentQuote && isTerminalQuoteStatus(currentQuote.status)){
      toast('⛔ Não é possível editar orçamento '+currentQuote.status.toLowerCase()+'.');
      _inFlightSaveEdit=false;
      setIsSavingQuote(false);
      return;
    }
    try{
      // [ONDA-A #9 2026-05-11] idem key estável saveEdit — lazy init, mantém durante retry.
      // TODO: updateQuoteAtomic ainda não aceita idempotencyKey — passar quando RPC for atualizada.
      if(!editIdemKeyRef.current){
        editIdemKeyRef.current=(crypto?.randomUUID?.())||genIdUUID();
      }
      let result=await updateQuoteAtomic({quoteId:activeId,form,expectedUpdatedAt:currentQuote?.updatedAt,idempotencyKey:editIdemKeyRef.current});
      // [ONDA-E E3 2026-05-11] Auto-retry silencioso 1x se conflito otimista.
      // Frequente: vendedora abriu modal, admin atualizou status (não os dados que ela edita), JSONB local stale.
      // Refetch fresh, se NADA dos campos editáveis mudou, retry silencioso. Senão abre modal amigável.
      if(!result.success && result.errorCode==='client_modified_concurrently'){
        try{
          const{data:fresh}=await sb.from('quotes').select('*').eq('id',activeId).single();
          if(fresh){
            result=await updateQuoteAtomic({quoteId:activeId,form,expectedUpdatedAt:fresh.updated_at,idempotencyKey:editIdemKeyRef.current});
            if(!result.success && result.errorCode==='client_modified_concurrently'){
              // 2º conflito → modal amigável
              setConcurrentEditModal({quoteId:activeId,freshQuote:fresh,attemptedForm:{...form}});
              // [ONDA-S B 2026-05-13] expected error → level=info (regra_falha_silenciosa_proibida: toast+Sentry mas classificado correto)
              if(typeof window.znxCaptureRpcError==='function'){
                window.znxCaptureRpcError('[ZNX] saveEdit RPC error',result.errorCode,{quoteId:activeId,errorMessage:result.errorMessage,idemKey:editIdemKeyRef.current,retried:true});
              } else if(typeof Sentry!=='undefined'){
                Sentry.captureException(new Error('[ZNX] saveEdit RPC error: '+result.errorCode),{extra:{quoteId:activeId,errorMessage:result.errorMessage,idemKey:editIdemKeyRef.current,retried:true}});
              }
              return;
            }
          }
        }catch(retryErr){
          znxLogWarn('[ZNX] saveEdit retry fetch falhou', retryErr);
        }
      }
      if(!result.success){
        // [FURO #515 FIX v223.6 2026-05-17 + v224.58 enriched] Reserva firme — toast detalhado com lista ORCs
        if(result.errorCode==='insufficient_stock_for_reserve'){
          let toastMsg = null;
          if(typeof window.ZNX?.lib?.enrichInsufficientStockToast === 'function'){
            toastMsg = await window.ZNX.lib.enrichInsufficientStockToast(result.errorMessage, products, activeId, sb);
          }
          toast(toastMsg || ('⚠️ '+(result.errorMessage||'Estoque insuficiente — outro vendedor reservou este produto primeiro. Atualize a página.')),'warning');
          if(typeof refreshProducts==='function')await refreshProducts(setProducts);
          if(typeof window.znxCaptureRpcError==='function'){
            window.znxCaptureRpcError('[ZNX] saveEdit insufficient_stock_for_reserve',result.errorCode,{quoteId:activeId,errorMessage:result.errorMessage,idemKey:editIdemKeyRef.current,enriched:!!toastMsg});
          }
          return;
        }
        // [v223.37 BUG-MONA-UX] tenta parsear pra toast amigável c/ nome do produto (invalid_item/insufficient_stock)
        const friendly=window.ZNX?.lib?.parseQuoteError?.(result.errorMessage,products);
        toast(friendly||('❌ '+(mapErrorToUX(result.errorCode,result.errorMessage))),friendly?'warning':'error');
        // [ONDA-S B 2026-05-13] expected errors (quote_terminal_status, forbidden, etc) → level=info no Sentry
        if(typeof window.znxCaptureRpcError==='function'){
          window.znxCaptureRpcError('[ZNX] saveEdit RPC error',result.errorCode,{quoteId:activeId,errorMessage:result.errorMessage,idemKey:editIdemKeyRef.current,friendly_match:!!friendly});
        } else if(typeof Sentry!=='undefined'){
          Sentry.captureException(new Error('[ZNX] saveEdit RPC error: '+result.errorCode),{extra:{quoteId:activeId,errorMessage:result.errorMessage,idemKey:editIdemKeyRef.current,friendly_match:!!friendly}});
        }
        return;
      }
      // [ONDA-A #9] sucesso confirmado → reset idem key pra próxima edição
      editIdemKeyRef.current=null;
      // [v224.115 2026-06-03] replay confirmou (db_timeout mascarou sucesso real) → toast verde explícito
      if(result.replayConfirmed){
        toast('✅ Salvo · o servidor demorou mas concluiu (orçamento confirmado).','success');
      }
      // legacy=true: quote pré-Fase B não existe no relacional → JSONB-only silencioso
      // [REFACTOR 20260422] ORC edit não ajusta stock físico — reserva recalculada em runtime
      // [BUG-FIX 20260504] Usa updatedAt RETORNADO PELO BANCO (não gera local) — assim
      // próxima edição não dispara optimistic lock por divergência de timestamp.
      const dbUpdatedAt=result.updatedAt||new Date().toISOString();
      // [FEAT 20260504] Normaliza splits Pix/Dinheiro/Misto pra ficar consistente com banco
      const totalE=Number(saleFinalTotal(form)||0);
      let pixE=Number(form.paymentPixValue||0), cashE=Number(form.paymentCashValue||0);
      if(form.paymentMethod==='Pix'){pixE=totalE; cashE=0;}
      else if(form.paymentMethod==='Dinheiro Vivo'){cashE=totalE; pixE=0;}
      else if(form.paymentMethod!=='Misto'){pixE=0; cashE=0;}
      setQuotes(prev=>prev.map(q=>q.id===activeId?{...q,...form,clientId:form.clientId,paymentPixValue:pixE,paymentCashValue:cashE,updatedAt:dbUpdatedAt}:q));
      setModal(null);
    }catch(e){
      console.error('[ZNX] saveEdit error:',e);
      // [v223.37 BUG-MONA-UX] tenta parsear pra toast amigável c/ nome do produto
      const friendly=window.ZNX?.lib?.parseQuoteError?.(e,products);
      if(friendly){toast(friendly,'warning');}else{toast('❌ Erro inesperado ao salvar alterações. Recarregue a página.');}
      if(typeof Sentry!=='undefined')Sentry.captureException(e,{extra:{quoteId:activeId,friendly_match:!!friendly}});
    }finally{
      _inFlightSaveEdit=false;
      setIsSavingQuote(false);
    }
  }

  async function updateNote(id,notes){
    if(!await znxGuard(null))return;
    try {
      // [BUG-FIX 20260504] persiste notes no banco — antes só setQuotes local
      const{error}=await sb.from('quotes').update({notes,updated_at:new Date().toISOString()}).eq('id',id);
      if(error){
        toast('❌ Erro ao salvar observação: '+error.message);
        if(typeof Sentry!=='undefined')Sentry.captureException(error,{extra:{context:'updateNote_orc',quoteId:id}});
        return;
      }
      setQuotes(prev=>prev.map(q=>q.id===id?{...q,notes}:q));
    } catch(e) {
      if(typeof Sentry!=='undefined')Sentry.captureException(e,{extra:{context:'updateNote_orc',quoteId:id}});
    }
  }
  async function updateStatus(id,status){
    // [SEC-001] Server-side role check — cancel/refuse quote (admin only)
    const CANCEL_STATUSES=['Cancelado','cancelado','Cancelada','cancelada','Recusado','recusado'];
    if(CANCEL_STATUSES.includes(status)){if(!await znxGuard(['admin']))return;}
    // [REFACTOR 20260422] ORC cancel/recusa não restaura stock — ORC nunca decrementou
    try {
      // [BUG-FIX 20260504] persiste status no banco — antes só setQuotes local, status voltava após F5
      const{error}=await sb.from('quotes').update({status,updated_at:new Date().toISOString()}).eq('id',id);
      if(error){
        toast('❌ Erro ao alterar status: '+error.message);
        if(typeof Sentry!=='undefined')Sentry.captureException(error,{extra:{context:'updateStatus_orc',quoteId:id,status}});
        return;
      }
      setQuotes(prev=>prev.map(q=>q.id===id?{...q,status}:q));
    } catch(e) {
      if(typeof Sentry!=='undefined')Sentry.captureException(e,{extra:{context:'updateStatus_orc',quoteId:id,status}});
    }
  }

  // [V6 20260506] Submete pedido de desconto após vendedora justificar motivo
  // [BUG-FIX 20260507] Agora ramifica em isEdit:
  //   - isEdit=false: cria orçamento NOVO (saveNew com desconto)
  //   - isEdit=true:  ATUALIZA o mesmo orçamento (saveEdit com desconto) — sem criar novo número
  async function submitDiscountWithReason(reason){
    if(!discountReasonModal) return;
    const { totalDesconto, totalLiquido, client, formSnapshot } = discountReasonModal;
    const { form: f, hasPriceBelow, isEdit, quoteId: editQuoteId } = formSnapshot;
    const drId=genIdUUID();
    if(isEdit){
      // === EDIÇÃO: atualiza o mesmo orçamento + cria DR vinculada ao MESMO quote_id ===
      _inFlightSaveEdit=true;
      try{
        const currentQuote=quotes.find(q=>q.id===editQuoteId);
        if(currentQuote && isTerminalQuoteStatus(currentQuote.status)){
          toast('⛔ Não é possível editar orçamento '+currentQuote.status.toLowerCase()+'.');
          return;
        }
        // 1. Atualiza o orçamento existente com os novos preços/descontos
        // [NOTA 20260507] update_quote_v2 NÃO atualiza coluna status — fazemos UPDATE direto após
        const updateResult=await updateQuoteAtomic({quoteId:editQuoteId,form:f,expectedUpdatedAt:currentQuote?.updatedAt});
        if(!updateResult.success){
          toast('❌ '+(mapErrorToUX(updateResult.errorCode,updateResult.errorMessage)));
          return;
        }
        // 1b. UPDATE direto pra status='Aguardando desconto' (RPC não atualiza esta coluna)
        try{
          const{error:statusErr}=await sb.from('quotes').update({status:'Aguardando desconto',updated_at:new Date().toISOString()}).eq('id',editQuoteId);
          if(statusErr){
            console.error('[ZNX] saveEdit isEdit status update failed:',statusErr);
            // Continua mesmo assim — DR ainda será criada e admin verá no Approval Hub
          }
        }catch(e){console.error('[ZNX] saveEdit status update error:',e);}
        // 2. Cria discount_request com motivo obrigatório, vinculada ao MESMO orçamento
        const drResult=await createDiscountRequestAtomic({
          id:drId,
          requested_by:user.name,
          request_reason:reason,
          client_id:f.clientId||null,
          client_name:client?.name||'—',
          quote_id:editQuoteId,
          is_orcamento:true,
          is_edit:true,
          has_price_below:hasPriceBelow,
          total_liquido:totalLiquido,
          total_desconto:totalDesconto,
          global_discount_type:f.globalDiscountType||null,
          global_discount_value:Number(f.globalDiscountValue||0),
          form_data:{...f,clientId:f.clientId,quoteId:editQuoteId},
          items:f.items
        });
        if(!drResult.success){
          toast('❌ Erro ao enviar pedido de desconto: '+(drResult.errorMessage||''));
          return;
        }
        // 3. Atualiza store local — mantém o MESMO orçamento (id e número intactos)
        const dbUpdatedAt=updateResult.updatedAt||new Date().toISOString();
        const totalE=Number(saleFinalTotal(f)||0);
        let pixE=Number(f.paymentPixValue||0), cashE=Number(f.paymentCashValue||0);
        if(f.paymentMethod==='Pix'){pixE=totalE; cashE=0;}
        else if(f.paymentMethod==='Dinheiro Vivo'){cashE=totalE; pixE=0;}
        else if(f.paymentMethod!=='Misto'){pixE=0; cashE=0;}
        setQuotes(prev=>prev.map(q=>q.id===editQuoteId?{...q,...f,clientId:f.clientId,status:'Aguardando desconto',paymentPixValue:pixE,paymentCashValue:cashE,updatedAt:dbUpdatedAt}:q));
        setDiscountReasonModal(null);
        setModal(null);
        toast(hasPriceBelow?'⚠️ Orçamento atualizado. Aguardando aprovação do admin (preço abaixo do catálogo).':'✅ Orçamento atualizado. Aguardando aprovação do admin.');
      }finally{_inFlightSaveEdit=false;}
      return;
    }
    // === CRIAÇÃO: cria orçamento NOVO ===
    const newQuoteId=genIdUUID();
    _inFlightSaveNew=true;
    try{
      // [ONDA-A #3 20260511] Idempotency key estável pro path "Aguardando desconto"
      // — reaproveita saveIdemKeyRef se já criado, senão gera novo. Replay seguro.
      if(!saveIdemKeyRef.current){
        saveIdemKeyRef.current=(crypto?.randomUUID?.())||genIdUUID();
      }
      const idemKeyAguardando=saveIdemKeyRef.current;
      // 1. Cria orçamento já com desconto + status 'Aguardando desconto'
      const formAguardando={...f,status:'Aguardando desconto'};
      const quoteResult=await createQuoteAtomic({form:formAguardando,quoteId:newQuoteId,idempotencyKey:idemKeyAguardando});
      if(!quoteResult.success){
        toast('❌ '+(mapErrorToUX(quoteResult.errorCode,quoteResult.errorMessage)));
        return;
      }
      // 2. Cria discount_request com motivo obrigatório
      const drResult=await createDiscountRequestAtomic({
        id:drId,
        requested_by:user.name,
        request_reason:reason,
        client_id:f.clientId||null,
        client_name:client?.name||'—',
        quote_id:quoteResult.quoteId||newQuoteId,
        is_orcamento:true,
        is_edit:false,
        has_price_below:hasPriceBelow,
        total_liquido:totalLiquido,
        total_desconto:totalDesconto,
        global_discount_type:f.globalDiscountType||null,
        global_discount_value:Number(f.globalDiscountValue||0),
        form_data:{...f,clientId:f.clientId,quoteId:quoteResult.quoteId},
        items:f.items
      });
      if(!drResult.success){
        toast('❌ Erro ao enviar pedido de desconto: '+(drResult.errorMessage||''));
        return;
      }
      const cliObj=clients.find(x=>x.id===f.clientId);
      const dbUpdatedAt=quoteResult.updatedAt||new Date().toISOString();
      const nq={...f,id:quoteResult.quoteId||newQuoteId,number:quoteResult.quoteNumber,date:today(),status:'Aguardando desconto',createdAt:dbUpdatedAt,updatedAt:dbUpdatedAt,clientId:f.clientId,clientName:cliObj?.name||f.clientName||'',sellerName:f.sellerName||user?.name||''};
      setQuotes(prev=>[...prev,nq]);
      // [ONDA-A #3 20260511] Reset idem key após sucesso — próxima criação gera UUID novo
      saveIdemKeyRef.current=null;
      setDiscountReasonModal(null);
      setModal(null);
      toast(hasPriceBelow?'⚠️ Orçamento '+quoteResult.quoteNumber+' criado. Aguardando aprovação do admin (preço abaixo do catálogo).':'✅ Orçamento '+quoteResult.quoteNumber+' criado. Aguardando aprovação do admin.');
    }finally{_inFlightSaveNew=false;}
  }

  // OB-006: doDelete → cancela no relacional via cancel_quote_v2 (idempotent para legacy)
  // [V2 20260506] reason agora obrigatório — abre CancelQuoteModal antes de chamar RPC
  async function doDelete(id, reason){
    if(!await znxGuard(['admin','vendedor']))return;
    if(_inFlightDoDelete){toast('⏳ Cancelando orçamento...');return;}
    if(!reason||!String(reason).trim()){
      toast('⚠️ Motivo do cancelamento é obrigatório.');
      return;
    }
    _inFlightDoDelete=true;
    try{
      const result=await cancelQuoteAtomic({quoteId:id, reason});
      const IGNORABLE=['quote_not_found','quote_already_cancelled'];
      if(!result.success&&!IGNORABLE.includes(result.errorCode)){
        toast('❌ '+(mapErrorToUX(result.errorCode,result.errorMessage)));
        // [ONDA-S B 2026-05-13] znxCaptureRpcError classifica expected vs unexpected
        if(typeof window.znxCaptureRpcError==='function'){
          window.znxCaptureRpcError('[ZNX] doDelete RPC error',result.errorCode,{quoteId:id,errorMessage:result.errorMessage});
        } else if(typeof Sentry!=='undefined'){
          Sentry.captureException(new Error('[ZNX] doDelete RPC error: '+result.errorCode),{extra:{quoteId:id,errorMessage:result.errorMessage}});
        }
        return;
      }
      setQuotes(prev=>prev.map(q=>q.id===id?{...q,status:'Cancelado',cancelReason:reason,canceledAt:new Date().toISOString(),canceledBy:user?.id||null,deletedAt:new Date().toISOString(),deletedBy:user?.name||''}:q));
      setDeleteConfirm(null);
      setCancelMotivoModal(null);
      if(modal&&activeId===id)setModal(null);
      toast('✅ Orçamento cancelado. Motivo registrado.');
    }catch(e){
      console.error('[ZNX] doDelete error:',e);
      if(typeof Sentry!=='undefined')Sentry.captureException(e,{extra:{quoteId:id}});
      toast('❌ Erro inesperado ao cancelar orçamento. Recarregue a página.');
    }finally{
      _inFlightDoDelete=false;
    }
  }

  function shareWhatsApp(q){
    const c=clients.find(x=>x.id===q.clientId);
    const lines=[
      `*ORÇAMENTO ${q.number}*`,
      `📅 Data: ${fmtDate(q.date)}`,
      `👤 Cliente: ${c?.name||'—'}`,
      ``,
      `*ITENS:*`,
      ...(q.items||[]).map((it,i)=>`${i+1}. ${it.name||it.productName||it.product_name||'Produto'} — ${it.qty}x ${fmt(itemNet(it))} = *${fmt(it.qty*itemNet(it))}*`),
      ``,
      `💰 *TOTAL: ${fmt(saleFinalTotal(q))}*`,
      q.validity?`📆 Válido até: ${fmtDate(q.validity)}`:'',
      q.notes?`📝 Obs: ${q.notes}`:'',
      ``,
      `_Obrigado pela preferência! 🙏_`
    ].filter(l=>l!==undefined&&l!==null);
    const text=lines.filter(l=>l!=='').join('\n');
    const phone=c?.whatsapp||c?.phone||'';
    const cleaned=phone.replace(/\D/g,'');
    const waNum=cleaned?(cleaned.length<=11?`55${cleaned}`:cleaned):'';
    const url=waNum?`https://wa.me/${waNum}?text=${encodeURIComponent(text)}`:`https://wa.me/?text=${encodeURIComponent(text)}`;
    window.open(url,'_blank');
  }

  // [ONDA-PDF-N3 v206 20260512] gerarOrcamentoPDF agora delega pra pdfShared.gerarDocumentoPDF
  // Mesma engine que gerarFaturaPDF. Visual idêntico exceto label header (ORÇAMENTO vs FATURA),
  // selo status (Aberto/Aprovado/Recusado/Aguardando vs Pago/Pendente), e label total
  // ("TOTAL DO ORÇAMENTO" vs "TOTAL DA FATURA"). Validade do orçamento aparece no card direito.
  // Antes: PDF v95 simplão (header 28mm, sem logo monograma, sem selo, dual box fixo,
  // tabela sem truncByWidth, sem faixa embalagem). Agora: PDF v138 premium idêntico ao venda.
  async function gerarOrcamentoPDF(quote, clients){
    if(typeof window.pdfShared?.gerarDocumentoPDF !== 'function'){
      toast('❌ pdfShared não carregado. Recarregue a página (Ctrl+Shift+R).','error');
      if(typeof Sentry!=='undefined') try{Sentry.captureMessage('[ZNX v206] pdfShared não disponível em gerarOrcamentoPDF',{level:'error'});}catch(_){}
      return;
    }
    // [BUG-FRETE v212 20260512] Refetch FRESH do banco antes de gerar PDF — mata cache stale.
    let freshQuote = quote;
    try{
      if(quote?.id && typeof sb!=='undefined'){
        const{data,error}=await sb.from('quotes').select('*').eq('id',quote.id).maybeSingle();
        if(!error && data) freshQuote = {...quote, ...data,
          // [v224.68 FIX 20260529] enrich camelCase createdAt/updatedAt pra pdfShared fmtH
          createdAt: data.created_at || quote.createdAt,
          updatedAt: data.updated_at || quote.updatedAt,
          items: (quote.items && quote.items.length ? quote.items : (data.items||[]))
        };
      }
    }catch(e){
      if(typeof Sentry!=='undefined') try{Sentry.captureMessage('[ZNX v212] refetch quote falhou (usando cache)',{level:'warning',extra:{message:e?.message,quoteId:quote?.id}});}catch(_){}
    }
    return window.pdfShared.gerarDocumentoPDF({kind:'ORÇAMENTO', data:freshQuote, clients:clients});
  }


  async function convert(q){
    // Verificar no estado atual (não na cópia stale)
    const current=quotes.find(x=>x.id===q.id);
    if(!current||current.status==='Convertido'){toast('⚠️ Este orçamento já foi convertido em venda.');setModal(null);return;}
    // [BUG-FIX 20260506] Refresh fresh do banco antes de validar conflitos.
    // Evita race com applyRows stale que fazia modal mostrar UUID + estoque 0
    // mesmo quando produto existia no banco com stock real (bug financeiro 2026-05-06 manhã).
    if(typeof refreshProducts==='function')await refreshProducts(setProducts);
    // [BUG-FIX UX 20260505] Lookup de nome para produtos sumidos do state local (active=false).
    // refreshProducts filtra active=true, então it.productId pode não estar em `products`.
    // Buscamos esses faltantes direto no banco SEM filtro — só pra exibir nome no modal de erro.
    // [BUG-FIX 20260506] JSONB items usam productName/product_name (não name) após trigger sync 2026-05-04
    const missingIds=q.items
      .filter(it=>!it.name&&!it.productName&&!it.product_name&&!productsById[it.productId])
      .map(it=>it.productId);
    let inactiveLookup={};
    if(missingIds.length>0){
      const{data:extra}=await sb.from('products').select('id,name,volume,is_decant,category,categoria,stock').in('id',missingIds);
      if(Array.isArray(extra))for(const x of extra)inactiveLookup[x.id]=x;
    }
    const conflicts=[];
    // [STOCK-RESERVE] usa getAvailableStockForSeller p/ refletir reservas de vendedores
    const _ctx=(typeof getUserContext==='function')?getUserContext(user):{role:user?.role,sellerName:user?.name};
    for(const it of q.items){
      const p=productsById[it.productId]||inactiveLookup[it.productId];
      const si=(typeof getAvailableStockForSeller==='function')
        ?getAvailableStockForSeller(products,quotes,it.productId,null,_ctx)
        :{available:p?.stock||0,reservedByOthers:0};
      if(it.qty>si.available){
        const baseName=it.name||it.productName||it.product_name||p?.name||p?.product_name||`Produto #${it.productId}`;
        const vol=p?.volume||(()=>{const m=baseName.match(/(\d+(?:\.\d+)?)\s*ml/i);return m?m[1]+'ml':null;})();
        const nm=baseName+(vol&&!baseName.toLowerCase().includes(vol.toLowerCase())?` ${vol}`:'');
        // [BUG-FIX 20260504] is_decant=true e fonte canonica do banco. Pula stock check pra TODOS
        // os decantes (sao fracionados sob demanda, stock sempre 0). Fallback string-based mantido
        // pra compat com produtos antigos sem flag setada.
        const isDecantItem=p?.is_decant===true||p?.category==='Decants'||p?.categoria==='Decants'||((p?.volume||'').toLowerCase()==='5ml')||baseName.toLowerCase().includes('decant');
        if(isDecantItem) continue;
        conflicts.push({productId:it.productId,name:nm,solicitado:it.qty,estoque:p?.stock||0,reservado:si.reservedByOthers,disponivel:si.available,reservations:Array.isArray(si.reservations)?si.reservations:[]});
      }
    }
    if(conflicts.length>0){setConflict(conflicts);setPendingConvert(q);return;}
    doConvert(q).catch(e=>console.error('[ZNX] doConvert error:',e));
  }

  async function doConvert(q){
    // [SEC-001] Server-side role check (3-layer: UI + frontend + backend RPC)
    if(!await znxGuard(['admin','financeiro']))return;
    // [ONDA1-A 2026-05-11] regra_loading_state_obrigatorio — ref + state pra disabled UI.
    // Inflight guard — evita double-submit entre renders concorrentes (caso Mona ORC-0343).
    if(convertInflightRef.current||_inFlightConvert){toast('⏳ Conversão em andamento...');return;}
    // Fix 2.2 — Guard: bloqueia reconversão de orçamento já convertido
    if(q.status==='Convertido'||q.saleNumber){
      toast('Este orçamento já foi convertido em venda '+(q.saleNumber||'')+'. Não é possível converter novamente.');
      return;
    }
    // Trava definitiva: re-verifica no estado real antes de criar a venda
    const current=quotes.find(x=>x.id===q.id);
    if(!current||current.status==='Convertido'){toast('Este orçamento já foi convertido em venda '+(current?.saleNumber||'')+'. Não é possível converter novamente.');setConflict(null);setPendingConvert(null);setModal(null);return;}
    convertInflightRef.current=true;
    setConvertingQuoteId(q.id);
    _inFlightConvert=true;
    try{
      const FRETE_VALS={Retirada:0,PAC:45,Sedex:85,Transportadora:120,Motoboy:30};
      const freteV=FRETE_VALS[q.frete]||0;
      // [BUG-FIX 20260504] Conversão sempre marca como 'Pago' — orçamento aprovado virou venda
      // confirmada. Antes: respeitava q.paymentStatus do orçamento (que podia ser Pendente).
      const paymentStatus='Pago';
      // [BUG-FIX 20260507] Convert orçamento Misto → venda: incluir paymentPixValue/paymentCashValue.
      // Antes: form só tinha paymentMethod='Misto' SEM os valores → backend RPC bate em
      // 'payment_split_invalid: pix>0 E cash>0' → erro 'Pagamento Misto exige...'
      // Reportado por Shaimaa convertendo orçamento Misto. Valores estão no quote.
      const qPix = Number(q.paymentPixValue||q.payment_pix_value||0);
      const qCash = Number(q.paymentCashValue||q.payment_cash_value||0);
      // [BUG-FIX 20260507] embalagemTipo + freteType estavam perdidos na conversão.
      // q.embalagem é NUMERIC (R$), q.embalagemTipo é TEXT (Sacola/Caixa). RPC usa
      // typeof form.embalagem === 'string' como fallback, mas como q.embalagem é numeric,
      // sem passar embalagemTipo explícito o PDF da venda mostrava "Nao especificada".
      // Reportado pelo Jamal: "quando da baixa ele não sai na nota".
      const qFreteType = q.freteType||q.frete_type||(typeof q.frete==='string'?q.frete:null);
      const qEmbType = q.embalagemTipo||q.embalagem_tipo||(typeof q.embalagem==='string'?q.embalagem:null);
      // Monta form no formato que createSaleAtomic espera
      const form={
        quoteId:q.id,clientId:q.clientId,date:today(),status:'Aberto',
        sellerName:q.sellerName||user?.name||'Admin',canal:q.canal||null,
        paymentMethod:q.paymentMethod||null,
        paymentPixValue: qPix,
        paymentCashValue: qCash,
        globalDiscountType:q.globalDiscountType||'',globalDiscountValue:q.globalDiscountValue||0,
        frete: qFreteType || q.frete || 'Retirada',
        freteType: qFreteType,
        embalagem: qEmbType || q.embalagem || 0,
        embalagemTipo: qEmbType,
        nfEnabled:q.nfEnabled||false,notaFiscal:q.notaFiscal||{},
        // [F1-01 + Bug-844 Layer 3 v223.28] Preserva TODOS campos do quote separadamente
        // (95.7% drift endereco_entrega fix). NÃO mais collapse notes||obs.
        notes: q.notes || null,
        obs: q.obs || null,
        obsTransportadora: q.obsTransportadora || q.obs_transportadora || null,
        enderecoEntrega: q.enderecoEntrega || q.endereco_entrega || null,
        trackingCode: q.trackingCode || q.tracking_code || null,
        items:(q.items||[]).map(i=>({productId:i.productId,qty:Number(i.qty),price:Number(i.price),discountPct:Number(i.discountPct||0),name:i.name||null}))
      };
      // [BUG-MONA-VND0729 v223.14 OBSERVABILITY] Snapshot pré-conversão pra detectar
      // silent qty-clipping de decante. Se acontecer, log Sentry com payload completo.
      // regra_falha_silenciosa_proibida: ZERO bloqueio, puro observability.
      const __qtySnapshot_v223_14 = Object.fromEntries(
        (q.items||[]).map(i => [String(i.productId), Number(i.qty) || 0])
      );
      const __decantIds_v223_14 = new Set(
        (q.items||[])
          .filter(i => {
            const p = productsById[i.productId];
            return p && (p.is_decant === true || p.categoria === 'Decants'
              || p.category === 'Decants'
              || ['5ML','2ML','3ML','10ML'].includes((p.volume||'').toUpperCase())
              || (i.name||'').toLowerCase().includes('decant'));
          })
          .map(i => String(i.productId))
      );
      let result=await createSaleAtomic({form,clients,paymentStatus,freteV,forceCreate:false,znxCounter,sales,quotes,kind:'accept_quote'});
      // [RG7-V2 ONDA-N3 v218.19 — 2026-05-13] result.queued — conversão salva offline em IDB
      // regra_falha_silenciosa_proibida: toast amarelo claro + Sentry breadcrumb
      // regra_idempotency_padrao_unico: drainer envia mesmo idemKey → server faz idempotent_replay
      if(result.success&&result.queued){
        console.log('[ZNX] doConvert QUEUED offline:',result.offlineQueueId);
        if(typeof Sentry!=='undefined')Sentry.addBreadcrumb({category:'offline-queue',message:'quote-convert queued offline',level:'info',data:{queueId:result.offlineQueueId,idemKey:result.idempotencyKey,quoteId:q.id}});
        toast('💾 Conversão salva offline — vai sincronizar quando voltar internet','warning');
        return;
      }
      if(!result.success){
        if(result.errorCode==='client_has_overdue_debt'){
          // Débito vencido — pede confirmação ao usuário antes de forçar
          const ok=await confirmForceCreateForOverdueClient(result.errorMessage);
          if(!ok){toast('Conversão cancelada.');return;}
          result=await createSaleAtomic({form,clients,paymentStatus,freteV,forceCreate:true,znxCounter,sales,quotes,kind:'accept_quote'});
          // [RG7-V2] queued no retry forceCreate (cliente em débito)
          if(result.success&&result.queued){
            console.log('[ZNX] doConvert forceCreate QUEUED offline:',result.offlineQueueId);
            if(typeof Sentry!=='undefined')Sentry.addBreadcrumb({category:'offline-queue',message:'quote-convert force queued offline',level:'info',data:{queueId:result.offlineQueueId,idemKey:result.idempotencyKey,quoteId:q.id}});
            toast('💾 Conversão salva offline — vai sincronizar quando voltar internet','warning');
            return;
          }
          if(!result.success){
            toast('❌ '+(mapErrorToUX(result.errorCode,result.errorMessage)));
            if(result.errorCode==='insufficient_stock')refreshProducts(setProducts);
            return;
          }
        }else{
          // [BUG-FIX 20260507] Quando outro user (financeiro/admin) já converteu o orçamento
          // ANTES da Isabella, o realtime pode não ter chegado e estado local fica stale.
          // Em vez de pedir Recarregue, ATUALIZA estado local com dado fresh do banco.
          const STALE_QUOTE_CODES=['quote_already_converted','quote_not_found_or_invalid','quote_invalid_status','quote_already_cancelled'];
          if(STALE_QUOTE_CODES.includes(result.errorCode)){
            try{
              const{data:freshQuote}=await sb.from('quotes').select('id,number,status,sale_id,sale_number,converted_at,converted_by,updated_at').eq('id',q.id).maybeSingle();
              if(freshQuote){
                setQuotes(prev=>prev.map(x=>x.id===q.id?{
                  ...x,
                  status:freshQuote.status,
                  saleId:freshQuote.sale_id,
                  saleNumber:freshQuote.sale_number,
                  updatedAt:freshQuote.updated_at
                }:x));
                setConflict(null);setPendingConvert(null);setModal(null);
                if(freshQuote.status==='Convertido'){
                  toast('ℹ️ Esse orçamento já foi convertido em '+(freshQuote.sale_number||'venda')+' por outro usuário. Lista atualizada.');
                } else if(freshQuote.status==='Cancelado'||freshQuote.status==='Recusado'){
                  toast('ℹ️ Esse orçamento foi '+freshQuote.status.toLowerCase()+' por outro usuário. Lista atualizada.');
                } else {
                  toast('ℹ️ Status do orçamento mudou. Lista atualizada — tente novamente.');
                }
                return;
              }
            }catch(_e){znxLogWarn('[ZNX] refresh stale quote failed', _e);}
          }
          toast('❌ '+(mapErrorToUX(result.errorCode,result.errorMessage)));
          if(result.errorCode==='insufficient_stock')refreshProducts(setProducts);
          // [ONDA-S B 2026-05-13] insufficient_stock/quote_terminal/etc são expected
          if(typeof window.znxCaptureRpcError==='function'){
            window.znxCaptureRpcError('[ZNX] doConvert RPC error',result.errorCode,{quoteId:q.id,errorMessage:result.errorMessage});
          } else if(typeof Sentry!=='undefined'){
            Sentry.captureException(new Error('[ZNX] doConvert RPC error: '+result.errorCode),{extra:{quoteId:q.id,errorMessage:result.errorMessage}});
          }
          return;
        }
      }
      // Sync state a partir da resposta autoritativa do backend
      const{saleId,saleNumber:num,recId,saleTotal,receivableDue}=result;
      // [v224.105] SUCCESS já confirmado pelo backend (sale committed). Stock refresh + post-mortem
      // movidos OFF critical path (fire-and-forget) · falha aqui NUNCA bloqueia nem mostra erro de
      // conversão (a venda JÁ está salva). refreshProducts é só reconciliação — realtime também cobre.
      (async()=>{ try{ await refreshProducts(setProducts); }catch(_re){ znxLogWarn('[ZNX] doConvert refreshProducts pos-commit (nao bloqueia)', _re); } })();
      // [BUG-MONA-VND0729 v223.14 OBSERVABILITY] Post-mortem check: comparar qty enviada
      // vs qty gravada no banco. Se qty de decante foi cortada, alerta Sentry.
      // [v224.105] OFF critical path · fire-and-forget (puro Sentry observability · zero UX impact).
      (async()=>{
      try {
        const{data:freshItems} = await sb
          .from('sale_items')
          .select('product_id,qty,product_name')
          .eq('sale_id', saleId);
        if (Array.isArray(freshItems)) {
          const __clipped = [];
          for (const it of freshItems) {
            const pid = String(it.product_id);
            const originalQty = __qtySnapshot_v223_14[pid];
            const gravadaQty = Number(it.qty) || 0;
            if (originalQty != null && gravadaQty < originalQty && __decantIds_v223_14.has(pid)) {
              __clipped.push({
                product_id: pid,
                product_name: it.product_name,
                orc_qty: originalQty,
                vnd_qty: gravadaQty,
                diff: gravadaQty - originalQty
              });
            }
          }
          if (__clipped.length > 0) {
            console.error('[ZNX v223.14] 🚨 DECANTE QTY CLIPPED:', __clipped);
            if (typeof window.znxCaptureRpcError === 'function') {
              window.znxCaptureRpcError(
                '[ZNX] decante_qty_clipped_post_convert',
                'decante_qty_clipped',
                {
                  quoteId: q.id,
                  quoteNumber: q.number,
                  saleId: saleId,
                  saleNumber: num,
                  clipped: __clipped,
                  sellerName: q.sellerName,
                  systemVersion: window.ZNX_SYSTEM_VERSION || 'unknown'
                }
              );
            } else if (typeof Sentry !== 'undefined') {
              Sentry.captureException(
                new Error('[ZNX] decante_qty_clipped_post_convert: ' + __clipped.length + ' itens'),
                { extra: { quoteId: q.id, saleId: saleId, clipped: __clipped } }
              );
            }
          }
        }
      } catch(_e) { znxLogWarn('[ZNX v223.14] post-mortem check failed', _e); }
      })();
      // Marca quote como Convertido no estado local
      setQuotes(prev=>prev.map(x=>x.id===q.id?{...x,status:'Convertido',saleNumber:num,saleId}:x));
      // Adiciona venda ao estado local (camelCase, espelho do doConvert original)
      const ns={
        id:saleId,number:num,date:today(),createdAt:new Date().toISOString(),updatedAt:new Date().toISOString(),
        clientId:q.clientId,items:q.items,status:'Aberto',paymentStatus,
        sellerName:q.sellerName||user?.name||'Admin',fromQuote:q.number,
        globalDiscountType:q.globalDiscountType||'',globalDiscountValue:q.globalDiscountValue||0,
        frete:q.frete||'Retirada',freteValor:FRETE_VALS[q.frete]||0,
        embalagem:q.embalagem||'',enderecoEntrega:q.enderecoEntrega||{},
        obsTransportadora:q.obsTransportadora||'',nfEnabled:q.nfEnabled||false,
        notaFiscal:q.notaFiscal||{},obs:q.notes||q.obs||'',paymentMethod:q.paymentMethod||'',
      };
      setSales(prev=>[...prev,ns]);
      // BUG-FIX-1: só criar receivable se paymentStatus==='Pendente'
      if(paymentStatus==='Pendente'){
        const client=clients.find(c=>c.id===q.clientId);
        setReceivables(prev=>[...prev,{id:recId,description:num+' — '+(client?.name||''),clientId:q.clientId,value:saleTotal,due:receivableDue,status:'Pendente',saleId}]);
      }
      setConflict(null);setPendingConvert(null);setModal(null);
      toast(result.replayConfirmed ? '✅ Convertido em venda '+num+' · servidor demorou mas concluiu!' : '✅ Orçamento convertido em venda '+num+'!');
    }catch(e){
      console.error('[ZNX] doConvert error:',e);
      if(typeof Sentry!=='undefined')Sentry.captureException(e,{extra:{quoteId:q.id}});
      toast('❌ Erro inesperado ao converter orçamento. Recarregue a página.');
    }finally{
      _inFlightConvert=false;
      convertInflightRef.current=false;
      setConvertingQuoteId(null);
    }
  }

  // Vendedor só vê os próprios orçamentos
  // Bug B fix: dedup por id evita duplicação visual quando applyRows e setQuotes colidem
  const myQuotes=useMemo(()=>{
    const seen=new Set();
    const base=quotes.filter(q=>{if(q.status==='Excluido')return false;if(seen.has(q.id))return false;seen.add(q.id);return true;});
    return user.role==='vendedor'?base.filter(q=>q.sellerName===user.name):base;
  },[quotes,user.name,user.role]);

  const filtered=useMemo(()=>{
    // Bug B fix: createdAt é ISO string preciso, date é só YYYY-MM-DD — usar createdAt como primário
    let list=[...myQuotes].sort((a,b)=>new Date(b.createdAt||b.date)-new Date(a.createdAt||a.date));
    if(filterStatus==='Todos')list=list.filter(q=>q.status!=='Convertido'&&q.status!=='Cancelado');
    else list=list.filter(q=>q.status===filterStatus);
    if(searchOrc.trim()){
      const sq=searchOrc.trim().toLowerCase();
      list=list.filter(o=>{
        const cli=clients.find(c=>c.id===o.clientId);
        return (o.number||'').toLowerCase().includes(sq)||(cli?.name||'').toLowerCase().includes(sq)||(o.sellerName||'').toLowerCase().includes(sq);
      });
    }
    return list;
  },[myQuotes,filterStatus,searchOrc,clients]);
  const counts=useMemo(()=>{
    const obj={Todos:myQuotes.length};
    ORC_STATUSES.forEach(s=>{obj[s]=myQuotes.filter(q=>q.status===s).length;});
    return obj;
  },[myQuotes]);

  return(
    <div>
      {/* ══ FULL-PAGE FORM (novo / editar) ══════════════════════ */}
      {/* [Backlog #579 split 2026-05-15] Header+Footer extraídos pra widgets/orcamentos/. Body (OrcPipeline + QuoteFormBody) intocado. */}
      {(modal==='new'||modal==='edit') && QuoteEditHeader && QuoteEditFooter && (
        <div style={{maxWidth:920,margin:'0 auto',paddingBottom:40}}>
          <QuoteEditHeader
            modal={modal}
            activeQuote={activeQuote}
            pendingNumber={pendingNumber}
            form={form}
            clients={clients}
            onExit={confirmExit}
          />
          {modal==='edit'&&<OrcPipeline status={form.status}/>}
          <QuoteFormBody form={form} setForm={setForm} itemForm={itemForm} setItemForm={setItemForm}
            prodSearch={prodSearch} setProdSearch={setProdSearch} showProdDrop={showProdDrop} setShowProdDrop={setShowProdDrop}
            clientSearch={clientSearch} setClientSearch={setClientSearch} showClientDrop={showClientDrop} setShowClientDrop={setShowClientDrop}
            clients={clients} products={products} quotes={quotes} sales={sales} editingId={modal==='edit'?activeId:null} vendedores={vendedores} user={user}/>
          <QuoteEditFooter
            modal={modal}
            activeQuote={activeQuote}
            isSavingQuote={isSavingQuote}
            onExit={confirmExit}
            onSave={modal==='new'?saveNew:saveEdit}
            onPDF={()=>gerarOrcamentoPDF(activeQuote,clients)}
            onWhatsApp={()=>shareWhatsApp(activeQuote)}
          />
        </div>
      )}

      
      {/* [Wave 3 LITE 2026-05-15] Exit confirmation — widget genérico */}
      <ExitConfirmModal
        open={exitConfirm}
        title="Sair do orçamento?"
        message={<>Você tem dados não salvos. Se sair agora, o número <strong>{pendingNumber||(activeQuote&&activeQuote.number)||''}</strong> será perdido e as alterações descartadas.</>}
        onCancel={()=>setExitConfirm(false)}
        onConfirm={doExit}
      />

      {/* ══ LISTA DE ORÇAMENTOS ══════════════════════════════════ */}
      {modal!=='new'&&modal!=='edit'&&(
        <>
          {/* [Onda V1] TabBar */}
          <Tabs
            tabs={[
              {id:'lista',label:'📋 Lista',count:quotes.length},
              {id:'pipeline',label:'🔄 Pipeline',count:0,disabled:true,tip:'Aguarda Onda V5'},
              {id:'insights',label:'📊 Insights',count:quotes.filter(q=>q.status==='Cancelado'||q.status==='Cancelada').length,tip:'Análise de Cancelamento (V2)'},
              {id:'360',label:'🎯 Orçamento 360º',count:selected360?1:0,disabled:!selected360,tip:'Aguarda Onda V3'}
            ]}
            active={mainTab}
            onChange={setMainTab}
          />

          {mainTab==='lista'&&<>
          <div className="page-header">
            {/* [L2-Orcamentos 2026-05-09] Title com contador adaptativo */}
            <div className="page-title">
              Orçamentos
              {(() => {
                const hasFilters=!!(searchOrc||filterStatus!=='Todos');
                return hasFilters
                  ? <span style={{fontSize:13,color:'#B89840',fontWeight:600,marginLeft:8}}>({filtered.length} de {myQuotes.length})</span>
                  : <span style={{fontSize:13,color:'#9CA3AF',fontWeight:400,marginLeft:8}}>({myQuotes.length})</span>;
              })()}
            </div>
            {!isReadOnly&&<button className="btn-gold" onClick={openNew} style={{display:'flex',alignItems:'center',gap:6}}>
              <Icon n="plus" size={14}/>Novo Orçamento
            </button>}
          </div>

          {/* FILTER TABS */}
        <div style={{marginBottom:12}}>
          <div className="search-bar" style={{maxWidth:360}}>
            <span className="search-icon">⌕</span>
            <input value={searchOrc} onChange={e=>setSearchOrc(e.target.value)} placeholder="🔍 Buscar nº, cliente ou vendedor..." style={{width:'100%'}}/>
            {searchOrc&&<button onClick={()=>setSearchOrc('')} style={{background:'none',border:'none',color:'#9CA3AF',cursor:'pointer',fontSize:16,padding:'0 8px'}}>✕</button>}
          </div>
        </div>

          <div style={{display:'flex',gap:6,marginBottom:16,flexWrap:'wrap'}}>
            {['Todos',...ORC_STATUSES].map(s=>(
              <button key={s} onClick={()=>setFilterStatus(s)}
                style={{padding:'5px 16px',borderRadius:20,fontSize:12,fontWeight:600,cursor:'pointer',transition:'all .15s',
                  background:filterStatus===s?'#1B2A4A':'transparent',
                  color:filterStatus===s?'#B89840':'#9CA3AF',
                  border:`1px solid ${filterStatus===s?'#1B2A4A':'#D1D5DB'}`}}>
                {s}{counts[s]>0&&<span style={{opacity:.7,fontWeight:400,marginLeft:4}}>({counts[s]})</span>}
              </button>
            ))}
          </div>

          {/* TABLE */}
          <div className="card" style={{padding:0}}>
            {/* [L2-Orcamentos 2026-05-09] Empty state inteligente — 3 estados */}
            {filtered.length===0?(
              <div style={{textAlign:'center',padding:'50px 20px'}}>
                <div style={{fontSize:38,marginBottom:10}}>{searchOrc||filterStatus!=='Todos'?'🔍':'📋'}</div>
                {searchOrc||filterStatus!=='Todos'?(
                  <>
                    <div style={{fontSize:15,fontWeight:600,color:'#374151',marginBottom:6}}>Nenhum orçamento bate com os filtros</div>
                    <div style={{fontSize:12,color:'#6B7280',marginBottom:14}}>
                      {searchOrc&&<>Busca: <strong>"{searchOrc}"</strong></>}
                      {searchOrc&&filterStatus!=='Todos'&&' · '}
                      {filterStatus!=='Todos'&&<>Status: <strong>{filterStatus}</strong></>}
                    </div>
                    <button onClick={()=>{setSearchOrc('');setFilterStatus('Todos');}}
                      style={{padding:'9px 22px',background:'#1B2A4A',color:'#fff',border:'none',borderRadius:6,fontSize:12,fontWeight:600,cursor:'pointer'}}>
                      ↻ Limpar todos os filtros
                    </button>
                  </>
                ):myQuotes.length===0?(
                  <>
                    <div style={{fontSize:15,fontWeight:600,color:'#374151',marginBottom:6}}>Nenhum orçamento ainda</div>
                    <div style={{fontSize:12,color:'#6B7280',marginBottom:14}}>Comece criando o primeiro orçamento — depois vira venda em 1 clique.</div>
                    {!isReadOnly&&<button onClick={openNew}
                      style={{padding:'9px 22px',background:'#B89840',color:'#fff',border:'none',borderRadius:6,fontSize:12,fontWeight:600,cursor:'pointer'}}>
                      ➕ Novo Orçamento
                    </button>}
                  </>
                ):(
                  <div style={{fontSize:13,color:'#6B7280'}}>Nenhum orçamento ativo. Veja Convertidos/Cancelados nos status.</div>
                )}
              </div>
            ):(
              <table>
                <thead>
                  <tr style={{background:'#1B2A4A'}}>
                    <th style={{color:'#B89840',fontWeight:700,padding:'12px 14px'}}>Nº</th>
                    <th style={{color:'#fff',fontWeight:600}}>Data</th>
                    <th style={{color:'#fff',fontWeight:600}}>Cliente</th>
                    <th style={{color:'#fff',fontWeight:600}}>Vendedor</th>
                    <th style={{color:'#fff',fontWeight:600}}>Itens</th>
                    <th style={{color:'#B89840',fontWeight:700}}>Total</th>
                    <th style={{color:'#fff',fontWeight:600}}>Status</th>
                    <th style={{color:'#fff',fontWeight:600}}>Ações</th>
                  </tr>
                </thead>
                <tbody>
                  {filtered.map(q=>{
                    const c=clients.find(x=>x.id===q.clientId);
                    return(
                      <tr key={q.id} style={{opacity:q.status==='Recusado'?.6:1}}>
                        <td>
                          <span className="gold" style={{fontWeight:700,cursor:'pointer'}} onClick={()=>openView(q)}>{q.number}</span>
                          {q.status==='Convertido'&&q.saleNumber&&<span style={{background:'#1B2A4A',color:'#C8A951',padding:'2px 8px',borderRadius:12,fontSize:11,fontWeight:'bold',marginLeft:6,display:'inline-block'}}>Convertido → {q.saleNumber}</span>}
                          {q.notes&&<span title={q.notes} style={{marginLeft:5,fontSize:11,cursor:'help'}}>📝</span>}
                        </td>
                        <td style={{fontSize:12}}>{fmtDate(q.date)}{(q.updatedAt||q.createdAt)&&<><br/><span style={{fontSize:11,color:'#9CA3AF'}}>{new Date(q.updatedAt||q.createdAt).toLocaleTimeString('pt-BR',{hour:'2-digit',minute:'2-digit'})}{q.updatedAt?' ✏':''}</span></>}</td>
                        <td style={{fontWeight:500}}>{c?.name||q.clientName||'—'}</td>
                        <td style={{fontSize:12,color:'#374151',fontWeight:500}}>{q.sellerName||'—'}</td>
                        <td style={{color:'#9CA3AF',fontSize:12}}>{(()=>{const items=q.items||[];const prods=items.length;const units=items.reduce((s,i)=>s+(i.qty||0),0);return<span>{prods} produto{prods!==1?'s':''}<br/><span style={{fontSize:11}}>{units} un.</span></span>;})()}</td>
                        <td className="gold" style={{fontWeight:700}}>
                          {/* [FEAT credit-preview 20260504-v17] Mostra preview do credit aplicado se cliente tem saldo e quote nao foi convertido */}
                          {(()=>{
                            const subtotal=saleFinalTotal(q);
                            const credit=Number(c?.creditBalance||c?.credit_balance||0);
                            const willApply=q.status!=='Convertido'&&credit>0&&subtotal>0?Math.min(credit,subtotal):0;
                            if(willApply>0){
                              return(
                                <div>
                                  <div style={{textDecoration:'line-through',color:'#9CA3AF',fontSize:12,fontWeight:500}}>{fmt(subtotal)}</div>
                                  <div style={{color:'#B89840',fontSize:14}}>{fmt(subtotal-willApply)}</div>
                                  <div style={{fontSize:10,color:'#10B981',fontWeight:600}}>−{fmt(willApply)} crédito</div>
                                </div>
                              );
                            }
                            return fmt(subtotal);
                          })()}
                        </td>
                        <td>
                          <span style={{display:'inline-block',padding:'2px 10px',borderRadius:12,fontSize:11,fontWeight:700,
                            background:(ORC_STATUS_COLORS[q.status]||'#A89070')+'22',
                            color:ORC_STATUS_COLORS[q.status]||'#A89070',
                            border:`1px solid ${(ORC_STATUS_COLORS[q.status]||'#A89070')}44`}}>
                            {q.status}
                          </span>
                        </td>
                        <td>
                          <div style={{display:'flex',gap:5,flexWrap:'wrap'}}>
                            {/* [v224.126 NUCLEAR] badge status + botões promover/cancelar */}
                            {window.ZNX&&window.ZNX.widgets&&window.ZNX.widgets.quotes&&window.ZNX.widgets.quotes.QuoteStatusActions&&React.createElement(window.ZNX.widgets.quotes.QuoteStatusActions,{quote:q,user:user,onPromoted:function(data){if(typeof setQuotes==='function'&&data&&data.new_status){setQuotes(function(prev){return prev.map(function(qq){return qq.id===q.id?Object.assign({},qq,{status:data.new_status}):qq;});});}}})}
                            <button className="btn-outline btn-sm" title="Orçamento 360º" onClick={()=>{setSelected360(q.id);setMainTab('360');}} style={{padding:'3px 8px',fontSize:11,borderColor:'#C8A951',color:'#92700A'}}>🎯</button>
                            <button className="btn-outline btn-sm" onClick={()=>openView(q)} style={{padding:'3px 8px',fontSize:11}}>👁 Ver</button>
                            {!isReadOnly&&!isTerminalQuoteStatus(q.status)&&<button className="btn-outline btn-sm" onClick={()=>openEdit(q)} style={{padding:'3px 8px',fontSize:11}}>✏ Editar</button>}
                            <button className="btn-outline btn-sm" onClick={()=>gerarOrcamentoPDF(q,clients)} style={{padding:'3px 8px',fontSize:11}}>📄 PDF</button>
                            <button className="btn-outline btn-sm" onClick={()=>shareWhatsApp(q)} style={{padding:'3px 8px',fontSize:11,borderColor:'#25D366',color:'#25D366'}}>📱 WA</button>
                            {/* [BUG-FIX 20260504] Financeiro PODE converter orçamento em venda (igual admin).
                                Antes: !isReadOnly bloqueava financeiro pq isReadOnly=true pra ele.
                                Agora: admin+financeiro vêem o botão. Vendedor não vê (já não conseguia
                                executar via znxGuard em doConvert L546). */}
                            {!isTerminalQuoteStatus(q.status)&&(user.role==='admin'||user.role==='financeiro')&&<button className="btn-gold btn-sm" onClick={()=>convert(q)} disabled={!!convertingQuoteId} style={{padding:'3px 8px',fontSize:11,opacity:convertingQuoteId?0.6:1,cursor:convertingQuoteId?'not-allowed':'pointer'}}>{convertingQuoteId===q.id?'⏳…':'→ Venda'}</button>}
                            {!isReadOnly&&!isTerminalQuoteStatus(q.status)&&<button className="btn-danger btn-sm" onClick={()=>setCancelMotivoModal({id:q.id,number:q.number,total:q.total,clientId:q.clientId,clientName:clients.find(c=>c.id===q.clientId)?.name||''})} style={{padding:'3px 8px',fontSize:11}}>🗑</button>}
                          </div>
                        </td>
                      </tr>
                    );
                  })}
                </tbody>
                <tfoot>
                  <tr style={{background:'#1B2A4A',borderTop:'2px solid #B89840'}}>
                    <td colSpan="5" style={{padding:'10px 14px',color:'#9CA3AF',fontSize:12,fontWeight:600}}>
                      {filtered.length} orçamento{filtered.length!==1?'s':''}
                      {' · '}{filtered.reduce((s,q)=>s+(q.items||[]).reduce((a,i)=>a+(i.qty||0),0),0)} unidades no total
                    </td>
                    <td style={{padding:'10px 14px',textAlign:'left'}}>
                      <div style={{fontSize:11,color:'#9CA3AF',textTransform:'uppercase',letterSpacing:.5,marginBottom:2}}>Subtotal listagem</div>
                      <div style={{color:'#B89840',fontWeight:900,fontSize:16}}>{fmt(filtered.reduce((s,q)=>s+saleFinalTotal(q),0))}</div>
                    </td>
                    <td colSpan="2"/>
                  </tr>
                </tfoot>
              </table>
            )}
          </div>
          </>}

          {/* ═══ ABA PIPELINE (placeholder) ═══ */}
          {mainTab==='pipeline'&&(
            <OrcamentoPipeline
              quotes={quotes}
              clients={clients}
              user={user}
              onSelect={(qid)=>{setSelected360(qid);setMainTab('360');}}
            />
          )}

          {/* ═══ ABA INSIGHTS — Análise de Cancelamento V2 ═══ */}
          {mainTab==='insights'&&(
            <OrcamentosCancelInsights quotes={quotes} sales={sales} userRole={user?.role} />
          )}
          {false&&mainTab==='insights'&&(
            <div className="card" style={{padding:40,textAlign:'center',color:'#6B7280'}}>
              <div style={{fontSize:48,marginBottom:14}}>📊</div>
              <div style={{fontSize:18,fontWeight:700,color:'#1B2A4A',marginBottom:8}}>Insights de Orçamentos — em construção</div>
              <div style={{fontSize:13,maxWidth:520,margin:'0 auto',lineHeight:1.6}}>
                Análise de cancelamento (49 = R$133k!), motivos top, tempo médio até converter (1.7h hoje), taxa por vendedora, gargalos.
              </div>
              <div style={{marginTop:16,fontSize:11,color:'#9CA3AF'}}>Aguarde Onda V2/V3.</div>
            </div>
          )}

          {/* ═══ ABA 360º (placeholder) ═══ */}
          {mainTab==='360'&&selected360&&(
            <OrcamentoTimeline
              quote={quotes.find(q=>q.id===selected360)||null}
              client={clients.find(c=>c.id===(quotes.find(q=>q.id===selected360)?.clientId))}
              products={products}
              discountRequests={discountRequests}
              sales={sales}
              user={user}
              onBack={()=>{setMainTab('lista');setSelected360(null);}}
              onEdit={(q)=>{setMainTab('lista');setSelected360(null);openEdit(q);}}
              onCancel={(q)=>setCancelMotivoModal({id:q.id,number:q.number,total:q.total,clientId:q.clientId,clientName:clients.find(c=>c.id===q.clientId)?.name||''})}
              onConvert={(q)=>{setMainTab('lista');setSelected360(null);convert(q);}}
              onPrintPdf={(q)=>gerarOrcamentoPDF(q,clients)}
              onWhatsApp={(q)=>shareWhatsApp(q)}
            />
          )}
          {mainTab==='360'&&!selected360&&(
            <div className="card" style={{padding:40,textAlign:'center',color:'#6B7280'}}>
              <div style={{fontSize:48,marginBottom:14}}>🎯</div>
              <div style={{fontSize:18,fontWeight:700,color:'#1B2A4A',marginBottom:8}}>Selecione um orçamento</div>
              <div style={{fontSize:13,maxWidth:520,margin:'0 auto',lineHeight:1.6}}>
                Volte pra Lista e clique no 🎯 dourado de qualquer orçamento pra ver Orçamento 360º.
              </div>
            </div>
          )}
        </>
      )}

      {/* ── MODAL VISUALIZAR ── */}
      {/* [Wave 4 KIMI 2026-05-15] Extraído pra widgets/orcamentos/QuoteViewModal.jsx */}
      {modal==='view' && activeQuote && (
        <QuoteViewModal
          activeQuote={activeQuote}
          clients={clients}
          products={products}
          user={user}
          convertingQuoteId={convertingQuoteId}
          isTerminalQuoteStatus={isTerminalQuoteStatus}
          setModal={setModal}
          setCancelMotivoModal={setCancelMotivoModal}
          openEdit={openEdit}
          gerarOrcamentoPDF={gerarOrcamentoPDF}
          shareWhatsApp={shareWhatsApp}
          convert={convert}
          updateNote={updateNote}
        />
      )}

      {/* ── MODAL CANCELAR ORÇAMENTO V2 (motivo obrigatório — Análise R$133k) ── */}
      {cancelMotivoModal && (
        <CancelQuoteModal
          quote={cancelMotivoModal}
          client={clients.find(c=>c.id===cancelMotivoModal.clientId)}
          onConfirm={async (reason)=>doDelete(cancelMotivoModal.id, reason)}
          onClose={()=>setCancelMotivoModal(null)}
        />
      )}

      {/* ── MODAL MOTIVO DESCONTO V6 (vendedora justifica antes de admin ver) ── */}
      {discountReasonModal && (
        <DiscountReasonModal
          totalDesconto={discountReasonModal.totalDesconto}
          totalLiquido={discountReasonModal.totalLiquido}
          client={discountReasonModal.client}
          onConfirm={submitDiscountWithReason}
          onClose={()=>setDiscountReasonModal(null)}
        />
      )}

      {/* ── MODAL CONFLITO DE ESTOQUE ── */}
      {/* [Wave 4 KIMI 2026-05-15] Extraído pra widgets/orcamentos/InsufficientStockConflictModal.jsx */}
      <InsufficientStockConflictModal
        conflict={conflict}
        setConflict={setConflict}
        pendingConvert={pendingConvert}
        setPendingConvert={setPendingConvert}
        openEdit={openEdit}
      />

      {/* ── ONDA-E E3 2026-05-11 ── MODAL EDIÇÃO CONCORRENTE ── */}
      {/* Sentry ZAYNEX-ERP-R: 58 events em 7d. Mostrava erro técnico crú no toast. */}
      {/* Agora: refetch silencioso + retry automático já feito no saveEdit. Se 2º também falhar, abre este modal. */}
      {/* [Wave 4 KIMI 2026-05-15] Extraído pra widgets/orcamentos/ConcurrentEditModal.jsx */}
      <ConcurrentEditModal
        concurrentEditModal={concurrentEditModal}
        setConcurrentEditModal={setConcurrentEditModal}
        setQuotes={setQuotes}
        setModal={setModal}
        editIdemKeyRef={editIdemKeyRef}
      />
    </div>
  );
}

  window.ZNX = window.ZNX || {};
  window.ZNX.components = window.ZNX.components || {};
  window.ZNX.components.Orcamentos = Orcamentos;
  window.Orcamentos = Orcamentos;

  window.ZNX.refactor_phase_6_loaded = window.ZNX.refactor_phase_6_loaded || {};
  window.ZNX.refactor_phase_6_loaded.Orcamentos = true;

})();
