// CRM Module function CRM() { const [clients, setClients] = React.useState([]); const [loading, setLoading] = React.useState(true); const [selected, setSelected] = React.useState(null); const [detailNotes, setDetailNotes] = React.useState([]); const [note, setNote] = React.useState(''); const [addModal, setAddModal] = React.useState(false); const [editModal, setEditModal] = React.useState(false); const [delConfirm, setDelConfirm] = React.useState(false); const [saving, setSaving] = React.useState(false); const emptyForm = { name: '', contact: '', email: '', phone: '', city: '', segment: 'SMB', status: 'Prospect', cnpj: '', birthday: '', address: '', cep: '' }; const [form, setForm] = React.useState(emptyForm); const [editForm, setEditForm] = React.useState(emptyForm); const [toast, setToast] = React.useState(null); const showToast = (msg, type = 'success') => { setToast({ msg, type }); setTimeout(() => setToast(null), 3000); }; const [cnpjLoading, setCnpjLoading] = React.useState(false); const [search, setSearch] = React.useState(''); const [statusFilter, setStatusFilter] = React.useState('all'); const fmtCnpj = (raw) => { const d = (raw || '').replace(/\D/g, '').slice(0, 14); if (d.length <= 2) return d; if (d.length <= 5) return `${d.slice(0,2)}.${d.slice(2)}`; if (d.length <= 8) return `${d.slice(0,2)}.${d.slice(2,5)}.${d.slice(5)}`; if (d.length <= 12) return `${d.slice(0,2)}.${d.slice(2,5)}.${d.slice(5,8)}/${d.slice(8)}`; return `${d.slice(0,2)}.${d.slice(2,5)}.${d.slice(5,8)}/${d.slice(8,12)}-${d.slice(12)}`; }; const fetchCnpjData = async (cnpj, setFormFn) => { const clean = (cnpj || '').replace(/\D/g, ''); if (clean.length !== 14) return; setCnpjLoading(true); try { const res = await fetch(`https://brasilapi.com.br/api/cnpj/v1/${clean}`); if (!res.ok) { setCnpjLoading(false); if (res.status === 404) showToast('CNPJ não encontrado na Receita.', 'danger'); else showToast(`Erro ao consultar CNPJ (${res.status}).`, 'danger'); return; } const data = await res.json(); const addressParts = [data.logradouro, data.numero, data.bairro].filter(p => p && String(p).trim()); const addressStr = addressParts.join(', '); const cepFmt = data.cep ? String(data.cep).replace(/(\d{5})(\d{3})/, '$1-$2') : ''; setFormFn(f => ({ ...f, name: f.name || data.razao_social || data.nome_fantasia || '', email: f.email || data.email || '', phone: f.phone || data.ddd_telefone_1 || '', city: f.city || (data.municipio ? `${data.municipio}${data.uf ? ', ' + data.uf : ''}` : ''), address: f.address || addressStr, cep: f.cep || cepFmt, })); showToast(`Dados de "${data.razao_social || data.nome_fantasia}" carregados!`); } catch (e) { console.error('BrasilAPI error:', e); showToast('Falha ao consultar BrasilAPI.', 'danger'); } finally { setCnpjLoading(false); } }; React.useEffect(() => { sb.from('clients').select('*').order('name').then(({ data }) => { if (data) setClients(data); setLoading(false); }); }, []); const openClient = (c) => { setSelected(c); setDetailNotes(c.notes || []); setNote(''); }; const closeClient = () => { setSelected(null); setDetailNotes([]); setNote(''); }; const saveNote = async () => { if (!note.trim() || !selected) return; const updated = [...detailNotes, note]; setDetailNotes(updated); setNote(''); await sb.from('clients').update({ notes: updated }).eq('id', selected.id); setClients(prev => prev.map(cl => cl.id === selected.id ? { ...cl, notes: updated } : cl)); showToast('Anotação salva!'); }; const openEdit = () => { setEditForm({ name: selected.name, contact: selected.contact || '', email: selected.email || '', phone: selected.phone || '', city: selected.city || '', segment: selected.segment || 'SMB', status: selected.status || 'Prospect', cnpj: selected.cnpj || '', birthday: selected.birthday || '', address: selected.address || '', cep: selected.cep || '' }); setEditModal(true); }; const handleEditSave = async () => { if (!editForm.name.trim()) { showToast('Preencha o nome.', 'danger'); return; } setSaving(true); const { data, error } = await sb.from('clients').update({ name: editForm.name, contact: editForm.contact, email: editForm.email, phone: editForm.phone, city: editForm.city, segment: editForm.segment, status: editForm.status, cnpj: editForm.cnpj, birthday: editForm.birthday, address: editForm.address, cep: editForm.cep, }).eq('id', selected.id).select().single(); setSaving(false); if (error) { showToast('Erro ao salvar.', 'danger'); return; } setClients(prev => prev.map(c => c.id === data.id ? data : c).sort((a, b) => a.name.localeCompare(b.name))); setSelected(data); setDetailNotes(data.notes || []); setEditModal(false); showToast('Cliente atualizado!'); }; const handleDelete = async () => { const id = selected.id; const nome = selected.name; await sb.from('clients').delete().eq('id', id); setClients(prev => prev.filter(c => c.id !== id)); setDelConfirm(false); closeClient(); showToast(`${nome} excluído.`); }; const handleCreate = async () => { if (!form.name.trim()) { showToast('Preencha o nome do cliente.', 'danger'); return; } setSaving(true); const { data, error } = await sb.from('clients').insert({ name: form.name, contact: form.contact, email: form.email, phone: form.phone, city: form.city, segment: form.segment, status: form.status, events_count: 0, total_spent: 0, notes: [], cnpj: form.cnpj, birthday: form.birthday, address: form.address, cep: form.cep, }).select().single(); setSaving(false); if (error) { showToast('Erro ao adicionar cliente.', 'danger'); return; } setClients(prev => [...prev, data].sort((a, b) => a.name.localeCompare(b.name))); setAddModal(false); setForm(emptyForm); showToast(`${data.name} adicionado com sucesso!`); }; const statusColor = { 'Ativo': 'success', 'Prospect': 'warning', 'Inativo': 'default' }; const segmentColor = { 'Enterprise': 'primary', 'SMB': 'default', 'PF': 'purple', 'Varejo': 'warning', 'Indústria': 'default', 'Alimentação': 'success', 'Saúde': 'primary', 'Tecnologia': 'default', 'Educação': 'default', 'Outro': 'default' }; const SEGMENT_OPTIONS = [ { value: 'Enterprise', label: 'Enterprise' }, { value: 'SMB', label: 'SMB' }, { value: 'PF', label: 'Pessoa Física' }, { value: 'Varejo', label: 'Varejo' }, { value: 'Indústria', label: 'Indústria' }, { value: 'Alimentação', label: 'Alimentação' }, { value: 'Saúde', label: 'Saúde' }, { value: 'Tecnologia', label: 'Tecnologia' }, { value: 'Educação', label: 'Educação' }, { value: 'Outro', label: 'Outro' }, ]; const totalAtivos = clients.filter(c => c.status === 'Ativo').length; // Filtro de busca + status const filteredClients = React.useMemo(() => { const q = search.trim().toLowerCase(); return clients.filter(c => { if (statusFilter !== 'all' && c.status !== statusFilter) return false; if (!q) return true; const cnpjClean = (c.cnpj || '').replace(/\D/g, ''); return ( (c.name || '').toLowerCase().includes(q) || (c.contact || '').toLowerCase().includes(q) || (c.email || '').toLowerCase().includes(q) || (c.phone || '').toLowerCase().includes(q) || (c.city || '').toLowerCase().includes(q) || cnpjClean.includes(q.replace(/\D/g, '')) ); }); }, [clients, search, statusFilter]); const receitaTotal = clients.reduce((s, c) => s + (c.total_spent || 0), 0); const ticketMedio = clients.length > 0 ? receitaTotal / clients.length : 0; const timeline = [ { icon: 'calendar', text: 'Evento confirmado', date: '22 Abr 2026', color: DS.colors.primary }, { icon: 'budget', text: 'Orçamento enviado', date: '18 Abr 2026', color: DS.colors.warning }, { icon: 'message', text: 'Reunião de briefing realizada', date: '10 Abr 2026', color: DS.colors.success }, ]; return (
{/* Header */}

