// Tela: Disparos — campanhas de WhatsApp com templates verificados pela Meta const dspHeaders = () => { const t = localStorage.getItem('ezza_token'); return { 'Content-Type':'application/json', ...(t?{'Authorization':`Bearer ${t}`}:{}) }; }; const DSP_STATUS = { APPROVED: { label: 'Aprovado', bg: 'oklch(0.94 0.06 150)', fg: 'oklch(0.35 0.13 155)' }, PENDING: { label: 'Em análise', bg: 'oklch(0.96 0.08 90)', fg: 'oklch(0.45 0.12 80)' }, REJECTED: { label: 'Recusado', bg: 'oklch(0.94 0.06 20)', fg: 'oklch(0.45 0.16 25)' }, }; const DSP_PUBLICOS = [ { id: 'contatos', label: 'Todos os contatos' }, { id: 'leads_abertos', label: 'Leads em aberto (não fechados)' }, { id: 'leads_quentes', label: 'Só leads QUENTES 🔥' }, { id: 'leads', label: 'Segmentar leads (etapa, temperatura, vendedor)' }, ]; // Etapas do funil — mesmos slugs do pipeline const DSP_ETAPAS = [ { id: '', label: 'Todas as etapas abertas' }, { id: 'novo-lead', label: 'Novo lead' }, { id: 'contato-feito', label: 'Contato feito' }, { id: 'em-andamento', label: 'Em andamento' }, { id: 'proposta-enviada', label: 'Proposta enviada' }, { id: 'fechado', label: 'Fechado' }, ]; // Biblioteca de modelos prontos — o cliente escolhe, personaliza e manda pra verificação const DSP_MODELOS = [ { id: 'boas_vindas', rotulo: '👋 Boas-vindas', cat: 'UTILITY', texto: 'Oi {{1}}! Que bom ter você com a gente 😊 Seu cadastro foi confirmado. Qualquer dúvida é só responder essa mensagem que a nossa equipe te ajuda!' }, { id: 'promocao', rotulo: '🏷️ Promoção', cat: 'MARKETING', texto: 'Oi {{1}}! Temos uma condição especial essa semana: [DESCREVA A OFERTA AQUI]. Válida até [DATA]. Quer aproveitar? É só responder essa mensagem!' }, { id: 'reengajamento', rotulo: '🔄 Reengajamento', cat: 'MARKETING', texto: 'Oi {{1}}, tudo bem? Faz um tempinho que a gente não se fala! Temos novidades que podem te interessar. Quer dar uma olhada? Responde aqui que eu te conto 😊' }, { id: 'aviso_pedido', rotulo: '📦 Aviso de pedido', cat: 'UTILITY', texto: 'Oi {{1}}! Passando pra avisar que seu pedido foi atualizado: [STATUS]. Qualquer dúvida é só chamar por aqui!' }, { id: 'agradecimento', rotulo: '💚 Agradecimento', cat: 'MARKETING', texto: 'Oi {{1}}! Passando só pra agradecer pela confiança 💚 Se precisar de qualquer coisa, estamos por aqui. Indique a gente pra um amigo e os dois ganham [BENEFÍCIO]!' }, ]; function NovoTemplateModal({ onClose, onCreated }) { const [nome, setNome] = React.useState(''); const [cat, setCat] = React.useState('MARKETING'); const [texto, setTexto] = React.useState(''); const [midia, setMidia] = React.useState(null); // File do anexo (imagem/vídeo) const [midiaPrev, setMidiaPrev] = React.useState(null); // object URL pra preview const [busy, setBusy] = React.useState(false); const [rodape, setRodape] = React.useState(''); const [botoes, setBotoes] = React.useState([]); // [{tipo, texto, url?, telefone?}] const midiaRef = React.useRef(null); const addBotao = () => setBotoes(b => [...b, { tipo:'QUICK_REPLY', texto:'' }]); const removerBotao = (i) => setBotoes(b => b.filter((_, j) => j !== i)); const mudarBotao = (i, patch) => setBotoes(b => b.map((x, j) => j === i ? { ...x, ...patch } : x)); const toast = useToast(); const usarModelo = (m) => { setNome(m.id); setCat(m.cat); setTexto(m.texto); }; const escolherMidia = (e) => { const f = e.target.files && e.target.files[0]; e.target.value = ''; if (!f) return; if (f.size > 16*1024*1024) { toast('Anexo muito grande (máx 16 MB)', { kind: 'err' }); return; } setMidia(f); setMidiaPrev({ url: URL.createObjectURL(f), tipo: f.type.startsWith('video/') ? 'video' : f.type.startsWith('image/') ? 'imagem' : 'arquivo', nome: f.name }); }; const removerMidia = () => { setMidia(null); setMidiaPrev(null); }; // Botao sem texto nao vai pra Meta: ela recusa o template inteiro. const botoesValidos = () => botoes.filter(b => (b.texto || '').trim()); const enviar = async () => { if (busy) return; setBusy(true); try { let r; if (midia) { // multipart quando há anexo de mídia no cabeçalho const fd = new FormData(); fd.append('nome', nome); fd.append('categoria', cat); fd.append('texto', texto); fd.append('midia', midia); fd.append('rodape', rodape); fd.append('botoes', JSON.stringify(botoesValidos())); const t = localStorage.getItem('ezza_token'); r = await fetch('/api/whatsapp/templates', { method: 'POST', headers: { ...(t?{'Authorization':`Bearer ${t}`}:{}) }, body: fd }); } else { r = await fetch('/api/whatsapp/templates', { method: 'POST', headers: dspHeaders(), body: JSON.stringify({ nome, categoria: cat, texto, rodape, botoes: botoesValidos() }) }); } const d = await r.json(); if (d.ok) { toast('Template enviado pra verificação da Meta ✓ (aprovação leva de minutos a 48h)', { duration: 5000 }); onCreated && onCreated(); onClose(); } else toast(d.error || 'Meta recusou', { kind: 'err', duration: 5000 }); } catch (e) { toast('Erro de conexão', { kind: 'err' }); } setBusy(false); }; return (
e.stopPropagation()} style={{width:560}}>
Novo template
Vai pra fila de verificação da Meta antes de poder ser usado
{ICN.x}
{DSP_MODELOS.map(m => ( usarModelo(m)} style={{cursor:'default', fontSize:11.5}}>{m.rotulo} ))}
setNome(e.target.value)} />
{[['MARKETING','Marketing (promoções, reengajamento)'],['UTILITY','Utilidade (avisos de pedido, confirmações)']].map(([id,lbl])=>( setCat(id)} style={{cursor:'default', fontSize:11.5}}>{lbl} ))}
{!midiaPrev ? (
midiaRef.current && midiaRef.current.click()}> {ICN.upload || '📎'} Anexar imagem, vídeo ou PDF (vira o cabeçalho do template)
) : (
{midiaPrev.tipo==='imagem' ? :
{midiaPrev.tipo==='video' ? '🎬' : '📄'}
}
{midiaPrev.nome}
{midiaPrev.tipo} · cabeçalho do template
)}