/* Painel Nanda — telas B: Treinamento, Agendamentos, Equipe, Ajuda, Configurações. window.App.ScreensB */
(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 { useState, useEffect } = React;
const AG = BR.agente || 'Nanda';
/* ─────────────── TREINAMENTO ─────────────── */
function Training() {
const tabs = ['Fatos', 'Documentos', 'Dúvidas', 'Simulador', 'Testes'];
const [tab, setTab] = useState('Fatos');
const trilha = [
['Fatos essenciais', 'Cadastre preço, link e horário'],
['Base de busca', 'Adicione FAQ ou material'],
['Dúvidas em dia', '10 abertas'],
['Golden set', '0 de 10 casos'],
['Avaliação ≥ 80%', 'Rode a primeira avaliação'],
];
const empty = {
Fatos: ['Nenhum fato cadastrado', 'Cadastre verdades que a ' + AG + ' repete palavra por palavra (preço, link, horário).'],
Documentos: ['Nenhum documento na base', 'Suba FAQ, tabela de valores ou materiais — viram base de busca (RAG).'],
Dúvidas: ['10 dúvidas abertas', 'Perguntas que a ' + AG + ' não soube responder. Transforme cada uma em fato.'],
Simulador: ['Simulador de conversa', 'Converse com a ' + AG + ' em ambiente de teste antes de liberar pro cliente.'],
Testes: ['Casos de teste (0 ativos)', 'Monte o golden set e rode a avaliação automática (meta ≥ 80%).'],
};
return (
TRILHA DO TREINAMENTO0/5
{trilha.map(([t, s], i) => (
{i + 1}
{t}
{s}
))}
{tabs.map((t) => (
))}
{empty[tab][0]}
{empty[tab][1]}
Adicionar
);
}
/* ─────────────── AGENDAMENTOS ─────────────── */
function Scheduling({ data }) {
const [view, setView] = useState('Mês');
const now = new Date();
// Mês exibido no calendário (independente do mês real) — sem isto o grid ficava
// sempre travado em new Date() e os botões de navegação não tinham pra onde escrever.
const [cursor, setCursor] = useState(() => new Date(now.getFullYear(), now.getMonth(), 1));
const y = cursor.getFullYear(), mo = cursor.getMonth();
const first = new Date(y, mo, 1).getDay();
const days = new Date(y, mo + 1, 0).getDate();
const evByDay = {};
(data.confirm || []).forEach((c) => {
const dt = new Date(c.quando);
if (dt.getFullYear() !== y || dt.getMonth() !== mo) return; // não vazar evento de outro mês pro mesmo número de dia
const dd = dt.getDate();
(evByDay[dd] = evByDay[dd] || []).push(c);
});
const cells = [];
for (let i = 0; i < first; i++) cells.push(null);
for (let dd = 1; dd <= days; dd++) cells.push(dd);
const wd = ['DOM', 'SEG', 'TER', 'QUA', 'QUI', 'SEX', 'SÁB'];
const monthName = cursor.toLocaleDateString('pt-BR', { month: 'long', year: 'numeric' });
const goPrevMonth = () => setCursor((d) => new Date(d.getFullYear(), d.getMonth() - 1, 1));
const goNextMonth = () => setCursor((d) => new Date(d.getFullYear(), d.getMonth() + 1, 1));
return (
{done} liberado na allowlist. Atenção: o login aqui hoje é e-mail/senha (Google OAuth não está habilitado neste projeto) — a pessoa só consegue entrar depois que uma conta com senha for criada pra ela em Authentication → Users no Supabase, não é automático como seria com Google.
Esta tabela é ilustrativa. Quem realmente tem acesso ao painel está em "Configurar".
setInviteOpen(false)} />
setCfgOpen(false)} />
);
}
/* ─────────────── AJUDA ─────────────── */
function Help() {
const faq = [
['Como a ' + AG + ' recebe mensagens?', 'Via webhook da Zernio (WhatsApp Business API oficial, evento message.received). Cada mensagem cai no backend (Railway), que roda o motor de conversa e responde pelo mesmo número.'],
['Preciso de template aprovado pela Meta?', 'Sim, para iniciar conversa ou falar fora da janela de 24h. Dentro da janela aberta pelo contato o texto é livre. Os templates são criados e aprovados na WABA da rádio.'],
['A ' + AG + ' responde tudo sozinha?', 'Só quando o modo ativo estiver ligado no backend (NANDA_ATIVA=1). Pausada, ela registra o que chega e não responde nada. Em qualquer conversa você pode "Assumir" e conduzir manualmente; ao "Retomar", ela volta.'],
['Como conecto o número?', 'O número é cadastrado na Zernio (WABA), e o webhook da Zernio aponta para o backend em /webhook-zernio. Status em Dashboard, widget "Status do Sistema" (exige login).'],
['Onde ficam os segredos (tokens)?', 'Somente no backend, via variáveis de ambiente. O painel usa apenas a anon key do Supabase (protegida por RLS) e a sessão de login. Service_role e API key da Zernio nunca entram no front.'],
['E áudio?', 'Entrada de áudio pode ser transcrita (Whisper) e a resposta pode sair em voz (TTS), tudo server-side e desligável por variável de ambiente.'],
];
const [open, setOpen] = useState(0);
return (
Checklist de ativação
{['Cadastrar o número dedicado na Zernio (WABA da rádio).', 'Configurar o webhook message.received apontando pro backend (/webhook-zernio).', 'Subir o perfil da ' + AG + ' em Configurações > Agente antes de liberar.', 'Ligar o modo ativo (NANDA_ATIVA=1) e validar com um número da equipe.'].map((s, i) =>
{s}
)}
Perguntas frequentes
{faq.map(([q, a], i) => (
{open === i &&
{a}
}
))}
);
}
/* ─────────────── NOTIFICAÇÕES — Aviso de Confirmação de Visita ───────────
Quando o CLIENTE confirma reunião com a Nanda, o backend (ver
src/notificacaoVisita.js) avisa cada destinatário ATIVO
aqui via WhatsApp (template Meta "confirmacao_visita_nanda"). Esta seção só
gerencia a LISTA (GET/POST/excluir em /api/notif-destinatarios) — quem
está nela é só UX de Admin, o backend também confia na sessão/serviço,
não recalcula papel aqui (mesma ressalva do resto do painel). */
function formatarTelDisplay(tel) {
const dg = String(tel || '').replace(/\D/g, '');
if (dg.length === 13 && dg.startsWith('55')) return `+55 (${dg.slice(2, 4)}) ${dg.slice(4, 9)}-${dg.slice(9, 13)}`;
return dg ? `+${dg}` : (tel || '');
}
function NotifVisitaSettings() {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(true);
const [err, setErr] = useState(null);
const [nome, setNome] = useState('');
const [telefone, setTelefone] = useState('');
const [busyAdd, setBusyAdd] = useState(false);
const [busyId, setBusyId] = useState(null);
const load = async () => {
setLoading(true); setErr(null);
try { setItems(await D.notifDestinatarios()); }
catch (e) { setErr(String(e.message || e)); }
finally { setLoading(false); }
};
useEffect(() => { load(); }, []);
const add = async () => {
const digits = telefone.replace(/\D/g, '');
if (!nome.trim()) { setErr('Digite o nome do destinatário.'); return; }
if (digits.length < 10) { setErr('Telefone inválido — use DDI+DDD+número, ex: 5524999998888.'); return; }
setBusyAdd(true); setErr(null);
try { await D.addNotifDestinatario({ nome: nome.trim(), telefone: digits }); setNome(''); setTelefone(''); await load(); }
catch (e) { setErr(String(e.message || e)); }
finally { setBusyAdd(false); }
};
const remove = async (item) => {
if (!window.confirm(`Remover ${item.nome} da lista de avisos de visita? A pessoa deixa de receber o aviso quando um cliente confirmar visita.`)) return;
setBusyId(item.id); setErr(null);
try { await D.removeNotifDestinatario(item.id); await load(); }
catch (e) { setErr(String(e.message || e)); }
finally { setBusyId(null); }
};
return (
Aviso de Confirmação de Visita
Quando um cliente confirma visita ou reunião com a {AG}, cada destinatário ativo abaixo recebe um aviso automático por WhatsApp
(template aprovado pela Meta) com nome do cliente, interesse, contato, objeções, dores e desejos identificados na conversa.
{err &&
{err}
}
{loading ? (
Carregando…
) : (
{items.map((it) => (
{it.nome}
{formatarTelDisplay(it.telefone)}
))}
{!items.length &&
Nenhum destinatário cadastrado — ninguém vai receber o aviso ainda.
Credenciais da Zernio vivem só no backend (env). O painel nunca exibe nem armazena a chave.
}
{tab === 'Notificações' && }
{tab === 'APIs' &&
Supabase
Backend {AG}
}
setConfirmOpen(false)} title="Salvar e ativar novo prompt?">
Isto substitui o prompt que a {AG} usa agora, ao vivo, em todas as próximas conversas. Uma nova versão é criada e a anterior fica guardada no banco (dá pra reverter). Confirmar?
setConfirmOpen(false)} disabled={saving}>Cancelar{saving ? 'Salvando…' : 'Salvar e ativar'}