// Tela: Contatos — lista + detalhe const { useState: useContatoState } = React; /* ─── Modal criar / editar ──────────────────────────────────── */ function ContatoModal({ onClose, onSave, initialData }) { const isEdit = !!initialData; const [nome, setNome] = useContatoState(initialData?.nome || ''); const [cargo, setCargo] = useContatoState(initialData?.cargo || ''); const [empresa, setEmpresa] = useContatoState(initialData?.empresa || ''); const [email, setEmail] = useContatoState(initialData?.email || ''); const [fone, setFone] = useContatoState(initialData?.fone || ''); const [cidade, setCidade] = useContatoState(initialData?.cidade || ''); const [tag, setTag] = useContatoState(initialData?.tag || 'Recorrente'); const [avatar, setAvatar] = useContatoState(initialData?.avatar || null); const [avatarDrag, setAvatarDrag] = useContatoState(false); const avatarRef = React.useRef(null); const toast = useToast(); const TAGS = ['VIP', 'Quente', 'Recorrente', 'Frio']; const TAG_CLS = { VIP: 'pill-dark', Quente: 'pill-pink', Recorrente: 'pill-mint', Frio: '' }; const valid = nome.trim().length > 0; const handleAvatarFile = (file) => { if (!file || !file.type.startsWith('image/')) return; const url = URL.createObjectURL(file); setAvatar(url); }; const submit = () => { if (!valid) return; const dados = { id: initialData?.id || Date.now(), avatar: avatar || `https://i.pravatar.cc/120?img=${Math.floor(Math.random()*70)+1}`, nome: nome.trim(), cargo: cargo.trim() || 'Contato', empresa: empresa.trim() || '—', email: email.trim() || '—', fone: fone.trim() || '—', cidade: cidade.trim() || '—', tag, }; if (onSave) onSave(dados); toast(isEdit ? `Contato "${nome.trim().split(' ')[0]}" atualizado ✓` : `Contato "${nome.trim().split(' ')[0]}" adicionado ✓` ); onClose(); }; return (
e.stopPropagation()}>
{isEdit ? <>Editar Contato : <>Novo Contato}
{isEdit ? 'Atualize as informações do contato' : 'Adicione um novo contato à base'}
{ICN.x}
{/* ── upload de foto ── */}
avatarRef.current?.click()} onDragOver={e => { e.preventDefault(); setAvatarDrag(true); }} onDragLeave={() => setAvatarDrag(false)} onDrop={e => { e.preventDefault(); setAvatarDrag(false); handleAvatarFile(e.dataTransfer.files?.[0]); }} > handleAvatarFile(e.target.files?.[0])} />
{avatar ? : }
setNome(e.target.value)} autoFocus />
setCargo(e.target.value)} />
setEmpresa(e.target.value)} />
setEmail(e.target.value)} />
setFone(e.target.value)} />
setCidade(e.target.value)} />
{TAGS.map(t => ( setTag(t)} style={{cursor:'default', fontSize:11}}> {t} ))}
); } /* ─── Linha da lista ────────────────────────────────────────── */ function ContactRow({ c, selected, onClick }) { return (
{c.nome}
{c.cargo} · {c.empresa}
{c.tag}
); } /* ─── Lista de contatos ─────────────────────────────────────── */ function ContactsList({ localContatos, setLocalContatos, onSelect, selectedId, onCreateContato }) { const [sort, setSort] = useContatoState('recentes'); const [tagFilter, setTagFilter]= useContatoState('Todos'); const [showNovo, setShowNovo] = useContatoState(false); const SORTS = ['recentes','nome','empresa']; const TAGS = ['Todos','VIP','Quente','Recorrente','Frio']; let lista = [...localContatos]; if (tagFilter !== 'Todos') lista = lista.filter(c => c.tag === tagFilter); if (sort === 'nome') lista.sort((a,b) => a.nome.localeCompare(b.nome)); if (sort === 'empresa') lista.sort((a,b) => a.empresa.localeCompare(b.empresa)); return ( <> Informações do Cliente} subtitle="Visão consolidada das contas e contatos" right={
s+n.valor,0)/1000000).toFixed(1).replace('.',',')}M`} pill={`+${OPORTUNIDADES.length} negócios`} pillColor="yellow" icon={ICN.chart}/> s+j.cards.length,0)}`} pill="+4 hoje" pillColor="mint" icon={ICN.cal}/>
} />
Contatos · {lista.length}
{TAGS.map(t => ( setTagFilter(t)} style={{cursor:'default',fontSize:11}}>{t} ))}
{SORTS.map(s => ( setSort(s)} style={{cursor:'default',fontSize:11,textTransform:'capitalize'}}>{s} ))}
setShowNovo(true)} />
{lista.map(c => ( onSelect(c.id)} /> ))}
{showNovo && ( setShowNovo(false)} onSave={onCreateContato || ((novo) => setLocalContatos(prev => [{ ...novo, id: Date.now() }, ...prev]))} /> )} ); } /* ─── Stat inline do cabeçalho ──────────────────────────────── */ function StatInline({ label, value, pill, pillColor, icon }) { return (
{icon}
{value} {pill}
{label}
); } /* ─── Mini calendário ───────────────────────────────────────── */ function MiniCalendar({ goto }) { const months = ['Janeiro','Fevereiro','Março','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro']; const [m, setM] = React.useState(4); const toast = useToast(); const today = m === 4 ? 13 : -1; const days = Array.from({length: 31}, (_, i) => i + 1); const startPad = 3; return (
setM((m + 11) % 12)} /> setM((m + 1) % 12)} />
{months[m]}
goto ? goto('calendario') : toast(`Abrindo ${months[m]} no calendário`)} />
{['D','S','T','Q','Q','S','S'].map((d,i) =>
{d}
)}
{Array(startPad).fill(null).map((_, i) =>
)} {days.slice(0, 21).map(d => { const isToday = d === today; const hasEv = [4, 11, 12, 16].includes(d); const isWeek = d >= 11 && d <= 17; return (
{hasEv && d !== today && ( )} {d}
); })}
); } /* ─── DL Row ────────────────────────────────────────────────── */ function DLRow({ icon, label, value, action }) { const toast = useToast(); return (
{icon}
{label}
{value}
toast(`${label} editado`)} />
); } function SrcChip({ color, label }) { return (
{label}
); } /* ─── Modal: Atribuir responsável ───────────────────────────── */ function AtribuirModal({ contact, onClose }) { const toast = useToast(); const [selected, setSelected] = React.useState(null); const membros = CONTATOS.slice(0, 6).map(m => ({ id: m.id, nome: m.nome.split(' ').slice(0, 2).join(' '), cargo: m.cargo, avatar: m.avatar, })); const confirmar = () => { if (!selected) { toast('Selecione um responsável'); return; } const m = membros.find(x => x.id === selected); toast(`${contact.nome.split(' ')[0]} atribuído(a) a ${m.nome.split(' ')[0]} ✓`); onClose(); }; return (
e.target === e.currentTarget && onClose()}>
Atribuir responsável
Selecione quem ficará responsável por {contact.nome.split(' ')[0]}:
{membros.map(m => (
setSelected(m.id)} style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '8px 10px', borderRadius: 10, cursor: 'pointer', background: selected === m.id ? 'var(--acc-blue)' : 'var(--bg-soft)', color: selected === m.id ? 'white' : 'inherit', transition: 'background .15s', }} >
{m.nome}
{m.cargo}
{selected === m.id && }
))}
); } /* ─── Detalhe do contato ────────────────────────────────────── */ const { InteractionCard, FunnelBars } = window; function ContactDetail({ contact, onBack, onEdit, onDelete, goto }) { const [showEdit, setShowEdit] = useContatoState(false); const [showAtribuir, setShowAtribuir] = React.useState(false); const [expandedInfo, setExpandedInfo] = React.useState(false); const toast = useToast(); const c = contact; const handleDelete = () => { if (onDelete) onDelete(c.id); toast(`Contato "${c.nome.split(' ')[0]}" excluído`); onBack(); }; return ( <> Informações do Cliente} subtitle={`${c.cargo} · ${c.empresa}`} back onBack={onBack} right={
s+n.valor,0)/1000000).toFixed(1).replace('.',',')}M`} pill={`+${OPORTUNIDADES.length} negócios`} pillColor="yellow" icon={ICN.chart}/> s+j.cards.length,0)}`} pill="+4 hoje" pillColor="mint" icon={ICN.cal}/>
} />
{/* Coluna principal */}
Histórico de Interações
{ const rows = [['Data','Negócio','Valor'],['04/05','Pacote Royal — Oportunidade','11250'],['16/05','Negócio mais relevante','21300'],['12/05','Sucesso absoluto — fechado','2100'],['11/05','Pacote Royal — Oportunidade','4160'],['02/05','Business Adaptativo','3140'],['02/05','Segundo negócio — comum','12350']]; const csv = '' + rows.map(r => r.map(v => `"${v}"`).join(',')).join('\n'); const a = Object.assign(document.createElement('a'), { href: URL.createObjectURL(new Blob([csv], {type:'text/csv;charset=utf-8'})), download: `historico-${c.nome.split(' ')[0].toLowerCase()}.csv` }); a.click(); URL.revokeObjectURL(a.href); toast('Histórico exportado em CSV ✓'); }}, { ic: ICN.filter, label: 'Filtrar período', toast: 'Filtros aplicados' }, ]} /> goto && goto('oportunidades')} />
{[ { d:"04/05", t:"Pacote Royal — Oportunidade", v:"R$ 11.250", c:"azul", ids:[0,4,10] }, { d:"16/05", t:"Negócio mais relevante", v:"R$ 21.300", c:"teal", ids:[1,5,11] }, { d:"12/05", t:"Sucesso absoluto — fechado", v:"R$ 2.100", c:"preto", ids:[2,6] }, { d:"11/05", t:"Pacote Royal — Oportunidade", v:"R$ 4.160", c:"amarelo", ids:[3,7] }, { d:"02/05", t:"Business Adaptativo", v:"R$ 3.140", c:"claro", ids:[8] }, { d:"02/05", t:"Segundo negócio — comum", v:"R$ 12.350", c:"claro", ids:[9,11] }, ].map((it,i) => )}
Agenda de Tarefas
{ toast('Tarefa criada para ' + c.nome.split(' ')[0] + ' ✓'); } }, { ic: ICN.cal, label: 'Ir para hoje', onClick: () => goto && goto('calendario') }, { ic: ICN.share, label: 'Compartilhar agenda', onClick: () => { navigator.clipboard?.writeText(`https://app.ezza.com.br/agenda/${c.id}`).catch(()=>{}); toast('Link da agenda copiado ✓'); } }, ]} /> goto && goto('calendario')} />
Funil de Etapas
R$ {(OPORTUNIDADES.filter(n => n.etapa !== 'Fechado').reduce((s,n) => s + n.valor, 0) / 1000).toFixed(0)}k em pipeline
Ponderado Total
{/* Coluna lateral */}
{ navigator.clipboard?.writeText(`https://app.ezza.com.br/contatos/${c.id}`).catch(()=>{}); toast('Link do contato copiado ✓'); }} /> setShowEdit(true) }, { ic: ICN.users, label: 'Atribuir a alguém', onClick: () => setShowAtribuir(true) }, { ic: ICN.cal, label: 'Agendar reunião', onClick: () => goto && goto('calendario') }, { divider: true }, { ic: ICN.trash, label: 'Excluir contato', danger: true, onClick: handleDelete }, ]} /> window.scrollTo({top:0, behavior:'smooth'})} />
{c.nome}
{c.cargo}
{c.empresa}
setShowEdit(true)} /> window.open(`mailto:${c.email}?subject=Olá, ${c.nome.split(' ')[0]}!`)} /> window.open(`tel:${c.fone.replace(/\D/g,'')}`)} /> goto && goto('oportunidades')} /> goto && goto('calendario')} />
Informações Detalhadas
setShowEdit(true)} /> setExpandedInfo(e => !e)} />
} action={ICN.link} /> {expandedInfo && <> {c.tag}} /> }
{showEdit && ( setShowEdit(false)} onSave={(updated) => { if (onEdit) onEdit(updated); setShowEdit(false); }} /> )} {showAtribuir && ( setShowAtribuir(false)} /> )} ); } /* ─── helpers API ───────────────────────────────────────────── */ const contatoHeaders = () => { const t = localStorage.getItem('ezza_token'); return { 'Content-Type': 'application/json', ...(t ? { 'Authorization': `Bearer ${t}` } : {}) }; }; /* ─── Contatos (root) ───────────────────────────────────────── */ function Contatos({ contactId, setContactId, goto }) { const { useEffect: useContatoEffect } = React; const [localContatos, setLocalContatos] = useContatoState([]); const [apiReady, setApiReady] = useContatoState(false); // Carrega do banco useContatoEffect(() => { fetch('/api/contatos', { headers: contatoHeaders() }) .then(r => r.json()) .then(d => { if (d.ok) setLocalContatos(d.contatos); else setLocalContatos([...CONTATOS]); // fallback demo }) .catch(() => setLocalContatos([...CONTATOS])) .finally(() => setApiReady(true)); }, []); const handleEdit = async (updated) => { setLocalContatos(prev => prev.map(c => c.id === updated.id ? { ...c, ...updated } : c)); try { await fetch(`/api/contatos/${updated.id}`, { method: 'PUT', headers: contatoHeaders(), body: JSON.stringify(updated), }); } catch (e) {} }; const handleDelete = async (id) => { setLocalContatos(prev => prev.filter(c => c.id !== id)); setContactId(null); try { await fetch(`/api/contatos/${id}`, { method: 'DELETE', headers: contatoHeaders() }); } catch (e) {} }; const handleCreate = async (novo) => { try { const r = await fetch('/api/contatos', { method: 'POST', headers: contatoHeaders(), body: JSON.stringify(novo), }); const d = await r.json(); if (d.ok) setLocalContatos(prev => [d.contato, ...prev]); else setLocalContatos(prev => [{ ...novo, id: Date.now() }, ...prev]); } catch (e) { setLocalContatos(prev => [{ ...novo, id: Date.now() }, ...prev]); } }; if (contactId) { const c = localContatos.find(x => x.id === contactId); if (c) { return ( setContactId(null)} onEdit={handleEdit} onDelete={handleDelete} goto={goto} /> ); } } return ( ); } window.Contatos = Contatos;