Clientes (CRM)

Gerencie relacionamentos e histórico de clientes

setAddModal(true)}>Novo cliente
{/* Stats */}
{[ { label: 'Clientes ativos', value: totalAtivos, color: DS.colors.primary }, { label: 'Receita total', value: 'R$ ' + (receitaTotal / 1000000).toFixed(1) + 'M', color: DS.colors.success }, { label: 'Ticket médio', value: 'R$ ' + Math.round(ticketMedio / 1000) + 'k', color: DS.colors.warning }, ].map(s => (
{s.label}
{s.value}
))}
{/* Busca + filtros */}
setSearch(e.target.value)} placeholder="Buscar por nome, contato, e-mail, telefone, cidade ou CNPJ..." style={{ width: '100%', boxSizing: 'border-box', border: `1px solid ${DS.colors.border}`, borderRadius: DS.radius.sm, padding: '10px 14px 10px 38px', fontSize: 13, outline: 'none', background: '#fff', fontFamily: 'inherit', }} />
{[['all', 'Todos'], ['Ativo', 'Ativos'], ['Prospect', 'Prospects'], ['Inativo', 'Inativos']].map(([val, label]) => ( ))}
{filteredClients.length} de {clients.length}
{/* Table */} {loading ? : ( <>
EmpresaContatoÚltimo eventoEventosSegmentoStatusTotal
{filteredClients.length === 0 ? : filteredClients.map((c, i) => (
openClient(c)} style={{ display: 'grid', gridTemplateColumns: '2fr 1.2fr 1fr 0.8fr 0.8fr 0.8fr 0.8fr', padding: '14px 24px', borderBottom: i < filteredClients.length - 1 ? `1px solid ${DS.colors.border}` : 'none', alignItems: 'center', cursor: 'pointer', transition: 'background 0.1s' }} onMouseEnter={e => e.currentTarget.style.background = '#F8FAFC'} onMouseLeave={e => e.currentTarget.style.background = 'transparent'} >
{c.name}
{c.contact} {(c.last_event || '').substring(0, 18)}{(c.last_event || '').length > 18 ? '…' : ''} {c.events_count || 0} {c.segment} {c.status} R$ {((c.total_spent || 0) / 1000).toFixed(0)}k
)) } )}
{/* ── Popup detalhe do cliente ── */} Editar setDelConfirm(true)}>Excluir
)}> {selected && (
{/* Coluna esquerda */}
{/* Stats */}
{[ { label: 'Total investido', value: 'R$ ' + (selected.total_spent || 0).toLocaleString('pt-BR'), color: DS.colors.primary }, { label: 'Eventos realizados', value: selected.events_count || 0, color: DS.colors.success }, { label: 'Ticket médio', value: selected.events_count > 0 ? 'R$ ' + Math.round((selected.total_spent || 0) / selected.events_count).toLocaleString('pt-BR') : '—', color: DS.colors.warning }, ].map(s => (
{s.label}
{s.value}
))}
{/* Timeline */}
Linha do tempo
{timeline.map((t, i) => (
{i < timeline.length - 1 &&
}
{t.text}
{t.date}
))}
{/* Notas */}
Anotações & Follow-up
{detailNotes.length === 0 ?
Nenhuma anotação ainda.
: detailNotes.map((n, i) => (
{n}
)) }
setNote(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') saveNote(); }} placeholder="Adicionar anotação..." style={{ flex: 1, border: `1px solid ${DS.colors.border}`, borderRadius: DS.radius.sm, padding: '7px 11px', fontSize: 13, outline: 'none', fontFamily: 'inherit' }} onFocus={e => e.target.style.borderColor = DS.colors.primary} onBlur={e => e.target.style.borderColor = DS.colors.border} /> Salvar
{/* Coluna direita — informações */}
{/* Avatar + badges */}
{selected.name}
{selected.contact}
{selected.segment} {selected.status}
selected.email ? window.open(`mailto:${selected.email}`) : showToast('E-mail não cadastrado.', 'danger')}>E-mail selected.phone ? window.open(`tel:${selected.phone.replace(/\D/g, '')}`) : showToast('Telefone não cadastrado.', 'danger')}>Ligar
{/* Dados */}
Contato
{[ { icon: 'mail', label: selected.email || '—' }, { icon: 'phone', label: selected.phone || '—' }, { icon: 'map', label: selected.city || '—' }, ...(selected.cnpj ? [{ icon: 'budget', label: 'CNPJ: ' + selected.cnpj }] : []), ...(selected.address ? [{ icon: 'map', label: selected.address }] : []), ...(selected.cep ? [{ icon: 'map', label: 'CEP ' + selected.cep }] : []), ...(selected.birthday ? [{ icon: 'calendar', label: 'Aniversário: ' + selected.birthday }] : []), ].map((r, idx) => (
{r.label}
))}
{/* Último evento */} {selected.last_event && (
Último evento
{selected.last_event}
)}
)} {/* Modal editar cliente */} setEditModal(false)} title="Editar Cliente" width={560}>
setEditForm(f => ({ ...f, name: v }))} placeholder="Ex: TechCorp Brasil" />
setEditForm(f => ({ ...f, contact: v }))} placeholder="Ex: Marcelo Andrade" /> setEditForm(f => ({ ...f, city: v }))} placeholder="Ex: São Paulo" />
setEditForm(f => ({ ...f, email: v }))} placeholder="Ex: contato@empresa.com" /> setEditForm(f => ({ ...f, phone: v }))} placeholder="Ex: (11) 3291-4400" />
setEditForm(f => ({ ...f, cnpj: fmtCnpj(v) }))} placeholder="Ex: 00.000.000/0001-00" onBlur={e => fetchCnpjData(e.target.value, setEditForm)} /> setEditForm(f => ({ ...f, birthday: v }))} placeholder="Ex: 15/03" />
setEditForm(f => ({ ...f, address: v }))} placeholder="Ex: Rua das Flores, 123 — Centro" /> setEditForm(f => ({ ...f, cep: v }))} placeholder="Ex: 60330-605" />
setEditForm(f => ({ ...f, status: v }))} options={[{ value: 'Prospect', label: 'Prospect' }, { value: 'Ativo', label: 'Ativo' }, { value: 'Inativo', label: 'Inativo' }]} />
setEditModal(false)}>Cancelar {saving ? 'Salvando…' : 'Salvar alterações'}
{/* Confirmação de exclusão */} setDelConfirm(false)} title="Excluir cliente" width={400}>

