// js/components/pages/Produtos.jsx
// Gestão de produtos (CRUD + estoque + custos)
// Extraído de index.html em Fase 6 (2026-04-29): L1081-L1612
// Deps runtime: fmt, nid, genIdUUID, dualWrite, toast, showConfirm, Modal, SmartSelect, Icon, znxGuard, Marcas
(function() {
  'use strict';
  const {useState, useEffect, useMemo, useRef} = React;

  // [REFACTOR Wave 3 #528 — 2026-05-15] Tabs widget extracted to widgets/Tabs.jsx.
  // [v224.73 + v224.55] vars+check MOVED to component body (preventivo · regra_validacao_helpers_runtime)

function Produtos({user,products,setProducts,suppliers,sales,quotes,clients}){
  // [v224.73 FIX 2026-05-30] vars+check em render time (10 refs)
  const Tabs = window.ZNX?.widgets?.Tabs;
  const Pagination = window.ZNX?.widgets?.produtos?.Pagination;
  const CatalogoPdfModal = window.ZNX?.widgets?.produtos?.CatalogoPdfModal;
  const BulkActionModal = window.ZNX?.widgets?.produtos?.BulkActionModal;
  const produtosCatalogoPdf = window.ZNX?.produtos?.catalogoPdf;
  const produtosCalcs = window.ZNX?.produtos?.calcs;
  const LossBanner = window.ZNX?.widgets?.produtos?.LossBanner;
  const BulkToolbar = window.ZNX?.widgets?.produtos?.BulkToolbar;
  const ProductFiltersBar = window.ZNX?.widgets?.produtos?.ProductFiltersBar;
  const ProductFormModal = window.ZNX?.widgets?.produtos?.ProductFormModal;
  if (!Tabs || !Pagination || !CatalogoPdfModal || !BulkActionModal || !produtosCatalogoPdf || !produtosCalcs
      || !LossBanner || !BulkToolbar || !ProductFiltersBar || !ProductFormModal) {
    const _msg = `[Produtos v224.73] widgets/bundles faltando: Tabs=${!!Tabs}, Pagination=${!!Pagination}, CatalogoPdfModal=${!!CatalogoPdfModal}, BulkActionModal=${!!BulkActionModal}, catalogoPdf=${!!produtosCatalogoPdf}, calcs=${!!produtosCalcs}, LossBanner=${!!LossBanner}, BulkToolbar=${!!BulkToolbar}, ProductFiltersBar=${!!ProductFiltersBar}, ProductFormModal=${!!ProductFormModal}`;
    console.error(_msg);
    window.Sentry?.captureMessage?.(_msg, 'error');
  }
  // [FEATURE Onda P1 20260505 sistema 4] sistema de abas Lista / Insights / Produto 360º
  const[activeTab,setActiveTab]=useState('lista');
  const[selected360,setSelected360]=useState(null);
  // [H1a v205 20260512] Persistência filtro busca em localStorage — vendedora muda página e volta sem perder filtro
  const[search,setSearch]=useState(()=>{try{return localStorage.getItem('znx_search_produtos')||'';}catch(_){return '';}});
  React.useEffect(()=>{try{localStorage.setItem('znx_search_produtos',search);}catch(_){}},[search]);
  const[modal,setModal]=useState(null);
  const[catalogModal,setCatalogModal]=useState(false);
  const[catOpts,setCatOpts]=useState({brands:[],categoria:'',genero:'',includePrice:true,includeStock:false});
  const[form,setForm]=useState({brand:'',productName:'',name:'',code:'',supplierId:'',stock:0,salePrice:0,avgCost:0,newQty:'',newCost:'',genero:'',concentracao:'',categoria:'',volume:'',estoqueMin:20,ncm:'',ean:'',tags:[]});
  const[filterGenero,setFilterGenero]=useState('');
  const[filterCategoria,setFilterCategoria]=useState('');
  const[page,setPage]=useState(1);
  const[filterSupp,setFilterSupp]=useState('');
  const[filterStatus,setFilterStatus]=useState('');
  const[filterBrand,setFilterBrand]=useState('');
  const[editingStockId,setEditingStockId]=useState(null);
  const[tempStock,setTempStock]=useState('');
  const[adjustStockProduct,setAdjustStockProduct]=useState(null); // [v223.36 FEAT-STOCK-ADMIN] modal ajuste estoque admin Jamal
  // [v224.51 2026-05-28] Tabela marcas via useStore · mescla com products.brand em allBrands (datalist Novo Produto)
  const[marcasTable]=useStore('marcas',[]);
  // [v224.40 F 20260526] Warehouses fetch · 2 cols Alfonso + Senador na tabela (qty por depósito)
  const[warehouses,setWarehouses]=useState([]);
  React.useEffect(function(){
    if(typeof window.sb === 'undefined') return;
    let cancelled = false;
    window.sb.from('warehouses').select('id,name,code').order('name')
      .then(function(res){
        if(cancelled) return;
        if(res.error){
          znxLogWarn('[v224.72 Produtos warehouses fetch]', res.error);
          if(window.Sentry) window.Sentry.captureException(res.error);
          return;
        }
        if(Array.isArray(res.data)) setWarehouses(res.data);
      })
      .catch(function(e){
        if(cancelled) return;
        znxLogWarn('[v224.40 F produtos warehouses]', e);
      });
    return function(){ cancelled = true; };
  }, []);
  const alfonsoWh = useMemo(function(){ return warehouses.find(function(w){return w.name === 'Alfonso';}); }, [warehouses]);
  const senadorWh = useMemo(function(){ return warehouses.find(function(w){return w.name === 'Senador';}); }, [warehouses]);
  const[sortCol,setSortCol]=useState(null);
  const[sortDir,setSortDir]=useState('asc');
  function toggleSort(col){setSortCol(prev=>{setSortDir(d=>prev===col?(d==='asc'?'desc':'asc'):'asc');return col;});}
  const SortTh=({col,children,style})=>{const active=sortCol===col;return<th style={{cursor:'pointer',userSelect:'none',whiteSpace:'nowrap',...style}} onClick={()=>toggleSort(col)}>{children}{active?sortDir==='asc'?' ▲':' ▼':' ⇅'}</th>;};
  const PAGE_SIZE=50;
  const isAdmin=user.role==='admin';
  const isAdminJamal=isAdmin && user.username==='jamal'; // [v223.36 FEAT-STOCK-ADMIN] guard UX-only — backend tem znx_role()='admin' check real
  const isVendedor=user.role==='vendedor';
  const canSeeCost=user.role==='admin';
  const[hideConfidential,setHideConfidential]=useState(false);
  const showCost=canSeeCost&&!hideConfidential;
  // [Onda L Fase 2 — 20260507] Vendedor não vê fornecedor nem código (info estratégica/operacional).
  // [v224.27 ESTOQUISTA-PRIVACY 20260526] Estoquista também NÃO vê fornecedor — via window.canSeeSupplier.
  // Admin tem toggle hideConfidential; demais roles permitidos veem por default.
  const showSupplier=(typeof window!=='undefined' && window.canSeeSupplier)
    ? window.canSeeSupplier(user)
    : (!isVendedor && user?.role!=='estoquista' && (!hideConfidential||!isAdmin));
  const showCode=!isVendedor;
  // [Onda L Fase 3 — 20260507] Bulk edit state
  const[selectedIds,setSelectedIds]=useState(new Set());
  const[bulkModal,setBulkModal]=useState(null); // {op, label} | null
  const[bulkValue,setBulkValue]=useState('');
  const[bulkInFlight,setBulkInFlight]=useState(false);
  // [FIX-A 20260511 — regra_loading_state_obrigatorio] Inflight pro save de produto.
  // Bug 2026-05-11: Jamal viu múltiplos toasts "duplicate key idx_products_brand_name_active_unique"
  // + "Servidor demorou muito". Causa: botão Salvar sem disabled → double-click disparava 2 INSERTs
  // em paralelo com (brand, name, active=true) idênticos → 2º estourava unique constraint.
  // Audit_log mostrou TODOS produtos salvos hoje aparecem 2x no mesmo segundo (combinado com FIX-B).
  const saveInflightRef = useRef(false);
  const[isSaving,setIsSaving]=useState(false); // re-render trigger pra mostrar "Salvando..."
  // [Onda L Fase 4 — 20260507] FTS server-side: quando user digita 3+ chars, busca no banco.
  // [BUG-FIX 2026-05-12 v199 AUDM6 Bug 1.1] AbortController evita race condition: se user digita rápido
  // "abc" → "abcd", request "abc" antiga não sobrescreve resultado de "abcd". Cancel via signal.aborted.
  const[ftsIds,setFtsIds]=useState(null); // null = sem fts (usa search local), Set = ids matched (pode estar vazio)
  const[ftsLoading,setFtsLoading]=useState(false);
  const ftsAbortRef = useRef(null);
  useEffect(()=>{
    const q = (search||'').trim();
    if(q.length < 3){ setFtsIds(null); setFtsLoading(false); return; }
    // Cancela request anterior (se existe) pra evitar race
    if(ftsAbortRef.current) ftsAbortRef.current.aborted = true;
    const myAbort = { aborted: false };
    ftsAbortRef.current = myAbort;
    setFtsLoading(true);
    const timer = setTimeout(async()=>{
      try{
        const result = await window.searchProductsFTS(q, 200);
        if(myAbort.aborted) return; // request mais nova chegou — descarta esse resultado
        if(result.success){
          setFtsIds(new Set((result.rows||[]).map(p=>p.id)));
        } else {
          setFtsIds(null); // erro RPC → fallback pra busca local
        }
      } catch(e){
        if(myAbort.aborted) return;
        znxLogWarn('[ZNX] FTS error', e);
        setFtsIds(null);
      }
      if(!myAbort.aborted) setFtsLoading(false);
    }, 300);
    return ()=>{ clearTimeout(timer); myAbort.aborted = true; };
  },[search]);

  async function saveInlineStock(p){
    // [FURO #513 FIX 2026-05-15 v220.1] DEFENSE-IN-DEPTH guard.
    // Função mantida pra rollback rápido se necessário, mas UI agora não chama mais.
    // Se for chamada (cache stale, dev tools, etc): bloqueia + toast + Sentry.
    setEditingStockId(null);
    toast('⚠️ Edição direta de estoque foi desabilitada. Use Depósitos > Ajuste de Inventário.');
    if(typeof Sentry!=='undefined'){
      Sentry.captureMessage('[FURO-513] saveInlineStock chamada após disable v220.1',{level:'warning',extra:{productId:p.id,productName:p.name}});
    }
    return;
    /* eslint-disable no-unreachable */
    // [SEC-001] Server-side role check — inline stock edit
    if(!await znxGuard(['admin','estoquista'])){setEditingStockId(null);return;}
    const val=Math.max(0,parseInt(tempStock)||0);
    if(val===p.stock){setEditingStockId(null);return;} // no-op se valor igual
    // [Onda L Fase 1 — 20260507] SELECT fresh ANTES + RPC atomic.
    // Antes: dualWrite direto vulnerável ao bug stale (mesmo do form modal).
    // Agora: pega stock real do banco; se divergir do que user vê, ele decide.
    try{
      const{data:fresh,error:freshErr}=await sb.from('products').select('stock,updated_at').eq('id',p.id).single();
      if(freshErr||!fresh){
        toast('❌ Não consegui validar estoque atual. Tente de novo.');
        setEditingStockId(null);
        return;
      }
      const stockBanco = Number(fresh.stock)||0;
      // Se user clicou "editar" mostrando 100 mas banco já tá em 95 (alguém vendeu enquanto ele digitava),
      // confirma se ele quer mesmo fixar em 'val' (sobrescrevendo o 95).
      if(stockBanco !== p.stock){
        const ok = await showConfirm({
          title:'⚠️ Estoque mudou enquanto você editava',
          message:`Você abriu com ${p.stock} unidades.\nNo banco agora tem ${stockBanco} (alguém pode ter vendido).\nVocê quer fixar em ${val}?\n\nSe sim, vai sobrescrever o valor do banco.`,
          confirmText:'Sim, fixar em '+val,
          confirmColor:'#EA580C'
        });
        if(!ok){setEditingStockId(null);return;}
      }
      // RPC atomic com source='inline_edit'
      const result = await window.updateProductAtomic({
        product_id: p.id,
        stock: val,
        source: 'inline_edit',
        notes_audit: 'Inline edit estoque: '+p.stock+' → '+val
      });
      if(!result.success){
        const msg = (typeof mapErrorToUX==='function' ? mapErrorToUX(result.errorCode, result.errorMessage) : null)
                  || result.errorMessage || 'Erro ao salvar estoque';
        toast('❌ ' + msg);
        // [ONDA-S B 2026-05-13] expected codes → level=info
        if(typeof window.znxCaptureRpcError==='function'){
          window.znxCaptureRpcError('saveInlineStock RPC',result.errorCode||'failed',{productId:p.id,oldStock:p.stock,newStock:val});
        } else if(typeof Sentry!=='undefined'){
          Sentry.captureException(new Error('saveInlineStock RPC failed'),{extra:{productId:p.id,oldStock:p.stock,newStock:val,errorCode:result.errorCode}});
        }
        return;
      }
      // Sync state local com retorno autoritativo
      setProducts(prev=>prev.map(x=>x.id===p.id?{...x,stock:val,avgCost:result.product?.avg_cost||x.avgCost,updatedAt:result.product?.updated_at}:x));
    } catch(e){
      console.error('[ZNX] saveInlineStock error:',e);
      toast('❌ Erro inesperado ao salvar estoque');
    } finally {
      setEditingStockId(null);
    }
  }

  // Marcas únicas para filtro
  // [v224.51 2026-05-28] allBrands UNE products.brand + marcas cadastradas (tabela marcas via useStore)
  // Resolve galinha-ovo: marca nova (ex: MPF 27/05) aparece no datalist do Novo Produto MESMO sem produto.
  // useStore('marcas') é global runtime (window.useStore) · mesmo pattern Marcas.jsx L34.
  const allBrands=useMemo(()=>{
    const fromProducts = products.map(p => p.brand || extractBrand(p.name)).filter(Boolean);
    const fromTable = (marcasTable||[]).map(m => m.name).filter(Boolean);
    return [...new Set([...fromProducts, ...fromTable])].sort();
  },[products, marcasTable]);

  const filtered=useMemo(()=>{
    const pBrand=p=>p.brand||extractBrand(p.name);
    const min=p=>p.estoqueMin||20;
    const preFiltered=products.filter(p=>{
      const matchSupp=!filterSupp||p.supplierId===Number(filterSupp);
      const matchBrand=!filterBrand||pBrand(p)===filterBrand;
      const matchStatus=!filterStatus||(
        filterStatus==='zerado'?p.stock<=0:
        filterStatus==='baixo'?isLowStock(p,sales):
        filterStatus==='acabando'?(p.stock>0&&p.stock<=min(p)):
        filterStatus==='prejuizo'?produtosCalcs.isLossSelling(p):
        // [Fase 3] Filtros de qualidade do cadastro
        filterStatus==='sem_fornecedor'?!p.supplierId:
        filterStatus==='sem_categoria'?!p.categoria:
        filterStatus==='sem_volume'?!p.volume:
        filterStatus==='sem_genero'?!p.genero:
        filterStatus==='sem_ncm'?!p.ncm:
        filterStatus==='sem_ean'?!p.ean:
        filterStatus==='custo_zero'?Number(p.avgCost||0)<=0:
        p.stock>min(p)
      );
      const matchGenero=!filterGenero||p.genero===filterGenero;
      const matchCategoria=!filterCategoria||p.categoria===filterCategoria;
      return matchSupp&&matchBrand&&matchStatus&&matchGenero&&matchCategoria;
    });
    if(!search.trim())return preFiltered;
    // [Fase 4] Se FTS ativo (>= 3 chars), filtra pelos IDs retornados pelo banco
    // [BUG-FIX 2026-05-12 v199 AUDM6 — busca "dur" retornava vazio]
    // ANTES: se FTS retornava size===0 → retornava [] direto, ignorando fuzzy local.
    // tsquery exige palavra completa, então busca curta "dur" NÃO matchava em FTS mas matchava em fuzzy local.
    // AGORA: combina FTS (precisão alta) + fuzzy local (substring) — UNION sem duplicar.
    const localMatches = search.trim() ? searchProducts(preFiltered,search) : preFiltered;
    if(ftsIds && ftsIds.size > 0){
      // FTS achou — UNIÃO com local (sem duplicar). Local cobre substrings que FTS não pega.
      const localIds = new Set(localMatches.map(p=>p.id));
      const ftsExtra = preFiltered.filter(p => ftsIds.has(p.id) && !localIds.has(p.id));
      return [...localMatches, ...ftsExtra];
    }
    // FTS desligado (busca curta) OU FTS vazio → SÓ fuzzy local (pega substring tipo "dur")
    return localMatches;
  },[products,search,filterSupp,filterBrand,filterStatus,filterGenero,filterCategoria,ftsIds]);

  // [Wave 25-alt v224.6] sorted delegado pra calcs.makeSortComparator
  const sorted=useMemo(()=>{
    if(!sortCol)return filtered;
    return [...filtered].sort(produtosCalcs.makeSortComparator(sortCol, sortDir, suppliers));
  },[filtered,sortCol,sortDir,suppliers]);
  const totalPages=Math.ceil(sorted.length/PAGE_SIZE);
  const paginated=sorted.slice((page-1)*PAGE_SIZE,page*PAGE_SIZE);
  useEffect(()=>setPage(1),[search,filterSupp,filterBrand,filterStatus,filterGenero,filterCategoria,sortCol,sortDir]);

  function openNew(){
    setForm({brand:'',productName:'',name:'',code:'',supplierId:suppliers[0]?.id||'',stock:0,salePrice:0,avgCost:0,newQty:'',newCost:'',genero:'',concentracao:'',categoria:'',volume:'',estoqueMin:20});
    setModal('new');
  }
  function openEdit(p){
    const brand=p.brand||extractBrand(p.name);
    const productName=p.productName||(p.name.startsWith(brand)?p.name.slice(brand.length).trim():p.name);
    // [BUG-FIX 20260507] Snapshot dos valores originais pra detectar mudança intencional
    // de avg_cost/stock no save (sem isso, edits manuais não salvavam — bug 9AM Azul).
    const snapshot = {
      avgCost: Number(p.avgCost)||0,
      stock: Number(p.stock)||0,
      salePrice: Number(p.salePrice)||0
    };
    setForm({...p,brand,productName,newQty:'',newCost:'',_originalSnapshot:snapshot});
    setModal('edit');
  }

  // [Onda L Fase 2 — 20260507] Duplicar produto: pré-popula form com dados,
  // limpa nome/código pra evitar duplicate constraint. Stock zerado pra forçar
  // novo cadastro consciente. Custo e preço mantidos pra acelerar.
  function openDuplicate(p){
    const brand=p.brand||extractBrand(p.name);
    const productName=p.productName||(p.name.startsWith(brand)?p.name.slice(brand.length).trim():p.name);
    setForm({
      brand,
      productName: productName + ' (cópia)',
      name: '',
      code: '',
      supplierId: p.supplierId || suppliers[0]?.id || '',
      stock: 0,
      salePrice: Number(p.salePrice)||0,
      avgCost: Number(p.avgCost)||0,
      newQty: '',
      newCost: '',
      genero: p.genero || '',
      concentracao: p.concentracao || '',
      categoria: p.categoria || '',
      volume: p.volume || '',
      estoqueMin: Number(p.estoqueMin)||20,
      is_decant: p.is_decant || p.isDecant || false,
      notes: p.notes || ''
    });
    setModal('new');
  }

  // [Onda L Fase 3 — 20260507] Bulk selection helpers
  function toggleSelect(id){
    setSelectedIds(prev=>{
      const next = new Set(prev);
      if(next.has(id)) next.delete(id); else next.add(id);
      return next;
    });
  }
  function selectAllVisible(){
    const ids = paginated.map(p=>p.id);
    setSelectedIds(prev=>{
      const allSelected = ids.every(id=>prev.has(id));
      if(allSelected){ const next=new Set(prev); ids.forEach(id=>next.delete(id)); return next; }
      return new Set([...prev, ...ids]);
    });
  }
  function clearSelection(){ setSelectedIds(new Set()); }

  async function executeBulk(){
    if(bulkInFlight) return;
    if(selectedIds.size === 0){ toast('Selecione ao menos um produto'); return; }
    if(!bulkModal?.op) return;
    if(bulkModal.requiresValue && !bulkValue.trim()){ toast('Informe o valor'); return; }
    const op = bulkModal.op;
    const value = bulkValue.trim();
    // Confirmação extra pra ações destrutivas
    if(op === 'archive'){
      const ok = await showConfirm({
        title:'⚠️ Arquivar '+selectedIds.size+' produtos',
        message:'Os produtos selecionados serão ARQUIVADOS (saem do catálogo, histórico preservado). Pode reverter editando. Confirma?',
        confirmText:'Arquivar '+selectedIds.size,
        confirmColor:'#DC2626'
      });
      if(!ok) return;
    } else if(op === 'price_pct' || op === 'price_absolute' || op === 'margin_min_pct'){
      const ok = await showConfirm({
        title:'💰 '+bulkModal.label+' em '+selectedIds.size+' produtos',
        message:'Esta operação altera o preço de '+selectedIds.size+' produtos. Auditoria fica gravada (admin pode reverter individualmente). Confirma?',
        confirmText:'Confirmar',
        confirmColor:'#EA580C'
      });
      if(!ok) return;
    }
    setBulkInFlight(true);
    try{
      const result = await window.bulkUpdateProducts({
        op, value: value||null,
        product_ids: Array.from(selectedIds)
      });
      if(!result.success){
        toast('❌ '+(result.errorMessage||'Erro na operação em massa'));
        return;
      }
      toast('✅ '+result.affected+' de '+result.requested+' produtos atualizados');
      setBulkModal(null);
      setBulkValue('');
      clearSelection();
      // Refresh products do banco (mais simples que aplicar mudanças localmente pra todas combinações)
      if(typeof window.refreshProducts === 'function') await window.refreshProducts(setProducts);
    } finally {
      setBulkInFlight(false);
    }
  }

  // Fase 4 — sinaliza globalmente qual produto está em edição para que applyRows
  // (Realtime/polling) não sobrescreva esse produto específico enquanto o modal está aberto.
  useEffect(()=>{
    const id=(modal==='edit'&&form&&form.id!=null)?form.id:null;
    window.__ZNX_PRODUCT_MODAL_ID__=id;
    return()=>{window.__ZNX_PRODUCT_MODAL_ID__=null;};
  },[modal,form?.id]);

  // Fase 5 — fecha modal quando outro Mac forçou reload via toast de conflito.
  useEffect(()=>{
    function onClose(){setModal(null);}
    window.addEventListener('znx:close-product-modal',onClose);
    return()=>window.removeEventListener('znx:close-product-modal',onClose);
  },[]);

  async function save(){
    // [FIX-A 20260511] Bloqueio anti-double-submit (regra_loading_state_obrigatorio).
    // Sem isso: 2 cliques rápidos no botão Salvar disparam 2 INSERTs paralelos
    // → unique constraint idx_products_brand_name_active_unique estoura no 2º.
    if(saveInflightRef.current){
      toast('⏳ Salvando, aguarde…');
      return;
    }
    saveInflightRef.current = true;
    setIsSaving(true);
    try {
      await _saveProductInner();
    } finally {
      saveInflightRef.current = false;
      setIsSaving(false);
    }
  }

  async function _saveProductInner(){
    // [SEC-001] Server-side role check — create/edit product
    if(!await znxGuard(['admin','estoquista']))return;
    const brand=(form.brand||'').trim();
    const productName=(form.productName||'').trim();
    const name=brand&&productName?`${brand} ${productName}`:(form.name||`${brand} ${productName}`).trim();
    const isNew=modal==='new';
    // [v224.53 FIX-5Q 2026-05-28] regra_falha_silenciosa_proibida
    // RPC update_product_v1 levantava 'invalid_payload' sem dizer QUAL campo faltou.
    // Sentry ZAYNEX-ERP-5Q: 3 events mesmo user em 13min (12:05/12:09/12:18 BRT).
    // Validação explícita ANTES do RPC com toast específico + foco no input vazio.
    if(isNew){
      if(!brand){toast('❌ Marca obrigatória. Preencha o campo Marca antes de salvar.');try{document.querySelector('input[name="brand"],input[placeholder*="arca"]')?.focus();}catch(_){}return;}
      if(!productName&&!form.name){toast('❌ Nome do produto obrigatório. Preencha o campo Nome.');try{document.querySelector('input[name="productName"],input[name="name"],input[placeholder*="ome"]')?.focus();}catch(_){}return;}
    }
    const id=isNew?genIdUUID():form.id;
    if(isNew){
      // BLOCO 5 — duplicate name check
      const nameExists=products.some(p=>p.name?.trim().toLowerCase()===name.trim().toLowerCase()&&p.id!==id);
      if(nameExists){toast('Ja existe um produto com este nome: '+name);return;}
    }
    // [BUG-FIX 20260506-stock-stale] Calcular stock e avg_cost com FRESH do banco (não do form).
    // Bug reportado por Jamal: vendi 96 e voltaram. Causa-raiz:
    //   1. Admin abre modal Editar Produto (form carrega stock=195 do JSONB local)
    //   2. Vendedor faz venda 96un → trigger decrementa stock no banco pra 99
    //   3. Admin clica Salvar sem mudar stock manualmente → frontend envia stock=195 (form stale)
    //   4. UPDATE products SET stock=195 sobrescreve o 99 → "voltou" 96 unidades
    // Histórico audit_log mostrou 42 ocorrências em 3 dias.
    //
    // Fix: stock só é enviado no payload em 2 cenários:
    //   A) Criação (isNew) — stock inicial vem do form
    //   B) Recebimento de lote (newQty>0 + newCost>0) — soma sobre stock FRESCO do banco
    //   C) Edit metadata (sem newQty) — NÃO envia stock (banco mantém valor real, isolado de cache stale)
    let stockFinal=null; // null = não inclui no payload
    let avgCostFinal=null;
    const nqty=Number(form.newQty)||0;
    const ncost=Number(form.newCost)||0;
    if(isNew){
      stockFinal=Number(form.stock)||0;
      avgCostFinal=Number(form.avgCost)||0;
      if(nqty>0&&ncost>0){
        avgCostFinal=stockFinal===0?ncost:Math.round(((stockFinal*avgCostFinal+nqty*ncost)/(stockFinal+nqty))*100)/100;
        stockFinal+=nqty;
      }
    }else if(nqty>0&&ncost>0){
      // Recebimento de lote em produto existente: pega stock FRESH do banco
      const{data:fresh,error:freshErr}=await sb.from('products').select('stock,avg_cost').eq('id',id).single();
      if(freshErr||!fresh){
        toast('❌ Erro ao buscar estoque atual: '+(freshErr?.message||'produto não encontrado'));
        return;
      }
      const stockBanco=Number(fresh.stock)||0;
      const avgCostBanco=Number(fresh.avg_cost)||0;
      avgCostFinal=stockBanco===0?ncost:Math.round(((stockBanco*avgCostBanco+nqty*ncost)/(stockBanco+nqty))*100)/100;
      stockFinal=stockBanco+nqty;
    }else{
      // [BUG-FIX 20260507 — 9AM Azul] Edit metadata sem lote novo:
      // detectar se admin MUDOU avg_cost ou stock manualmente vs snapshot original.
      // Se mudou intencionalmente, incluir no payload (admin only) com SELECT fresh defensivo.
      // Sem isso, ajuste manual de custo/estoque é silenciosamente ignorado.
      const snap = form._originalSnapshot || {};
      const formAvgCost = Number(form.avgCost)||0;
      const formStock = Number(form.stock)||0;
      const avgCostChanged = Math.abs(formAvgCost - (snap.avgCost||0)) > 0.005; // tolerância 0.5 cent
      const stockChanged = formStock !== (snap.stock||0);
      if(avgCostChanged || stockChanged){
        // Confirma com admin que é ajuste intencional (evita F5 acidental de form stale)
        if(user.role !== 'admin'){
          toast('⚠ Apenas admin pode ajustar custo/estoque manualmente. Use "Receber lote" pra entradas regulares.');
          return;
        }
        // SELECT fresh do banco pra avisar se há divergência grande (defesa stale)
        const{data:fresh}=await sb.from('products').select('stock,avg_cost').eq('id',id).single();
        if(fresh){
          const stockDiff = Math.abs(formStock - (Number(fresh.stock)||0));
          // Se diff de stock > 5 e admin não mexeu no input → form stale, NÃO sobrescreve
          if(!stockChanged && stockDiff > 5){
            console.warn('[ZNX] Stock no form (' + formStock + ') divergente do banco (' + fresh.stock + ') — preservando banco');
          } else if(stockChanged){
            // Admin mexeu intencionalmente — confirmar via prompt antes de sobrescrever
            const confirmMsg = `Você está alterando o estoque de ${snap.stock} para ${formStock} unidades.\n\nEstoque atual no banco: ${fresh.stock}\n\nConfirma este ajuste manual?`;
            if(!await showConfirm({title:'⚠️ Ajuste manual de estoque',message:confirmMsg,confirmText:'Confirmar ajuste',confirmColor:'#EA580C'})){
              return;
            }
            stockFinal = formStock;
          }
          if(avgCostChanged){
            avgCostFinal = formAvgCost;
          }
        }
      }
    }
    // [Onda L Fase 1 — 20260507] Substitui dualWrite split por RPC update_product_v1 atomic.
    // Trigger products_price_history grava audit estruturado com source='manual_edit' (default).
    // Stoque/custo só entram no payload se foram calculados (lote novo) OU mudança intencional admin.
    const supplierIdSafe=(form.supplierId&&/^[0-9a-f-]{36}$/i.test(form.supplierId))?form.supplierId:null;
    const rpcPayload={
      is_new: isNew,
      product_id: id,
      name, product_name:productName, brand,
      code: form.code?.trim()||null,
      sale_price: Number(form.salePrice),
      active: form.active!==false,
      // [BUG-FIX 20260515 ORC-0609] Auto-detecta DECANTE pelo brand — não depende mais
      // de admin lembrar de marcar checkbox OU setar categoria='Decants'.
      // Defesa em depth: 4 caminhos pra is_decant=true (manual, alias isDecant, categoria, brand).
      is_decant: form.is_decant===true||form.isDecant===true||(form.categoria==='Decants')||(String(brand||'').toUpperCase().startsWith('DECANTE')),
      notes: form.notes?.trim()||null,
      genero: form.genero||null,
      concentracao: form.concentracao||null,
      categoria: form.categoria||null,
      volume: form.volume||null,
      estoque_min: Number(form.estoqueMin)||20,
      supplier_id: supplierIdSafe,
      ncm: form.ncm?.trim()||null,
      ean: form.ean?.trim()||null,
      tags: Array.isArray(form.tags) ? form.tags : [],
      source: nqty>0 ? 'stock_entry' : 'manual_edit',
      notes_audit: nqty>0 ? `Recebimento de lote: +${nqty} un. a ${ncost.toFixed(2)}` : 'Edição manual produto'
    };
    if(stockFinal!==null)rpcPayload.stock=stockFinal;
    if(avgCostFinal!==null)rpcPayload.avg_cost=avgCostFinal;

    const result = await window.updateProductAtomic(rpcPayload);
    if(!result.success){
      const msg = (typeof mapErrorToUX==='function' ? mapErrorToUX(result.errorCode, result.errorMessage) : null)
                || result.errorMessage || 'Erro ao salvar produto';
      toast('❌ ' + msg);
      // [ONDA-S B 2026-05-13] expected codes → level=info
      if(typeof window.znxCaptureRpcError==='function'){
        window.znxCaptureRpcError('updateProductAtomic',result.errorCode||'failed',{productId:id});
      } else if(typeof Sentry!=='undefined'){
        Sentry.captureException(new Error('updateProductAtomic failed'),{extra:{productId:id,errorCode:result.errorCode}});
      }
      return;
    }
    // Sync state local com retorno autoritativo do banco
    const fresh = result.product || {};
    if(isNew){
      setProducts(prev=>[...prev, {
        ...form, id: result.product_id || id, name, brand, productName,
        stock: fresh.stock||0, salePrice: Number(fresh.sale_price)||0, avgCost: Number(fresh.avg_cost)||0,
        supplierId: fresh.supplier_id, active: fresh.active, updatedAt: fresh.updated_at
      }]);
    } else {
      setProducts(prev=>prev.map(p=>p.id===id?{
        ...p, ...form, name, brand, productName,
        stock: fresh.stock !== undefined ? Number(fresh.stock) : p.stock,
        salePrice: Number(fresh.sale_price)||p.salePrice,
        avgCost: Number(fresh.avg_cost)||p.avgCost,
        avg_cost: Number(fresh.avg_cost)||p.avgCost,
        supplierId: fresh.supplier_id||p.supplierId,
        active: fresh.active,
        updatedAt: fresh.updated_at
      }:p));
    }
    setModal(null);
    if(nqty>0) toast('✅ Lote de '+nqty+' un. adicionado. Estoque atualizado.');
    else toast('✅ Produto salvo.');
  }

  async function del(id){
    // [SEC-001] Server-side role check — delete product
    if(!await znxGuard(['admin']))return;
    const inSales=sales.some(s=>s.status!=='Cancelada'&&(s.items||[]).some(it=>nid(it.productId,id)));
    const inQuotes=quotes.some(q=>q.status!=='Convertido'&&q.status!=='Cancelado'&&(q.items||[]).some(it=>nid(it.productId,id)));
    const hasHistory=inSales||inQuotes;
    const confirmMsg=hasHistory
      ?'Este produto tem histórico de '+(inSales?'vendas':'')+(inSales&&inQuotes?' e ':'')+(inQuotes?'orçamentos':'')+' registrados.\n\nAo arquivar, o histórico é preservado. O produto não aparecerá mais no catálogo nem em novos orçamentos/vendas.'
      :'Excluir produto definitivamente?';
    if(await showConfirm({title:hasHistory?'Arquivar Produto':'Excluir Produto',message:confirmMsg,confirmText:hasHistory?'Arquivar mesmo assim':'Excluir',confirmColor:hasHistory?'#D97706':'#DC2626'})){
      // Fase 4 — soft-delete. Grava no relacional PRIMEIRO via dualWrite, depois JSONB/state.
      const deletedAt=new Date().toISOString();
      const ok=await dualWrite('products',id,{deleted_at:deletedAt,updated_at:deletedAt,active:false},false,()=>{
        setProducts(prev=>prev.map(p=>p.id===id?{...p,deletedAt,updatedAt:deletedAt,active:false}:p));
      });
      if(ok)toast(hasHistory?'Produto arquivado (histórico preservado).':'Produto excluído.');
    }
  }

  // [Wave 25-alt v224.6] productsWithLoss + custoPreview delegados pra calcs factories
  const productsWithLoss = useMemo(()=>produtosCalcs.filterProductsWithLoss(products),[products]);
  const custoPreview = useMemo(()=>produtosCalcs.computeCustoPreview(form),[form.newQty,form.newCost,form.stock,form.avgCost]);

  // [Wave 7 KIMI 2026-05-17] Pagination extraído para widgets/produtos/Pagination.jsx
  // [Wave 7 KIMI 2026-05-17] gerarCatalogoPDF extraído para lib/produtos/catalogoPdf.js (factory async)

  return(
    <div>
      {/* [Onda P1] TabBar */}
      <Tabs
        tabs={[
          {id:'lista',label:'📋 Lista',count:products.length},
          {id:'insights',label:'📊 Insights',count:0,tip:'Top, ranking, estoque, marcas, sugestões IA'},
          {id:'360',label:'🎯 Produto 360º',count:selected360?1:0,disabled:!selected360,tip:'Health Score, top clientes, sazonalidade, cross-sell'}
        ]}
        active={activeTab}
        onChange={setActiveTab}
      />

      {/* ═══════════════════════ ABA LISTA ═══════════════════════ */}
      {activeTab==='lista'&&<>

      {/* [Wave 25-alt v224.6] 3 blocos extraídos: LossBanner + BulkToolbar + ProductFiltersBar */}
      {LossBanner && (
        <LossBanner
          isAdmin={isAdmin}
          productsWithLoss={productsWithLoss}
          onClearAndFilterLoss={()=>{setFilterStatus('prejuizo');setSearch('');setFilterBrand('');setFilterSupp('');setFilterGenero('');setFilterCategoria('');}}
        />
      )}

      {BulkToolbar && (
        <BulkToolbar
          isAdmin={isAdmin}
          selectedIds={selectedIds}
          setBulkModal={setBulkModal}
          clearSelection={clearSelection}
        />
      )}

      {ProductFiltersBar && (
        <ProductFiltersBar
          products={products} filtered={filtered}
          search={search} setSearch={setSearch}
          ftsLoading={ftsLoading} ftsIds={ftsIds}
          filterBrand={filterBrand} setFilterBrand={setFilterBrand} allBrands={allBrands}
          filterSupp={filterSupp} setFilterSupp={setFilterSupp} suppliers={suppliers}
          filterStatus={filterStatus} setFilterStatus={setFilterStatus} isAdmin={isAdmin}
          filterGenero={filterGenero} setFilterGenero={setFilterGenero}
          filterCategoria={filterCategoria} setFilterCategoria={setFilterCategoria}
          hideConfidential={hideConfidential} setHideConfidential={setHideConfidential}
          onOpenNew={openNew}
          onOpenCatalog={()=>{setCatOpts({brands:[],categoria:'',genero:'',includePrice:true,includeStock:false});setCatalogModal(true)}}
          userRole={user.role}
        />
      )}

      <div className="card" style={{padding:0}}>
        <table>
          <thead><tr>
            {isAdmin && (
              <th style={{width:32,textAlign:'center'}}>
                <input type="checkbox"
                  checked={paginated.length>0 && paginated.every(p=>selectedIds.has(p.id))}
                  onChange={selectAllVisible}
                  title="Selecionar todos visíveis"
                  style={{cursor:'pointer'}}/>
              </th>
            )}
            <SortTh col="brand">Marca</SortTh>
            <SortTh col="name">Produto</SortTh>
            {showCode&&<th>Código</th>}
            {showSupplier&&<th>Fornecedor</th>}
            <SortTh col="stock">Estoque</SortTh>
            {/* [v224.40 F] 2 cols qty Alfonso+Senador · só se warehouses fetched */}
            {alfonsoWh && <th style={{textAlign:'right',whiteSpace:'nowrap',color:'#B89840',fontWeight:600}}>Alfonso</th>}
            {senadorWh && <th style={{textAlign:'right',whiteSpace:'nowrap',color:'#1B2A4A',fontWeight:600}}>Senador</th>}
            {showCost&&<SortTh col="avgCost">Custo Médio</SortTh>}
            <SortTh col="salePrice">Preço Venda</SortTh>
            {showCost&&<th title="Margem = (Preço − Custo) ÷ Preço">Margem</th>}
            <SortTh col="status">Status</SortTh><th>Ações</th>
          </tr></thead>
          <tbody>
            {paginated.map(p=>{
              const sup=suppliers.find(s=>s.id===p.supplierId);
              const brand=p.brand||extractBrand(p.name);
              const pname=p.productName||(p.name.startsWith(brand)?p.name.slice(brand.length).trim():p.name);
              const loss=produtosCalcs.isLossSelling(p);
              return(
                <tr key={p.id} style={loss?{background:'#FEF2F2',borderLeft:'3px solid #DC2626'}:(selectedIds.has(p.id)?{background:'#EFF6FF'}:undefined)}>
                  {isAdmin && (
                    <td style={{textAlign:'center'}}>
                      <input type="checkbox"
                        checked={selectedIds.has(p.id)}
                        onChange={()=>toggleSelect(p.id)}
                        style={{cursor:'pointer'}}/>
                    </td>
                  )}
                  <td><span style={{color:'#2563EB',fontWeight:600,fontSize:12}}>{brand}</span></td>
                  <td style={{fontWeight:500,maxWidth:240}}>
                    {loss && isAdmin && (
                      <span title={`Vendendo com prejuízo — Custo R$ ${Number(p.avgCost||0).toFixed(2)} ≥ Preço R$ ${Number(p.salePrice||0).toFixed(2)}`}
                        style={{fontSize:14,marginRight:6,cursor:'help'}}>⚠️</span>
                    )}
                    {pname}{p.volume&&<span style={{marginLeft:6,fontSize:11,color:'#9CA3AF',fontWeight:400,background:'#F3F4F6',borderRadius:4,padding:'1px 5px'}}>{p.volume}{String(p.volume).toLowerCase().includes('ml')?'':'ml'}</span>}
                  </td>
                  {showCode&&<td className="dim" style={{fontSize:11}}>{p.code}</td>}
                  {showSupplier&&<td className="dim">{sup?.name||'—'}</td>}
                  <td>
                    {/* [FURO #513 FIX 2026-05-15 v220.1] Inline edit DESABILITADO.
                        Causava drift products.stock vs SUM(pws.quantity) — admin editava direto
                        e pws ficava desync (12 produtos drift descobertos no pre-flight).
                        Solução: read-only + tooltip direcionando pra Depósitos > Ajuste de Inventário.
                        Função saveInlineStock mantida com guard interno (defesa em profundidade).
                        [v223.36 FEAT-STOCK-ADMIN 20260520] Botão 🔧 ajuste via depósito SÓ pra Jamal admin.
                        Backend RPC warehouse_stock_adjust mantém invariante products.stock = SUM(pws) via trigger. */}
                    <div style={{display:'flex',alignItems:'center',gap:6}}>
                      <strong
                          onClick={()=>{if(isAdmin && !isAdminJamal)toast('💡 Pra ajustar estoque, vá em Depósitos > Ajuste de Inventário (mantém pws sincronizado).');}}
                          title={isAdmin && !isAdminJamal?'Pra ajustar estoque, use Depósitos > Ajuste de Inventário':''}
                          style={isAdmin && !isAdminJamal?{cursor:'help',padding:'2px 8px',borderRadius:6,background:'#F0F2F5',display:'inline-block',minWidth:32,textAlign:'center'}:{padding:'2px 8px',borderRadius:6,background:'#F0F2F5',display:'inline-block',minWidth:32,textAlign:'center'}}>
                        {p.stock}
                      </strong>
                      {isAdminJamal && (
                        <button onClick={()=>setAdjustStockProduct(p)} title="Ajustar estoque por depósito (admin)"
                          style={{background:'#FEF3C7',border:'1px solid #C8A95155',borderRadius:4,padding:'2px 6px',cursor:'pointer',fontSize:12}}>
                          🔧
                        </button>
                      )}
                    </div>
                  </td>
                  {/* [v224.40 F] qty Alfonso (cor: zero=vermelho · ≤20=laranja · else=verde) */}
                  {alfonsoWh && (()=>{
                    const bd = p.stock_breakdown || p.stockBreakdown || {};
                    const qa = bd[alfonsoWh.id] || 0;
                    return <td style={{textAlign:'right',fontWeight:600,fontSize:13,color:qa===0?'#DC2626':(qa<=20?'#EA580C':'#15803D'),fontVariantNumeric:'tabular-nums'}}>{qa}</td>;
                  })()}
                  {/* [v224.40 F] qty Senador (mesma escala cor) */}
                  {senadorWh && (()=>{
                    const bd = p.stock_breakdown || p.stockBreakdown || {};
                    const qs = bd[senadorWh.id] || 0;
                    return <td style={{textAlign:'right',fontWeight:600,fontSize:13,color:qs===0?'#DC2626':(qs<=20?'#EA580C':'#15803D'),fontVariantNumeric:'tabular-nums'}}>{qs}</td>;
                  })()}
                  {showCost&&<td className="gold">{fmt(p.avgCost)}</td>}
                  <td>{fmt(p.salePrice)}</td>
                  {showCost && (()=>{
                    const cost=Number(p.avgCost||0);
                    const price=Number(p.salePrice||0);
                    if(cost<=0||price<=0) return <td className="dim" style={{fontSize:11}}>—</td>;
                    const marginPct=((price-cost)/price)*100;
                    const color=marginPct<=0?'#DC2626':marginPct<10?'#EA580C':marginPct<25?'#D97706':marginPct<40?'#16A34A':'#059669';
                    return <td style={{fontWeight:600,color,fontSize:13}}>{marginPct.toFixed(1)}%</td>;
                  })()}
                  <td>{produtosCalcs.stockBadge(p, sales)}</td>
                  <td>
                    <div style={{display:'flex',gap:6}}>
                      <button className="btn-outline btn-sm" title="Produto 360º" onClick={()=>{setSelected360(p);setActiveTab('360');}} style={{borderColor:'#C8A951',color:'#92700A'}}>🎯</button>
                      {(isAdmin||user.role==='estoquista')&&<button className="btn-outline btn-sm" onClick={()=>openEdit(p)} title="Editar"><Icon n="edit" size={12}/></button>}
                      {(isAdmin||user.role==='estoquista')&&<button className="btn-outline btn-sm" onClick={()=>openDuplicate(p)} title="Duplicar produto" style={{borderColor:'#7C3AED',color:'#7C3AED'}}>📋</button>}
                      {isAdmin&&<button className="btn-danger btn-sm" onClick={()=>del(p.id).catch(e=>console.error('[ZNX] del product error:',e))} title="Excluir"><Icon n="trash" size={12}/></button>}
                    </div>
                  </td>
                </tr>
              );
            })}
            {/* [L2-Produtos 2026-05-09] Empty state inteligente - diferencia filtros ativos vs base vazia */}
            {paginated.length===0&&(()=>{
              const activeFilters=[search&&'busca',filterBrand&&'marca',filterSupp&&'fornecedor',filterStatus&&'status',filterGenero&&'gênero',filterCategoria&&'categoria'].filter(Boolean);
              const hasFilters=activeFilters.length>0;
              const colspanCount=isAdmin?14:11;
              return(
                <tr><td colSpan={colspanCount} style={{textAlign:'center',padding:50}}>
                  {hasFilters?(
                    <>
                      <div style={{fontSize:38,marginBottom:10}}>🔍</div>
                      <div style={{fontSize:15,fontWeight:600,color:'#374151',marginBottom:6}}>Nenhum produto bate com os filtros</div>
                      <div style={{fontSize:12,color:'#6B7280',marginBottom:14}}>Filtros ativos: <strong>{activeFilters.join(' · ')}</strong></div>
                      <button onClick={()=>{setSearch('');setFilterBrand('');setFilterSupp('');setFilterStatus('');setFilterGenero('');setFilterCategoria('');setPage(1);}}
                        style={{padding:'9px 22px',background:'#1B2A4A',color:'#fff',border:'none',borderRadius:6,fontSize:12,fontWeight:600,cursor:'pointer'}}>
                        ↻ Limpar todos os filtros
                      </button>
                    </>
                  ):products.length===0?(
                    <>
                      <div style={{fontSize:38,marginBottom:10}}>📦</div>
                      <div style={{fontSize:15,fontWeight:600,color:'#374151',marginBottom:6}}>Nenhum produto cadastrado</div>
                      <div style={{fontSize:12,color:'#6B7280',marginBottom:14}}>Comece cadastrando seu primeiro produto.</div>
                      {(isAdmin||user.role==='estoquista')&&(
                        <button onClick={openNew}
                          style={{padding:'9px 22px',background:'#B89840',color:'#fff',border:'none',borderRadius:6,fontSize:12,fontWeight:600,cursor:'pointer'}}>
                          ➕ Cadastrar primeiro produto
                        </button>
                      )}
                    </>
                  ):(
                    <div style={{fontSize:13,color:'#6B7280',padding:20}}>Nenhum produto nesta página. Volte ao início da paginação.</div>
                  )}
                </td></tr>
              );
            })()}
          </tbody>
        </table>
        <Pagination page={page} totalPages={totalPages} setPage={setPage} PAGE_SIZE={PAGE_SIZE} sortedLength={sorted.length}/>
      </div>
      </>}

      {/* ═══════════════════════ ABA INSIGHTS (Onda P2) ═══════════════════════ */}
      {activeTab==='insights'&&typeof ProdutosInsightsTab!=='undefined'&&(
        <ProdutosInsightsTab products={products} sales={sales} clients={clients||[]}/>
      )}

      {/* ═══════════════════════ ABA 360º (Onda P4) ═══════════════════════ */}
      {activeTab==='360'&&(
        selected360
          ?<ProdutoTimeline
              product={selected360}
              products={products}
              sales={sales}
              clients={clients||[]}
              user={user}
              onBack={()=>{setSelected360(null);setActiveTab('lista');}}
            />
          :<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}}>Nenhum produto selecionado</div>
            <div style={{fontSize:13,maxWidth:520,margin:'0 auto',lineHeight:1.6}}>
              Volte pra <button className="btn-outline btn-sm" onClick={()=>setActiveTab('lista')}>Lista</button> e clique no 🎯 ao lado de um produto.
            </div>
          </div>
      )}

      {/* [Wave 25-alt v224.6] Modal extraído pra ProductFormModal · save handler permanece aqui */}
      {modal && ProductFormModal && (
        <ProductFormModal
          modal={modal}
          form={form} setForm={setForm}
          allBrands={allBrands}
          suppliers={suppliers}
          custoPreview={custoPreview}
          isSaving={isSaving}
          onSave={save}
          onClose={()=>setModal(null)}
        />
      )}


      {/* [Wave 7 KIMI 2026-05-17] Modal Catálogo PDF extraído pra widgets/produtos/CatalogoPdfModal.jsx */}
      {catalogModal && CatalogoPdfModal && (
        <CatalogoPdfModal
          allBrands={allBrands}
          catOpts={catOpts}
          setCatOpts={setCatOpts}
          products={products}
          onClose={()=>setCatalogModal(false)}
          onGenerate={()=>produtosCatalogoPdf.gerarCatalogoPDF({products, catOpts, onClose:()=>setCatalogModal(false)})}
        />
      )}

      {/* [Wave 7 KIMI 2026-05-17] Modal bulk operation extraído pra widgets/produtos/BulkActionModal.jsx */}
      {bulkModal && BulkActionModal && (
        <BulkActionModal
          bulkModal={bulkModal}
          selectedIdsSize={selectedIds.size}
          setBulkModal={setBulkModal}
          bulkValue={bulkValue}
          setBulkValue={setBulkValue}
          bulkInFlight={bulkInFlight}
          suppliers={suppliers}
          executeBulk={executeBulk}
        />
      )}

      {/* [v223.36 FEAT-STOCK-ADMIN 20260520] Modal ajuste rápido estoque admin Jamal (warehouse_stock_adjust RPC) */}
      {adjustStockProduct && AjusteEstoqueRapidoModal && (
        <AjusteEstoqueRapidoModal
          product={adjustStockProduct}
          onClose={()=>setAdjustStockProduct(null)}
          onSuccess={()=>setAdjustStockProduct(null)}
        />
      )}
    </div>
  );
}

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

  window.ZNX.refactor_phase_6_loaded = window.ZNX.refactor_phase_6_loaded || {};
  window.ZNX.refactor_phase_6_loaded.Produtos = true;
  // [Wave 25-alt marker v224.6] confirma extract executado
  window.Produtos_v224_6_wave25alt = true;

})();
