/* Painel Nanda — telas A: Dashboard, Pipeline, Chat, Contatos. window.App.ScreensA */ (function () { const I = window.App.Icons; const U = window.App.UI; const D = window.App.Data; const CFG = window.NANDA_CONFIG || {}; const BR = CFG.brand || {}; const AG = BR.agente || 'Nanda'; const { useState, useMemo, useEffect, useRef } = React; // Datas defensivas: integrações externas podem criar sessions // sem historico e, no futuro, pode vir mensagem sem timestamp. Formatar data // inválida NUNCA pode quebrar a tela — devolve '' e a UI segue. const fmtHrs = (iso) => { if (!iso) return ''; const t = new Date(iso).getTime(); if (!isFinite(t)) return ''; const diff = (Date.now() - t) / 3600e3; if (diff < 1) return Math.max(1, Math.round(diff * 60)) + 'm'; if (diff < 24) return Math.round(diff) + 'h'; return Math.round(diff / 24) + 'd'; }; const hhmm = (iso) => { if (!iso) return ''; const d = new Date(iso); return isFinite(d.getTime()) ? d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' }) : ''; }; // Só mensagens reais (objetos). A sync do CRM cria sessions com data SEM // historico (vira []) e um item null no meio do array não pode derrubar o // Chat inteiro (TypeError em .timestamp — bug visto em produção 2026-09-01). const validMsgs = (arr) => (Array.isArray(arr) ? arr.filter((m) => m && typeof m === 'object') : []); /* ─────────────── DASHBOARD ─────────────── */ function LineChart({ serie }) { const W = 560, H = 210, pad = 34, top = 36; const ticks = [0, 9, 18, 27, 36]; const n = serie.length; const x = (i) => pad + (i * (W - pad - 8)) / (n - 1); const y = (v) => H - pad - (v / top) * (H - pad - 12); const line = serie.map((v, i) => `${i ? 'L' : 'M'}${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(' '); const area = `${line} L${x(n - 1).toFixed(1)} ${H - pad} L${x(0).toFixed(1)} ${H - pad} Z`; return ( {ticks.map((t) => ( {t} ))} {serie.map((v, i) => )} Hoje ); } function StatusBanner({ health }) { const ok = health && health.connected; const pct = health ? health.pct : 0; const [open, setOpen] = useState(false); return (
{ok ? : }
Status do Sistema
{ok ? 'WhatsApp conectado e operante' : 'Há configurações obrigatórias pendentes'}
{pct}% OK
{open && (
WhatsApp (Zernio) conectado{(health && health.connected) ? 'Sim' : 'Não'}
Conta WhatsApp Business ativa{(health && health.smartphoneConnected) ? 'Sim' : 'Não'}
Modo{(health && health.demo) ? 'Demonstração' : 'Produção'}
{health && health.error && (
Erro reportado: {String(health.error)}
)}
)} ); } function Dashboard({ data }) { const [range, setRange] = useState('hoje'); const dash = data.dash || {}; return (
{[['hoje', 'Hoje'], ['7d', '7 Dias'], ['30d', '30 Dias']].map(([k, l]) => ( ))}
} />
Volume de Atendimentos
Interações da IA hoje
Conversões
Reuniões, consultas e ações concluídas
Hoje{dash.conversoesPeriodo} conv.
Total no período{dash.conversoesPeriodo} conversões
); } /* ─────────────── NOVO LEAD (modal compartilhado: Pipeline "Novo deal" / Contatos "Novo contato") ─────────────── */ function NewLeadModal({ open, onClose, defaultColuna, onCreated }) { const [nome, setNome] = useState(''); const [telefone, setTelefone] = useState(''); const [prioridade, setPrioridade] = useState('media'); const [coluna, setColuna] = useState(defaultColuna || 'novo'); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); useEffect(() => { if (open) { setNome(''); setTelefone(''); setPrioridade('media'); setColuna(defaultColuna || 'novo'); setErr(null); } }, [open, defaultColuna]); const inputCls = 'h-10 w-full rounded-lg border border-line bg-surface px-3 text-[13.5px] text-fg placeholder:text-faint outline-none focus:border-brand/60'; const submit = async () => { if (!nome.trim() || !telefone.trim()) { setErr('Nome e telefone são obrigatórios.'); return; } setBusy(true); setErr(null); try { const { lead } = await D.createLead({ nome: nome.trim(), telefone: telefone.trim(), prioridade, coluna }); onCreated && onCreated(lead); onClose(); } catch (e) { setErr(String(e.message || e)); } finally { setBusy(false); } }; return (
setNome(e.target.value)} placeholder="Nome completo" className={inputCls} />
setTelefone(e.target.value)} placeholder="55 24 99999-9999" className={inputCls} />
{err &&
{err}
}
Cancelar {busy ? 'Salvando…' : 'Criar lead'}
); } /* ─────────────── EDITAR LEAD (modal compartilhado: Contatos / Chat — só Admin) ─────────────── */ function EditLeadModal({ open, onClose, lead, onUpdated }) { const [nome, setNome] = useState(''); const [telefone, setTelefone] = useState(''); const [email, setEmail] = useState(''); const [status, setStatus] = useState('novo'); const [busy, setBusy] = useState(false); const [err, setErr] = useState(null); useEffect(() => { if (open && lead) { setNome(lead.nome || ''); setTelefone(lead.telefone || ''); setEmail(lead.email || ''); setStatus(lead.status || 'novo'); setErr(null); } }, [open, lead]); const inputCls = 'h-10 w-full rounded-lg border border-line bg-surface px-3 text-[13.5px] text-fg placeholder:text-faint outline-none focus:border-brand/60'; const submit = async () => { if (!lead) return; if (!nome.trim() || !telefone.trim()) { setErr('Nome e telefone são obrigatórios.'); return; } const patch = {}; if (nome.trim() !== (lead.nome || '')) patch.nome = nome.trim(); if (telefone.trim() !== (lead.telefone || '')) patch.telefone = telefone.trim(); if (email.trim() !== (lead.email || '')) patch.email = email.trim(); if (status !== lead.status) patch.status = status; if (!Object.keys(patch).length) { onClose(); return; } setBusy(true); setErr(null); try { const { lead: updated } = await D.updateLead(lead.id, patch); onUpdated && onUpdated(updated); onClose(); } catch (e) { setErr(String(e.message || e)); } finally { setBusy(false); } }; return (
setNome(e.target.value)} placeholder="Nome completo" className={inputCls} />
setTelefone(e.target.value)} placeholder="55 24 99999-9999" className={inputCls} />
setEmail(e.target.value)} placeholder="opcional" className={inputCls} />
{err &&
{err}
}
Cancelar {busy ? 'Salvando…' : 'Salvar'}
); } /* ─────────────── ETAPAS DO PIPELINE (módulo "Configurar") ─────────────── */ function EstagiosManager() { const [estagios, setEstagios] = useState([]); const [loading, setLoading] = useState(true); const [err, setErr] = useState(null); const [busyId, setBusyId] = useState(null); const [newNome, setNewNome] = useState(''); const [newCor, setNewCor] = useState('#5aa2ff'); const [newTipo, setNewTipo] = useState('manual'); const [creating, setCreating] = useState(false); const load = async () => { setLoading(true); setErr(null); try { setEstagios(await D.pipelineEstagios()); } catch (e) { setErr(String(e.message || e)); } finally { setLoading(false); } }; useEffect(() => { load(); }, []); const patch = async (id, p) => { setBusyId(id); try { await D.updateEstagio(id, p); await load(); } catch (e) { setErr(String(e.message || e)); } finally { setBusyId(null); } }; const remove = async (id, nome) => { if (!window.confirm(`Excluir a etapa "${nome}"? Os leads não são apagados, só perdem essa referência.`)) return; setBusyId(id); try { await D.deleteEstagio(id); await load(); } catch (e) { setErr(String(e.message || e)); } finally { setBusyId(null); } }; const move = async (idx, dir) => { const target = estagios[idx + dir]; const cur = estagios[idx]; if (!target) return; setBusyId(cur.id); try { await Promise.all([D.updateEstagio(cur.id, { ordem: target.ordem }), D.updateEstagio(target.id, { ordem: cur.ordem })]); await load(); } catch (e) { setErr(String(e.message || e)); } finally { setBusyId(null); } }; const create = async () => { if (!newNome.trim()) return; setCreating(true); try { await D.createEstagio({ nome: newNome.trim(), cor: newCor, tipo: newTipo, criterios: '' }); setNewNome(''); setNewCor('#5aa2ff'); setNewTipo('manual'); await load(); } catch (e) { setErr(String(e.message || e)); } finally { setCreating(false); } }; if (loading) return
Carregando etapas…
; return (
Manual: você move o lead arrastando o card. Automático: a {AG} move sozinha quando a conversa atender aos critérios abaixo.
{err &&
{err}
}
{estagios.map((e, idx) => (
patch(e.id, { cor: ev.target.value })} title="Cor da etapa" className="h-8 w-8 shrink-0 cursor-pointer rounded border border-line bg-transparent p-0" /> { const v = ev.target.value.trim(); if (v && v !== e.nome) patch(e.id, { nome: v }); }} className="h-8 flex-1 rounded-md border border-line bg-surface2 px-2 text-[13px] text-fg outline-none focus:border-brand/60" />
{e.tipo === 'automatico' && (