// 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}/>
},
{k:'cnpj',l:'CNPJ'},
{k:'cidade',l:'Cidade/UF',r:(v,r)=>`${v||'—'}/${r.uf||''}`},
{k:'contato',l:'Contato',r:(v,r)=>},
{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
? 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 });