// Topnav overlays: SearchPalette, MessagesPanel, NotificationsPanel, ProfileMenu const { useState: useOvState, useEffect: useOvEffect, useRef: useOvRef } = React; // ─── SEARCH PALETTE (⌘K style) ─────────────────────────────────────────────── function SearchPalette({ onClose, onNavigate }) { const [q, setQ] = useOvState(''); const [selIdx, setSelIdx] = useOvState(0); const inputRef = useOvRef(); useOvEffect(() => { inputRef.current?.focus(); }, []); const ql = q.toLowerCase(); const contatos = CONTATOS.filter(c => !q || (c.nome + c.empresa + c.email).toLowerCase().includes(ql)).slice(0, 5); const leads = LEADS.filter(l => !q || (l.nome + l.empresa + l.whatsapp).toLowerCase().includes(ql)).slice(0, 4); const casos = CASOS.filter(c => !q || (c.assunto + c.empresa + c.id).toLowerCase().includes(ql)).slice(0, 3); const actions = [ { id: 'novo-lead', label: 'Criar novo lead', hint: 'Pipeline · ação rápida', go: 'pipeline' }, { id: 'novo-contato', label: 'Adicionar contato', hint: 'Contatos · ação rápida', go: 'contatos' }, { id: 'agendar', label: 'Agendar reunião', hint: 'Calendário · ação rápida', go: 'calendario' }, { id: 'relatorio', label: 'Gerar relatório mensal', hint: 'Relatórios · ação rápida', go: 'relatorios' }, ].filter(a => !q || a.label.toLowerCase().includes(ql)); // Lista plana de todos os itens navegáveis (para teclado) const flatItems = [ ...actions.map(a => ({ go: a.go })), ...contatos.map(() => ({ go: 'contatos' })), ...leads.map(() => ({ go: 'pipeline' })), ...casos.map(() => ({ go: 'casos' })), ]; const navigate = (idx) => { const item = flatItems[idx >= 0 ? idx : selIdx]; if (item) { onNavigate(item.go); onClose(); } }; useOvEffect(() => { setSelIdx(0); }, [q]); useOvEffect(() => { const h = (e) => { if (e.key === 'Escape') { onClose(); return; } if (e.key === 'ArrowDown') { e.preventDefault(); setSelIdx(i => Math.min(i + 1, flatItems.length - 1)); } if (e.key === 'ArrowUp') { e.preventDefault(); setSelIdx(i => Math.max(i - 1, 0)); } if (e.key === 'Enter') { e.preventDefault(); navigate(-1); } }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [selIdx, flatItems.length, onClose]); return (
e.stopPropagation()}>
{ICN.search} setQ(e.target.value)} placeholder="Buscar contatos, leads, casos, ações…" /> ESC
{(() => { let globalIdx = 0; const Item = ({ children, go, isImg }) => { const idx = globalIdx++; const isSel = idx === selIdx; return (
setSelIdx(idx)} onClick={() => { onNavigate(go); onClose(); }} > {children}
); }; return ( <> {actions.length > 0 && (
Ações rápidas
{actions.map(a => (
{ICN.bolt2}
{a.label}
{a.hint}
{ICN.arrowRight}
))}
)} {contatos.length > 0 && (
Contatos
{contatos.map(c => (
{c.nome}
{c.cargo} · {c.empresa}
{c.tag}
))}
)} {leads.length > 0 && (
Leads
{leads.map(l => (
{ICN.trendUp}
{l.nome.split(' ').slice(0,2).join(' ')} · {l.empresa}
R$ {l.valor.toLocaleString('pt-BR')} · {l.etapa.replace('-', ' ')}
{l.temp.toUpperCase()}
))}
)} {casos.length > 0 && (
Casos
{casos.map(c => (
{ICN.bolt}
{c.id} · {c.assunto}
{c.empresa} · {c.atualizado}
{c.prioridade}
))}
)} ); })()} {q && contatos.length + leads.length + casos.length + actions.length === 0 && (
Nenhum resultado para "{q}".
)}
navegar abrir ⌘K busca global
); } // ─── MESSAGES PANEL ────────────────────────────────────────────────────────── const MESSAGES = [ { id: 1, who: 0, snip: "Pode revisar a proposta antes da reunião de amanhã? Obrigada!", when: "há 4 min", unread: true }, { id: 2, who: 1, snip: "Fechamos o contrato — vou enviar os documentos para assinatura.", when: "há 32 min", unread: true }, { id: 3, who: 4, snip: "Adorei o briefing, mandei alguns ajustes em anexo. 📎", when: "há 1h", unread: true }, { id: 4, who: 7, snip: "Reagendamos para sexta às 15h?", when: "há 2h" }, { id: 5, who: 3, snip: "Equipe técnica já está alinhada para o go-live.", when: "ontem" }, { id: 6, who: 9, snip: "Obrigado pelo retorno rápido — vou apresentar para o conselho.", when: "ontem" }, { id: 7, who: 5, snip: "Vamos marcar uma call rápida sobre o lote 1208?", when: "2 dias" }, ]; function MessagesPanel({ onClose, onNavigate }) { const [tab, setTab] = useOvState('todas'); const [readIds, setReadIds] = useOvState(new Set()); const toast = useToast(); const isUnread = (m) => m.unread && !readIds.has(m.id); const markRead = (id) => setReadIds(prev => new Set([...prev, id])); const naoLidas = MESSAGES.filter(isUnread).length; const list = tab === 'todas' ? MESSAGES : MESSAGES.filter(isUnread); return (
Mensagens
{naoLidas > 0 ? `${naoLidas} não lida${naoLidas !== 1 ? 's' : ''} · ` : ''}{MESSAGES.length} conversas ativas
{ICN.x}
setTab('todas')}>Todas {MESSAGES.length}
setTab('naolidas')}>Não lidas {naoLidas}
{list.length === 0 && (
Nenhuma mensagem não lida 🎉
)} {list.map(m => { const c = CONTATOS[m.who]; const unread = isUnread(m); return (
{ markRead(m.id); if (onNavigate) { onNavigate('jornadas'); onClose(); } }} style={{opacity: unread ? 1 : 0.7}}>
{c.nome.split(' ').slice(0,2).join(' ')} {m.when}
{m.snip}
{unread && }
); })}
); } // ─── NOTIFICATIONS PANEL ───────────────────────────────────────────────────── const NOTIFS = [ { id: 1, type: 'win', icon: ICN.check, who: 0, txt: "fechou a oportunidade", extra: "Tecnoluz Solar · R$ 482.300", when: "há 8 min", unread: true, color: "mint" }, { id: 2, type: 'lead', icon: ICN.bolt2, who: 1, txt: "criou um novo lead quente", extra: "Construtora Paineiras", when: "há 35 min", unread: true, color: "lav" }, { id: 3, type: 'task', icon: ICN.cal, who: 7, txt: "atribuiu uma tarefa a você",extra: "Demo — Indústria Aurora", when: "há 1h", unread: true, color: "yellow" }, { id: 4, type: 'caso', icon: ICN.bolt, who: 3, txt: "abriu um caso crítico", extra: "Atraso lote 1208 · CASO-2835",when: "há 2h", color: "pink" }, { id: 5, type: 'mention',icon: ICN.mail, who: 4, txt: "mencionou você em um comentário", extra: "Briefing Studio Maracá", when: "ontem", color: "blue" }, { id: 6, type: 'sla', icon: ICN.refresh, who: null, txt: "SLA do CASO-2825 vence em 2 horas", extra: "Aurora · prioridade Alta", when: "ontem", color: "pink" }, { id: 7, type: 'goal', icon: ICN.trendUp, who: null, txt: "Meta do trimestre atingiu 78%", extra: "+12% em relação a abril", when: "há 2 dias", color: "mint" }, ]; const NOTIF_NAV = { win: 'oportunidades', lead: 'pipeline', task: 'calendario', caso: 'casos', mention: 'jornadas', sla: 'casos', goal: 'relatorios', }; function NotificationsPanel({ onClose, onNavigate }) { const [tab, setTab] = useOvState('todas'); const [readIds, setReadIds] = useOvState(new Set()); const toast = useToast(); const isUnread = (n) => n.unread && !readIds.has(n.id); const markRead = (id) => setReadIds(prev => new Set([...prev, id])); const markAll = () => { setReadIds(new Set(NOTIFS.map(n => n.id))); toast('Todas notificações marcadas como lidas'); }; const handleNotifClick = (n) => { markRead(n.id); const dest = NOTIF_NAV[n.type]; if (dest && onNavigate) { onNavigate(dest); onClose(); } }; const naoLidas = NOTIFS.filter(isUnread).length; const list = tab === 'naolidas' ? NOTIFS.filter(isUnread) : tab === 'sistema' ? NOTIFS.filter(n => n.type === 'sla' || n.type === 'goal') : NOTIFS; return (
Notificações
{naoLidas > 0 ? `${naoLidas} não lida${naoLidas > 1 ? 's' : ''}` : 'Tudo em dia ✓'}
{naoLidas > 0 && (
{ICN.doubleCheck}
)}
{ICN.x}
setTab('todas')}>Todas {NOTIFS.length}
setTab('naolidas')}>Não lidas {naoLidas}
setTab('sistema')}>Sistema
{list.length === 0 && (
{tab === 'naolidas' ? 'Nenhuma notificação não lida 🎉' : 'Nenhuma notificação.'}
)} {list.map(n => { const c = n.who != null ? CONTATOS[n.who] : null; const unread = isUnread(n); return (
handleNotifClick(n)} style={{opacity: unread ? 1 : 0.7, cursor: NOTIF_NAV[n.type] ? 'default' : 'default'}}>
{n.icon}
{c && {c.nome.split(' ').slice(0,2).join(' ')}} {c ? ` ${n.txt}` : {n.txt}}
{n.extra}
{n.when}
{unread && }
); })}
); } // ─── PROFILE MENU ──────────────────────────────────────────────────────────── function ProfileMenu({ onClose, onNavigate, onLogout }) { // Computa pipeline total e % de oportunidades fechadas vs total const totalPipeline = OPORTUNIDADES.reduce((s, n) => s + n.valor, 0); const fechadas = OPORTUNIDADES.filter(n => n.etapa === 'Fechado'); const taxaFechamento = Math.round((fechadas.length / Math.max(1, OPORTUNIDADES.length)) * 100); const pipelineK = (totalPipeline / 1000).toFixed(0); return (
Letícia Andrade
leticia.andrade@ezza.com.br
Plano Pro · 23 dias
R$ {pipelineK}k
Pipeline total
{taxaFechamento}%
Taxa de fechamento
onNavigate('perfil')} /> onNavigate('equipe')} /> onNavigate('configuracoes')} />
onNavigate('ajuda')} /> onNavigate('app-desktop')} />
{ onLogout && onLogout(); }} />
Ezza CRM · v2.6.1 · Status: operacional
); } function MenuItem({ icon, label, hint, right, danger, onClick }) { return (
{icon}
{label}
{hint &&
{hint}
}
{right || {ICN.arrowRight}}
); } // ─── POPOVER SHELL ─────────────────────────────────────────────────────────── function Popover({ onClose, anchor = 'right-0', width = 380, children }) { useOvEffect(() => { const h = (e) => { if (e.key === 'Escape') onClose(); }; window.addEventListener('keydown', h); return () => window.removeEventListener('keydown', h); }, [onClose]); // anchor is rough mapping to the icon position in TopNav. // The popover anchors top: 88 (below topnav) right: variable const rightMap = { 'right-0': 14, 'right-1': 60, 'right-2': 106 }; const right = rightMap[anchor] ?? 14; return ( <>
{children}
); } // ─── WELCOME TOUR ───────────────────────────────────────────────────────────── function WelcomeTour({ onFinish, goto }) { const [step, setStep] = useOvState(0); const [spot, setSpot] = useOvState(null); // BoundingClientRect do elemento alvo const CARD_W = 440; // Cada passo: qual elemento destacar e onde colocar o card em relação a ele const STEPS = [ { icon: '🧭', screen: 'jornadas', target: '.sidebar', place: 'right', title: 'Navegue com um clique', body: 'A barra lateral dá acesso a todas as telas do sistema. Cada ícone é um atalho direto — Pipeline, Jornadas, Contatos, Oportunidades, Calendário, Relatórios, Casos e Configurações.', }, { icon: '📊', screen: 'painel', target: '.main', place: 'center', title: 'Painel de Controle', body: 'O dashboard reúne tudo em um só lugar: KPIs de receita e conversão, funil de pipeline ponderado, próximas tarefas do dia e histórico de interações recentes. Ponto de partida do seu dia.', }, { icon: '🎯', screen: 'jornadas', target: '.main', place: 'center', title: 'Jornadas do Cliente', body: 'O kanban operacional é o coração da Ezza. Cada card é um atendimento percorrendo etapas — da triagem ao encerramento. Clique no avatar de um card para abrir a conversa com o cliente.', }, { icon: '⚡', screen: 'pipeline', target: '.main', place: 'center', title: 'Pipeline com IA SDR', body: 'Gerencie seus leads comerciais em colunas por etapa. Arraste os cards para avançar no funil. Clique no ícone WhatsApp para abrir a conversa — a IA SDR pode responder automaticamente em seu lugar.', }, { icon: '👥', screen: 'contatos', target: '.main', place: 'center', title: 'Gestão de Contatos', body: 'Cadastro completo de clientes e prospects: histórico de interações, oportunidades vinculadas, agendamentos, e-mail e ligação com um clique. Filtre por tag ou ordene como preferir.', }, { icon: '💼', screen: 'oportunidades', target: '.main', place: 'center', title: 'Oportunidades', body: 'Acompanhe todos os negócios em andamento em lista ou kanban. Filtre por etapa, responsável e probabilidade. Use os três pontinhos para editar, atribuir, reagendar ou marcar como ganha.', }, { icon: '🎫', screen: 'casos', target: '.main', place: 'center', title: 'Casos & Atendimentos', body: 'Central de suporte integrada: abra tickets, acompanhe SLA, atribua responsáveis e mude o status de cada caso. Os atalhos rápidos permitem ações em lote como arquivar e enviar pesquisa de satisfação.', }, { icon: '📈', screen: 'relatorios', target: '.main', place: 'center', title: 'Relatórios e Analytics', body: 'Visualize receita, ticket médio, ciclo de venda e taxa de conversão por período. Ative "Comparar" para ver a evolução frente ao período anterior. Exporte em PDF, CSV ou Excel com um clique.', }, { icon: '⚙️', screen: 'configuracoes', target: '.set-aside', place: 'right', title: 'Configurações', body: 'Aqui você personaliza tudo: dados da empresa, WhatsApp Business, Agente IA, status do sistema, integrações, automações, notificações e faturamento. Cada aba é uma área independente.', }, { icon: '🤖', screen: 'configuracoes', target: '.set-body', place: 'left', title: 'Agente IA — SDR Virtual', body: 'Configure o robô de vendas: defina o nome, objetivo, persona e o script que ele usa nas conversas. Ajuste os horários de atendimento e faça chamadas de teste direto do painel. O agente responde clientes no WhatsApp enquanto você foca no que importa.', onEnter: () => { // Clica na aba "Agente IA" dentro de Configurações const tabs = document.querySelectorAll('.set-tab'); tabs.forEach(t => { if (t.textContent.includes('Agente')) t.click(); }); }, }, { icon: '🔍', screen: 'painel', target: '.topnav', place: 'below', title: 'Busca global e atalhos', body: 'Pressione ⌘K (ou Ctrl+K) a qualquer momento para encontrar contatos, leads e casos instantaneamente. Os ícones no canto superior direito abrem mensagens, notificações e seu perfil.', }, ]; const s = STEPS[step]; const isLast = step === STEPS.length - 1; // Navega para a tela do passo useOvEffect(() => { if (s.screen && goto) goto(s.screen); }, [step]); // Mede o elemento alvo após a tela renderizar; executa onEnter se definido useOvEffect(() => { const run = () => { // 1. onEnter opcional (ex: clicar em uma aba) if (s.onEnter) s.onEnter(); // 2. mede depois do onEnter ter tido tempo de atualizar o DOM setTimeout(() => { const el = s.target ? document.querySelector(s.target) : null; setSpot(el ? el.getBoundingClientRect() : null); }, 80); }; const t = setTimeout(run, 150); return () => clearTimeout(t); }, [step]); // Calcula posição do spotlight (com padding) const PAD = 6; const spotRect = spot ? { left: spot.left - PAD, top: spot.top - PAD, width: spot.width + PAD * 2, height: spot.height + PAD * 2, borderRadius: 18, } : { left: 0, top: 0, width: 0, height: 0, borderRadius: 0 }; // Calcula posição do card baseado no spotlight real const vw = window.innerWidth; const vh = window.innerHeight; const MARGIN = 24; let cardLeft, cardTop; if (!spot) { // fallback: centro da tela cardLeft = (vw - CARD_W) / 2; cardTop = (vh - 300) / 2; } else if (s.place === 'right') { // à direita do elemento, centralizado verticalmente cardLeft = spotRect.left + spotRect.width + MARGIN; cardTop = Math.max(MARGIN, Math.min(vh - 320 - MARGIN, spotRect.top + spotRect.height / 2 - 150)); } else if (s.place === 'left') { // à esquerda do elemento, centralizado verticalmente cardLeft = Math.max(MARGIN, spotRect.left - CARD_W - MARGIN); cardTop = Math.max(MARGIN, Math.min(vh - 320 - MARGIN, spotRect.top + spotRect.height / 2 - 150)); } else if (s.place === 'below') { // abaixo do elemento, centralizado horizontalmente cardLeft = Math.max(MARGIN, Math.min(vw - CARD_W - MARGIN, spotRect.left + spotRect.width / 2 - CARD_W / 2)); cardTop = spotRect.top + spotRect.height + MARGIN; } else { // 'center': centro do spotlight cardLeft = Math.max(MARGIN, Math.min(vw - CARD_W - MARGIN, spotRect.left + spotRect.width / 2 - CARD_W / 2)); cardTop = Math.max(MARGIN, Math.min(vh - 320 - MARGIN, spotRect.top + spotRect.height / 2 - 150)); } const next = () => isLast ? onFinish() : setStep(step + 1); const back = () => { if (step > 0) setStep(step - 1); }; // Portal → document.body. Spotlight e card com position:fixed + coordenadas reais em px. return ReactDOM.createPortal( <> {/* Spotlight — usa getBoundingClientRect do elemento alvo */}
{/* Card — coordenadas calculadas em px baseadas no rect do elemento alvo */}
{s.icon}
{STEPS.map((_, i) => (
))} {step + 1}/{STEPS.length}
{s.title}
{s.body}
{step > 0 && ( )}
{/* fim card */} , document.body ); } window.SearchPalette = SearchPalette; window.MessagesPanel = MessagesPanel; window.NotificationsPanel = NotificationsPanel; window.ProfileMenu = ProfileMenu; window.WelcomeTour = WelcomeTour;