Tem certeza que deseja excluir {selected?.name}? Esta ação não pode ser desfeita.

setDelConfirm(false)}>Cancelar Excluir
{/* Modal novo cliente */} { setAddModal(false); setForm(emptyForm); }} title="Novo Cliente" width={560}>
setForm(f => ({ ...f, name: v }))} placeholder="Ex: TechCorp Brasil" />
setForm(f => ({ ...f, contact: v }))} placeholder="Ex: Marcelo Andrade" /> setForm(f => ({ ...f, city: v }))} placeholder="Ex: São Paulo" />
setForm(f => ({ ...f, email: v }))} placeholder="Ex: contato@empresa.com" /> setForm(f => ({ ...f, phone: v }))} placeholder="Ex: (11) 3291-4400" />
setForm(f => ({ ...f, cnpj: fmtCnpj(v) }))} placeholder="Ex: 00.000.000/0001-00" onBlur={e => fetchCnpjData(e.target.value, setForm)} /> setForm(f => ({ ...f, birthday: v }))} placeholder="Ex: 15/03" />
setForm(f => ({ ...f, address: v }))} placeholder="Ex: Rua das Flores, 123 — Centro" /> setForm(f => ({ ...f, cep: v }))} placeholder="Ex: 60330-605" />
setForm(f => ({ ...f, status: v }))} options={[ { value: 'Prospect', label: 'Prospect' }, { value: 'Ativo', label: 'Ativo' }, { value: 'Inativo', label: 'Inativo' }, ]} />
{ setAddModal(false); setForm(emptyForm); }}>Cancelar {saving ? 'Salvando…' : 'Adicionar cliente'}
{toast && setToast(null)} />}
); } Object.assign(window, { CRM });