// 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 (
Gerencie relacionamentos e histórico de clientes
Tem certeza que deseja excluir {selected?.name}? Esta ação não pode ser desfeita.