/* global React, Icon, BRL, NUM, Ticker, Drawer */ function MPdv({ onNav }) { const [tab, setTab] = React.useState('pdv'); const [sku, setSku] = React.useState(null); const [skuTab, setSkuTab] = React.useState('det'); const [caixaAberto, setCaixaAberto] = React.useState(true); const [showEncerrar, setShowEncerrar] = React.useState(false); const [produtoForm, setProdutoForm] = React.useState(null); // { mode: 'add' } | { mode: 'edit', produto } const [produtoToDelete, setProdutoToDelete] = React.useState(null); const [fornecedorForm, setFornecedorForm] = React.useState(null); const [fornecedorToDelete, setFornecedorToDelete] = React.useState(null); const [clientes, setClientes] = React.useState([]); const [loadingClientes, setLoadingClientes] = React.useState(true); const [clienteForm, setClienteForm] = React.useState(null); const [clienteToDelete, setClienteToDelete] = React.useState(null); const [clienteOpen, setClienteOpen] = React.useState(null); const [clienteTab, setClienteTab] = React.useState('det'); const [toast, setToast] = React.useState(null); const [produtos, setProdutos] = React.useState([]); const [fornecedores, setFornecedores] = React.useState([]); const [loadingProdutos, setLoadingProdutos] = React.useState(true); const [loadingFornecedores, setLoadingFornecedores] = React.useState(true); const [loadError, setLoadError] = React.useState(null); const showToast = (msg, ms = 4000) => { setToast(msg); setTimeout(() => setToast(null), ms); }; const reloadProdutos = React.useCallback(async () => { try { setLoadError(null); const rows = await window.dataApi.listProdutos(); setProdutos(rows); } catch (e) { setLoadError(e?.message || 'Falha ao carregar produtos'); } finally { setLoadingProdutos(false); } }, []); const reloadFornecedores = React.useCallback(async () => { try { const rows = await window.dataApi.listFornecedores(); setFornecedores(rows); } catch (e) { // ignora silencioso (já há erro de produtos) } finally { setLoadingFornecedores(false); } }, []); const reloadClientes = React.useCallback(async () => { try { const rows = await window.dataApi.listClientes(); setClientes(rows); } catch (e) { // ignora } finally { setLoadingClientes(false); } }, []); React.useEffect(() => { reloadProdutos(); reloadFornecedores(); reloadClientes(); }, [reloadProdutos, reloadFornecedores, reloadClientes]); const handleSaveProduto = async (form) => { const editing = produtoForm?.mode === 'edit'; try { if (editing) { await window.dataApi.updateProduto(produtoForm.produto.id, form); await reloadProdutos(); setProdutoForm(null); showToast(`Produto ${form.cod} atualizado`); } else { const created = await window.dataApi.insertProduto(form); await reloadProdutos(); setProdutoForm(null); showToast(`Produto ${created.cod} cadastrado · ${created.desc}`); } } catch (e) { const msg = e?.message || String(e); if (/duplicate|unique/i.test(msg)) throw new Error('Já existe um produto com este código.'); throw e; } }; const handleDeleteProduto = async () => { await window.dataApi.deleteProduto(produtoToDelete.id); await reloadProdutos(); const cod = produtoToDelete.cod; setProdutoToDelete(null); showToast(`Produto ${cod} excluído`); }; const handleSaveFornecedor = async (form) => { const editing = fornecedorForm?.mode === 'edit'; try { if (editing) { await window.dataApi.updateFornecedor(fornecedorForm.fornecedor.id, form); await reloadFornecedores(); setFornecedorForm(null); showToast(`Fornecedor ${form.nome} atualizado`); } else { const created = await window.dataApi.insertFornecedor(form); await reloadFornecedores(); setFornecedorForm(null); showToast(`Fornecedor ${created.nome} cadastrado`); } } catch (e) { const msg = e?.message || String(e); if (/duplicate|unique/i.test(msg)) throw new Error('Já existe um fornecedor com este CNPJ.'); throw e; } }; const handleSaveCliente = async (form) => { const editing = clienteForm?.mode === 'edit'; try { if (editing) { await window.dataApi.updateCliente(clienteForm.cliente.id, form); await reloadClientes(); setClienteForm(null); showToast(`Cliente ${form.nome} atualizado`); } else { const created = await window.dataApi.insertCliente(form); await reloadClientes(); setClienteForm(null); showToast(`Cliente ${created.nome} cadastrado`); } } catch (e) { const msg = e?.message || String(e); if (/duplicate|unique/i.test(msg)) throw new Error('Já existe um cliente com este CNPJ.'); throw e; } }; const handleDeleteCliente = async () => { await window.dataApi.deleteCliente(clienteToDelete.id); await reloadClientes(); const nome = clienteToDelete.nome; setClienteToDelete(null); showToast(`Cliente ${nome} excluído`); }; const handleDeleteFornecedor = async () => { try { await window.dataApi.deleteFornecedor(fornecedorToDelete.id); await reloadFornecedores(); await reloadProdutos(); // pode ter mexido em fornecedor_principal_id de produtos const nome = fornecedorToDelete.nome; setFornecedorToDelete(null); showToast(`Fornecedor ${nome} excluído`); } catch (e) { throw e; } }; return ( <>
Operação Loja FísicaM11

PDV · Estoque · NF Manual

BTM Serviços e Comércio LTDA · CNPJ 33.804.351/0001-04 · operação varejo + emissão NF-e por produto/cliente cadastrado.

{caixaAberto ? `CAIXA ABERTO · ${(window.sglUser?.displayName || 'OPERADOR').toUpperCase()}` : 'CAIXA FECHADO'}
{loadError && (
Falha ao carregar produtos: {loadError}
)}
setTab('pdv')}>PDV (Venda)
setTab('est')}>Estoque {loadingProdutos ? '...' : produtos.length}
setTab('forn')}>Fornecedores {loadingFornecedores ? '...' : fornecedores.length}
setTab('cli')}>Clientes {loadingClientes ? '...' : clientes.length}
setTab('nf')}>NF Manual
setTab('fech')}>Fechamento de Caixa
{tab === 'pdv' && } {tab === 'est' && { setSku(s); setSkuTab('det'); }} onAdd={() => setProdutoForm({ mode: 'add' })} onEdit={(p) => setProdutoForm({ mode: 'edit', produto: p })} onDelete={(p) => setProdutoToDelete(p)} />} {tab === 'forn' && setFornecedorForm({ mode: 'add' })} onEdit={(f) => setFornecedorForm({ mode: 'edit', fornecedor: f })} onDelete={(f) => setFornecedorToDelete(f)} />} {tab === 'cli' && setClienteForm({ mode: 'add' })} onOpen={(c) => { setClienteOpen(c); setClienteTab('det'); }} onEdit={(c) => setClienteForm({ mode: 'edit', cliente: c })} onDelete={(c) => setClienteToDelete(c)} />} {tab === 'nf' && } {tab === 'fech' && setShowEncerrar(true)} />}
{sku && setSku(null)} tab={skuTab} onTab={setSkuTab} onNav={onNav} onReload={reloadProdutos} showToast={showToast} />} {produtoForm && ( setProdutoForm(null)} onSave={handleSaveProduto} /> )} {produtoToDelete && ( setProdutoToDelete(null)} onConfirm={handleDeleteProduto} /> )} {fornecedorForm && ( setFornecedorForm(null)} onSave={handleSaveFornecedor} /> )} {fornecedorToDelete && ( setFornecedorToDelete(null)} onConfirm={handleDeleteFornecedor} /> )} {clienteForm && ( setClienteForm(null)} onSave={handleSaveCliente} /> )} {clienteToDelete && ( setClienteToDelete(null)} onConfirm={handleDeleteCliente} /> )} {clienteOpen && ( setClienteOpen(null)} onNav={onNav} onEdit={() => { setClienteForm({ mode: 'edit', cliente: clienteOpen }); setClienteOpen(null); }} onDelete={() => { setClienteToDelete(clienteOpen); setClienteOpen(null); }} /> )} {showEncerrar && ( setShowEncerrar(false)} onConfirm={() => { setShowEncerrar(false); setCaixaAberto(false); showToast('Caixa encerrado · relatório do dia gerado · sangria de R$ 487,30 protocolada · turno Maria fechado às ' + new Date().toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' })); }} /> )} {toast && (
{toast}
)} ); } function ConfirmModal({ titulo, texto, confirmLabel, danger, onClose, onConfirm }) { return (
e.stopPropagation()}>
{titulo}
{texto}
); } function PDVView({ produtos }) { const [cart, setCart] = React.useState([]); const [busca, setBusca] = React.useState(''); const total = cart.reduce((s, i) => s + i.qt * i.vu, 0); const inputRef = React.useRef(null); const handleBuscar = (e) => { if (e.key !== 'Enter' || !busca.trim()) return; const q = busca.trim().toLowerCase(); const p = produtos.find(x => (x.ean && x.ean.toLowerCase() === q) || x.cod.toLowerCase() === q || x.desc.toLowerCase().includes(q) ); if (!p) return; setCart(prev => { const idx = prev.findIndex(it => it.sku === p.sku); if (idx >= 0) { const nx = [...prev]; nx[idx] = { ...nx[idx], qt: nx[idx].qt + 1 }; return nx; } return [...prev, { sku: p.sku, desc: p.desc, qt: 1, vu: p.preco || p.custo }]; }); setBusca(''); inputRef.current?.focus(); }; return (
Buscar produto · código de barras ou descrição
setBusca(e.target.value)} onKeyDown={handleBuscar} style={{ fontSize: 14, padding: '10px 12px' }} autoFocus />
{cart.length === 0 && (
Carrinho vazio
Use o leitor de código de barras ou busque um produto acima.
)} {cart.length > 0 && ( {cart.map((it, i) => ( ))}
SKUDescriçãoQtdVl.Un.Subtotal
{it.sku} {it.desc} {it.qt} {BRL(it.vu)} {BRL(it.qt * it.vu)}
)}
TOTAL
{BRL(total)}
{cart.length} itens · {cart.reduce((s, i) => s + i.qt, 0)} un.
Forma de pagamento
{[ { i: 'dollar', l: 'Dinheiro' }, { i: 'dollar', l: 'PIX' }, { i: 'dollar', l: 'Débito' }, { i: 'dollar', l: 'Crédito' }, ].map(p => ( ))}
Cupom / NFC-e
); } function EstoqueView({ produtos, loading, onOpen, onAdd, onEdit, onDelete }) { const [busca, setBusca] = React.useState(''); const filtrados = React.useMemo(() => { if (!busca.trim()) return produtos; const q = busca.trim().toLowerCase(); return produtos.filter(p => (p.sku && p.sku.toLowerCase().includes(q)) || p.cod.toLowerCase().includes(q) || p.desc.toLowerCase().includes(q) || (p.cat && p.cat.toLowerCase().includes(q)) ); }, [produtos, busca]); const abaixoMin = produtos.filter(s => s.st < s.min).length; const valorEstoque = produtos.reduce((a, s) => a + s.st * s.custo, 0); const giroMedio = produtos.length > 0 ? Math.round(produtos.reduce((a, s) => a + (s.giro || 0), 0) / produtos.length) : 0; return (
0 ? 'amber' : 'lime'} />
Estoque por SKU
clique para detalhes
setBusca(e.target.value)} style={{ width: 240 }} />
{loading && ( )} {!loading && filtrados.length === 0 && ( )} {!loading && filtrados.map(r => { const baixo = r.st < r.min; const atencao = r.st < r.min * 1.5; return ( onOpen(r)} style={{ cursor: 'pointer' }}> ); })}
SKU Descrição Categoria Estoque Mínimo Movim. 30d Status Ações
Carregando produtos do Supabase...
{produtos.length === 0 ? 'Nenhum produto cadastrado. Use o cadastro de produtos para começar.' : 'Nenhum produto encontrado para a busca.'}
{r.sku}
{r.desc}
{r.cod}
{r.cat} {NUM(r.st)} {r.min} +{r.mov30.entr} / -{r.mov30.said} {baixo ? REPOR : atencao ? ATENÇÃO : OK} e.stopPropagation()}>
); } function KpiSmall({ l, v, sub, cls }) { return (
{l}
{v}
{sub}
); } function NFManualView() { return (
Emissão NF-e Manual
CLIENTE (CNPJ/CPF)
NATUREZA OPERAÇÃO
CFOP
PRODUTO
QTD
VL.UN
DESCONTO
Preview da NF-e
BTM SERVIÇOS E COMÉRCIO LTDA — CNPJ 33.804.351/0001-04
NÚMERO002848
SÉRIE1
CHAVE3526 0512 3456 7800 0190
VALORES
); } function Row({ k, v, bold }) { return
{k}{v}
; } function FechView({ caixaAberto, onEncerrar }) { return (
Fechamento de Caixa · {new Date().toLocaleDateString('pt-BR')} · {window.sglUser?.displayName || 'Operador'}
{caixaAberto ? 'CAIXA ABERTO' : 'CAIXA FECHADO'}
MOVIMENTAÇÃO POR FORMA
RESUMO OPERACIONAL
{caixaAberto ? ( ) : (
Caixa encerrado
Relatório do turno gerado · sangria protocolada · operadora deslogada. Para reabrir, use o login do PDV.
)}
); } /* ── FORNECEDORES VIEW ─────────────────────────────────────── */ function FornecedoresView({ fornecedores, loading, onAdd, onEdit, onDelete }) { const [busca, setBusca] = React.useState(''); const filtrados = React.useMemo(() => { if (!busca.trim()) return fornecedores; const q = busca.trim().toLowerCase(); return fornecedores.filter(f => f.nome.toLowerCase().includes(q) || (f.cnpj && f.cnpj.toLowerCase().includes(q)) || (f.email && f.email.toLowerCase().includes(q)) ); }, [fornecedores, busca]); return (
Fornecedores cadastrados
{loading ? '...' : `${fornecedores.length} registros · ${fornecedores.filter(f => f.ativo).length} ativos`}
setBusca(e.target.value)} style={{ width: 260 }} />
{loading && ( )} {!loading && filtrados.length === 0 && ( )} {!loading && filtrados.map(f => ( ))}
Nome CNPJ E-mail Telefone Status Cadastrado em Ações
Carregando fornecedores...
{fornecedores.length === 0 ? 'Nenhum fornecedor cadastrado. Clique em "Novo fornecedor" para começar.' : 'Nenhum resultado para a busca.'}
{f.nome} {f.cnpj || '—'} {f.email || '—'} {f.telefone || '—'} {f.ativo ? ATIVO : INATIVO} {new Date(f.created_at).toLocaleDateString('pt-BR')}
); } /* ── CLIENTES VIEW ─────────────────────────────────────────── */ function ClientesView({ clientes, loading, onAdd, onOpen, onEdit, onDelete }) { const [busca, setBusca] = React.useState(''); const filtrados = React.useMemo(() => { if (!busca.trim()) return clientes; const q = busca.trim().toLowerCase(); return clientes.filter(c => c.nome.toLowerCase().includes(q) || (c.nome_fantasia && c.nome_fantasia.toLowerCase().includes(q)) || (c.cnpj && c.cnpj.toLowerCase().includes(q)) || (c.cidade && c.cidade.toLowerCase().includes(q)) ); }, [clientes, busca]); return (
Clientes cadastrados · órgãos públicos e privados
{loading ? '...' : `${clientes.length} registros · ${clientes.filter(c => c.ativo).length} ativos`}
setBusca(e.target.value)} style={{ width: 260 }} />
{loading && ( )} {!loading && filtrados.length === 0 && ( )} {!loading && filtrados.map(c => ( onOpen(c)} style={{ cursor: 'pointer' }}> ))}
Nome / Razão social Tipo CNPJ Cidade/UF Contato Status Ações
Carregando clientes...
{clientes.length === 0 ? 'Nenhum cliente cadastrado. Clique em "Novo cliente" para começar.' : 'Nenhum resultado para a busca.'}
{c.nome}
{c.nome_fantasia &&
{c.nome_fantasia}
}
{(c.tipo || 'publico').toUpperCase()} {c.cnpj || '—'} {c.cidade ? `${c.cidade}/${c.uf || '—'}` : '—'} {c.email &&
{c.email}
} {c.telefone &&
{c.telefone}
}
{c.ativo ? ATIVO : INATIVO} e.stopPropagation()}>
); } /* ── CLIENTE FORM MODAL (Add e Edit) ───────────────────────── */ function ClienteFormModal({ cliente, onClose, onSave }) { const editing = !!cliente; const [form, setForm] = React.useState({ nome: cliente?.nome || '', nome_fantasia: cliente?.nome_fantasia || '', cnpj: cliente?.cnpj || '', tipo: cliente?.tipo || 'publico', email: cliente?.email || '', telefone: cliente?.telefone || '', endereco: cliente?.endereco || '', cidade: cliente?.cidade || '', uf: cliente?.uf || '', cep: cliente?.cep || '', contato_responsavel: cliente?.contato_responsavel || '', observacoes: cliente?.observacoes || '', ativo: cliente ? !!cliente.ativo : true, }); const [erro, setErro] = React.useState(null); const [saving, setSaving] = React.useState(false); const set = (k, v) => setForm({ ...form, [k]: v }); const submit = async () => { if (!form.nome.trim()) { setErro('Nome / razão social é obrigatório'); return; } setErro(null); setSaving(true); try { await onSave(form); } catch (e) { setErro(e?.message || 'Falha ao salvar'); setSaving(false); } }; return (
e.stopPropagation()}>
{editing ? 'Editar cliente' : 'Cadastrar novo cliente'}
M11 · órgão público ou privado
NOME / RAZÃO SOCIAL *
set('nome', e.target.value)} placeholder="Ex: Prefeitura Municipal de Niterói" autoFocus />
NOME FANTASIA / SIGLA
set('nome_fantasia', e.target.value)} placeholder="Ex: Pref. Niterói" />
TIPO
CNPJ
set('cnpj', e.target.value)} placeholder="00.000.000/0000-00" />
E-MAIL
set('email', e.target.value)} placeholder="compras@orgao.gov.br" />
TELEFONE
set('telefone', e.target.value)} placeholder="(00) 0000-0000" />
CONTATO RESPONSÁVEL
set('contato_responsavel', e.target.value)} placeholder="Setor de Compras / Nome do responsável" />
ENDEREÇO DE ENTREGA PADRÃO
set('endereco', e.target.value)} placeholder="Rua, número, complemento, bairro" />
CIDADE
set('cidade', e.target.value)} />
UF
CEP
set('cep', e.target.value)} placeholder="00000-000" />
OBSERVAÇÕES