// SGAB v2 — Clientes & Equipamentos const { useState } = React; // ── CLIENTES ────────────────────────────────────────────────────── function Clientes({ user, toast, onNav }) { const [db, setDb] = useState(() => SGAB.getDB()); const [q, setQ] = useState(''); const [modal, setModal] = useState(false); const [editItem, setEdit] = useState(null); const [form, setForm] = useState({}); const [detalhe, setDetalhe] = useState(null); const { fmt } = SGAB; function reload() { setDb(SGAB.getDB()); } function F(k, v) { setForm(f=>({...f,[k]:v})); } const lista = db.clientes.filter(c => !q || c.razaoSocial.toLowerCase().includes(q.toLowerCase()) || (c.cnpj||'').includes(q) || (c.cidade||'').toLowerCase().includes(q.toLowerCase()) || (c.contato||'').toLowerCase().includes(q.toLowerCase()) ); function openNew() { setEdit(null); setForm({ ativo:true, uf:'SP' }); setModal(true); } function openEdit(c) { setEdit(c); setForm({...c}); setModal(true); } function salvar() { if (!form.razaoSocial?.trim()) { toast('Razão Social obrigatória.','error'); return; } const ndb = SGAB.getDB(); if (editItem) { const i = ndb.clientes.findIndex(c=>c.id===editItem.id); if (i>=0) ndb.clientes[i] = {...ndb.clientes[i],...form}; } else { ndb.clientes.push({id:SGAB.nxtId(ndb,'clientes'),createdAt:fmt.today(),...form}); } SGAB.saveDB(ndb); reload(); setModal(false); toast(editItem?'Cliente atualizado!':'Cliente cadastrado!'); if (detalhe) setDetalhe(ndb.clientes.find(c=>c.id===detalhe.id)||null); } function excluir(c) { if (!confirm(`Excluir cliente "${c.razaoSocial}"?`)) return; const ndb = SGAB.getDB(); ndb.clientes = ndb.clientes.filter(x=>x.id!==c.id); SGAB.saveDB(ndb); reload(); setDetalhe(null); toast('Cliente removido.','info'); } function toggleAtivo(c) { const ndb = SGAB.getDB(); const i = ndb.clientes.findIndex(x=>x.id===c.id); if (i>=0) ndb.clientes[i].ativo = !ndb.clientes[i].ativo; SGAB.saveDB(ndb); reload(); if (detalhe) setDetalhe(ndb.clientes[i]); toast('Status atualizado.'); } const ufs = ['AC','AL','AP','AM','BA','CE','DF','ES','GO','MA','MT','MS','MG','PA','PB','PR','PE','PI','RJ','RN','RS','RO','RR','SC','SP','SE','TO']; // ── DETALHE DO CLIENTE ──────────────────────────────────────── if (detalhe) { const cli = db.clientes.find(c=>c.id===detalhe.id) || detalhe; const equips = db.equipamentos.filter(e=>e.clienteId===cli.id); const ordens = db.ordens.filter(o=>o.clienteId===cli.id); const receber = db.financeiro.receber.filter(r=>r.clienteId===cli.id); return (
setDetalhe(null)} actions={<> openEdit(cli)}>Editar toggleAtivo(cli)}>{cli.ativo?'Desativar':'Ativar'} }/>

Dados do Cliente

{[['Razão Social',cli.razaoSocial],['Fantasia',cli.fantasia],['CNPJ',cli.cnpj],['IE',cli.ie],['Endereço',`${cli.endereco||''}, ${cli.bairro||''}`],['Cidade/UF',`${cli.cidade||''}/${cli.uf||''}`],['CEP',cli.cep]].map(([l,v])=>v&&(
{l} {v}
))}

Contato

{[['Responsável',cli.contato],['Departamento',cli.depto],['Telefone',cli.tel],['Celular',cli.cel],['E-mail',cli.email]].map(([l,v])=>v&&(
{l} {v}
))} {cli.obs&&
Obs: {cli.obs}
}
{[['Balanças',equips.length,'equipamentos'],['OS',ordens.length,'ordens'],['A Receber',receber.filter(r=>r.status!=='Recebida').length,'financeiro']].map(([l,v,nav])=>(
onNav(nav)} style={{padding:'12px 10px',borderRadius:10,background:'#F8FAFF',border:'1px solid #E8EDF4',textAlign:'center',cursor:'pointer',transition:'border-color 0.15s'}} onMouseEnter={e=>e.currentTarget.style.borderColor='#1E6FD9'} onMouseLeave={e=>e.currentTarget.style.borderColor='#E8EDF4'}>
{v}
{l}
))}
{/* EQUIPAMENTOS */}

Balanças cadastradas ({equips.length})

onNav('equipamentos')}>Cadastrar balança
{ const d=SGAB.fmt.daysTo(v); const c=d===null?'#6B7280':d<0?'#DC2626':d<=30?'#D97706':'#16A34A'; return {SGAB.fmt.date(v)}; }}, ]} data={equips} empty="Nenhuma balança cadastrada para este cliente." />
setModal(false)} title={editItem?'Editar Cliente':'Novo Cliente'} width={680} footer={<>setModal(false)}>CancelarSalvar}>
); } // ── LISTA ────────────────────────────────────────────────────── return (
c.ativo).length} clientes ativos`} actions={Novo Cliente}/>
{v}
{r.fantasia}
}, {k:'cnpj',l:'CNPJ'}, {k:'cidade',l:'Cidade/UF',r:(v,r)=>`${v||'—'}/${r.uf||''}`}, {k:'contato',l:'Contato',r:(v,r)=>
{v}
{r.tel}
}, {k:'ativo',l:'Status',r:v=>}, {k:'id',l:'',thStyle:{width:80},r:(_,r)=>(
e.stopPropagation()}>
)}, ]} data={lista} onRow={r=>setDetalhe(r)} empty="Nenhum cliente encontrado." />
setModal(false)} title={editItem?'Editar Cliente':'Novo Cliente'} width={680} footer={<>setModal(false)}>CancelarSalvar}>
); } function ClienteForm({ form, F, ufs, toast }) { const [cnpjLoading, setCnpjLoading] = useState(false); function maskCnpjCpf(v) { const d = (v||'').replace(/\D/g,'').slice(0,14); if (d.length <= 11) { return d.replace(/(\d{3})(\d)/,'$1.$2') .replace(/(\d{3})\.(\d{3})(\d)/,'$1.$2.$3') .replace(/(\d{3})(\d{1,2})$/,'$1-$2'); } return d.replace(/^(\d{2})(\d)/,'$1.$2') .replace(/^(\d{2})\.(\d{3})(\d)/,'$1.$2.$3') .replace(/\.(\d{3})(\d)/,'.$1/$2') .replace(/(\d{4})(\d)/,'$1-$2'); } async function lookupCnpj(digits) { setCnpjLoading(true); try { const r = await fetch(`https://brasilapi.com.br/api/cnpj/v1/${digits}`); if (!r.ok) throw new Error('não encontrado'); const d = await r.json(); F('razaoSocial', d.razao_social || ''); F('fantasia', d.nome_fantasia || d.razao_social || ''); const log = [d.descricao_tipo_de_logradouro, d.logradouro].filter(Boolean).join(' ').trim(); F('endereco', [log, d.numero].filter(Boolean).join(', ').trim()); F('bairro', d.bairro || ''); F('cep', d.cep ? String(d.cep).replace(/\D/g,'').replace(/(\d{5})(\d{3})/,'$1-$2') : ''); F('cidade', d.municipio || ''); F('uf', d.uf || ''); if (d.ddd_telefone_1) F('tel', d.ddd_telefone_1); if (d.email) F('email', d.email); toast && toast('Dados carregados da Receita Federal', 'success'); } catch (e) { toast && toast('CNPJ não encontrado ou serviço indisponível.', 'error'); } finally { setCnpjLoading(false); } } function onCnpjChange(v) { const formatted = maskCnpjCpf(v); const prev = (form.cnpj||'').replace(/\D/g,''); F('cnpj', formatted); const digits = formatted.replace(/\D/g,''); if (digits.length === 14 && prev.length !== 14) lookupCnpj(digits); } return ( <> F('razaoSocial',v)} placeholder="Nome completo da empresa"/> F('fantasia',v)} placeholder="Como é conhecido"/>
{cnpjLoading && (
)}
{cnpjLoading ?
Buscando na Receita Federal…
:
Os campos serão preenchidos automaticamente ao digitar um CNPJ válido.
} F('ie',v)} placeholder="000.000.000-00"/> F('endereco',v)} placeholder="Rua, número"/> F('bairro',v)}/> F('cep',v)} placeholder="00000-000"/> F('cidade',v)}/> F('uf',v)} options={ufs}/> F('contato',v)} placeholder="Nome do contato"/> F('depto',v)} placeholder="Ex: Compras, Manutenção"/> F('tel',v)} placeholder="(11) 0000-0000"/> F('cel',v)} placeholder="(11) 9 0000-0000"/> F('email',v)} type="email" placeholder="email@empresa.com.br"/> F('obs',v)} placeholder="Horários preferenciais, instruções de acesso, etc."/> ); } // ── EQUIPAMENTOS ────────────────────────────────────────────────── function Equipamentos({ user, toast }) { const [db, setDb] = useState(() => SGAB.getDB()); const [q, setQ] = useState(''); const [modal, setModal] = useState(false); const [editItem, setEdit] = useState(null); const [form, setForm] = useState({}); const { fmt } = SGAB; function reload() { setDb(SGAB.getDB()); } function F(k,v) { setForm(f=>({...f,[k]:v})); } const lista = db.equipamentos.filter(e => !q || (e.tag||'').toLowerCase().includes(q.toLowerCase()) || (e.modelo||'').toLowerCase().includes(q.toLowerCase()) || (e.serie||'').toLowerCase().includes(q.toLowerCase()) || db.clientes.find(c=>c.id===e.clienteId)?.razaoSocial.toLowerCase().includes(q.toLowerCase()) ); function openNew() { setEdit(null); setForm({ativo:true,tipoCalib:'RBC',classe:'III'}); setModal(true); } function openEdit(e) { setEdit(e); setForm({...e}); setModal(true); } function salvar() { if (!form.clienteId) { toast('Selecione o cliente.','error'); return; } if (!form.tag?.trim()) { toast('TAG/Patrimônio obrigatório.','error'); return; } const ndb = SGAB.getDB(); if (editItem) { const i = ndb.equipamentos.findIndex(e=>e.id===editItem.id); if (i>=0) ndb.equipamentos[i] = {...ndb.equipamentos[i],...form,clienteId:+form.clienteId}; } else { ndb.equipamentos.push({id:SGAB.nxtId(ndb,'equip'),...form,clienteId:+form.clienteId}); } SGAB.saveDB(ndb); reload(); setModal(false); toast(editItem?'Balança atualizada!':'Balança cadastrada!'); } function excluir(e) { if (!confirm(`Excluir balança ${e.tag}?`)) return; const ndb = SGAB.getDB(); ndb.equipamentos = ndb.equipamentos.filter(x=>x.id!==e.id); SGAB.saveDB(ndb); reload(); toast('Balança removida.','info'); } const cliOpts = db.clientes.filter(c=>c.ativo).map(c=>({v:c.id,l:c.razaoSocial})); const classeOpts = [{v:'I',l:'Classe I — Especial'},{v:'II',l:'Classe II — Fino'},{v:'III',l:'Classe III — Médio'},{v:'IIII',l:'Classe IIII — Grosseiro'}]; const calibOpts = [{v:'RBC',l:'Calibração RBC (Acreditada CGCRE)'},{v:'Rastreada',l:'Calibração Rastreada (DOC 11)'}]; const portariaOpts = ['OIML R-76','Portaria 157/22','MDCIE 261/02','MTIC 63/44','Portaria 236/94']; return (
e.ativo).length} balanças ativas`} actions={Nova Balança}/>
{v}}, {k:'clienteId',l:'Cliente',r:v=>db.clientes.find(c=>c.id===v)?.fantasia||'—'}, {k:'fab',l:'Fabricante'}, {k:'modelo',l:'Modelo'}, {k:'serie',l:'N° Série'}, {k:'cap',l:'Capacidade'}, {k:'classe',l:'Classe'}, {k:'tipoCalib',l:'Tipo Calib.',r:v=>{v}}, {k:'proxCalib',l:'Próx. Calib.',r:v=>{ const d=fmt.daysTo(v); const c=d===null?'#6B7280':d<0?'#DC2626':d<=30?'#D97706':'#16A34A'; return {fmt.date(v)}; }}, {k:'id',l:'',thStyle:{width:72},r:(_,r)=>(
e.stopPropagation()}>
)}, ]} data={lista} empty="Nenhuma balança encontrada."/>
setModal(false)} title={editItem?`Editar — ${editItem.tag}`:'Nova Balança'} width={640} footer={<>setModal(false)}>CancelarSalvar}> F('clienteId',v)} options={cliOpts} placeholder="Selecionar cliente..."/> F('fab',v)} placeholder="Ex: Toledo, Filizola"/> F('modelo',v)} placeholder="Ex: Prix 3 Plus"/> F('serie',v)} placeholder="Ex: TL-2024-001"/> F('tag',v)} placeholder="Ex: ALX-001"/> F('cap',v)} placeholder="Ex: 30 kg"/> F('ptrab',v)} placeholder="Ex: 10 g"/> F('divE',v)} placeholder="Ex: 10 g"/> F('divD',v)} placeholder="Ex: 10 g"/> F('classe',v)} options={classeOpts}/> F('portaria',v)} options={portariaOpts.map(p=>({v:p,l:p}))}/> F('local',v)} placeholder="Ex: Checkout 1, Açougue, Expedição"/> F('tipoCalib',v)} options={calibOpts}/> F('ultimaCalib',v)} type="date"/> F('proxCalib',v)} type="date"/>
); } Object.assign(window, { Clientes, Equipamentos });