diff --git a/server.ts b/server.ts index d0c1496..e4a4dae 100644 --- a/server.ts +++ b/server.ts @@ -189,6 +189,16 @@ app.post('/api/auth/register', async (req: Request, res: Response) => { { expiresIn: '7d' } ); + // Send welcome WhatsApp message if template configured + if (whatsapp) { + const welcomeSettings = await prisma.systemSettings.findFirst(); + if (welcomeSettings?.whatsappTemplateWelcome) { + const firstName = name.split(' ')[0]; + const msg = processTemplate(welcomeSettings.whatsappTemplateWelcome, { nome: firstName }); + sendWhatsappNotification(whatsapp, msg).catch(console.error); + } + } + res.status(201).json({ token, user: newUser @@ -1558,6 +1568,11 @@ app.post('/api/admin/whatsapp/logout', authenticateToken, requireAdmin, async (r } }); +export function processTemplate(template: string | null | undefined, vars: Record): string { + if (!template) return ''; + return template.replace(/\{(\w+)\}/g, (_, key) => vars[key] || `{${key}}`); +} + export async function sendWhatsappNotification(phone: string, text: string) { try { const settings = await prisma.systemSettings.findFirst(); @@ -1569,8 +1584,8 @@ export async function sendWhatsappNotification(phone: string, text: string) { let cleanPhone = phone.replace(/\D/g, ''); if (!cleanPhone) return false; - // Add DDI 55 for Brazil if not present - if (cleanPhone.length === 10 || cleanPhone.length === 11) { + // Add DDI 55 for Brazil if not present (10 or 11 digits = local number) + if (cleanPhone.length <= 11) { cleanPhone = '55' + cleanPhone; } @@ -1986,12 +2001,44 @@ app.post('/api/webhooks/asaas', async (req: Request, res: Response) => { data: { unlockedCourses: newUnlocked } }); console.log(`Webhook: Curso ${courseIdToUnlock} LIBERADO para usuário ${user.email}`); + + // Notify via WhatsApp - Payment confirmed (course) + const pSettings = await prisma.systemSettings.findFirst(); + if (pSettings?.whatsappTemplatePayment && user.whatsapp) { + const firstName = user.name.split(' ')[0]; + const course = await prisma.course.findUnique({ where: { id: courseIdToUnlock }, select: { title: true } }); + const msg = processTemplate(pSettings.whatsappTemplatePayment, { + nome: firstName, + curso: course?.title || 'Curso', + plano: '', + valor: payment.value ? `R$ ${Number(payment.value).toFixed(2).replace('.', ',')}` : '' + }); + sendWhatsappNotification(user.whatsapp, msg).catch(console.error); + } } else { await prisma.user.update({ where: { id: user.id }, data: { subscriptionStatus: 'ACTIVE' } }); console.log(`Webhook: Assinatura LIBERADA para usuário ${user.email}`); + + // Notify via WhatsApp - Payment confirmed (plan) + const pSettings = await prisma.systemSettings.findFirst(); + if (pSettings?.whatsappTemplatePayment && user.whatsapp) { + const firstName = user.name.split(' ')[0]; + let planTitle = 'Assinatura'; + if (existingPayment?.planId) { + const plan = await prisma.plan.findUnique({ where: { id: existingPayment.planId }, select: { name: true } }); + planTitle = plan?.name || 'Assinatura'; + } + const msg = processTemplate(pSettings.whatsappTemplatePayment, { + nome: firstName, + curso: '', + plano: planTitle, + valor: payment.value ? `R$ ${Number(payment.value).toFixed(2).replace('.', ',')}` : '' + }); + sendWhatsappNotification(user.whatsapp, msg).catch(console.error); + } } } else if (isOverdue || isRefunded || event === 'SUBSCRIPTION_DELETED') { if (!isCoursePayment) { @@ -2515,6 +2562,51 @@ app.post('/api/admin/reply-message', authenticateToken, requireAdmin, async (req } }); +// Admin: Broadcast WhatsApp message to all students with anti-spam delay +app.post('/api/admin/whatsapp/broadcast', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { + try { + const { message, delaySeconds } = req.body; + if (!message) return res.status(400).json({ error: 'message é obrigatório' }); + + const settings = await prisma.systemSettings.findFirst(); + if (!settings?.whatsappConnected || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return res.status(400).json({ error: 'WhatsApp não está configurado ou conectado.' }); + } + + const students = await prisma.user.findMany({ + where: { role: 'student', NOT: { whatsapp: null } }, + select: { id: true, name: true, whatsapp: true } + }); + + const delayMs = Math.max(5, Math.min(Number(delaySeconds) || 10, 120)) * 1000; + + res.json({ success: true, total: students.length, message: `Envio iniciado para ${students.length} alunos com intervalo de ${delayMs / 1000}s.` }); + + // Fire and forget + (async () => { + let sent = 0; + let failed = 0; + for (const student of students) { + if (!student.whatsapp) continue; + try { + const firstName = student.name.split(' ')[0]; + const msg = processTemplate(message, { nome: firstName }); + await sendWhatsappNotification(student.whatsapp, msg); + sent++; + } catch (e) { + failed++; + console.error(`Broadcast: failed to send to ${student.whatsapp}`, e); + } + await new Promise(r => setTimeout(r, delayMs)); + } + console.log(`[Broadcast WhatsApp] Concluído: ${sent} enviados, ${failed} falhos de ${students.length} alunos.`); + })(); + } catch (error) { + console.error('Error broadcasting:', error); + res.status(500).json({ error: 'Internal server error' }); + } +}); + if (process.env.NODE_ENV !== 'production') { const { createServer: createViteServer } = await import('vite'); const vite = await createViteServer({ diff --git a/src/components/AdminMessagesView.tsx b/src/components/AdminMessagesView.tsx index 35b048f..8ec0f8b 100644 --- a/src/components/AdminMessagesView.tsx +++ b/src/components/AdminMessagesView.tsx @@ -1,7 +1,6 @@ -import React, { useState, useEffect } from 'react'; -import { Mail, MessageSquare, BookOpen, Send, User, Reply, CheckCircle2, AlertCircle } from 'lucide-react'; +import React, { useState, useEffect, useRef } from 'react'; +import { Mail, MessageSquare, BookOpen, Send, User, Reply, CheckCircle2, AlertCircle, Smartphone, Save, Smile, Megaphone, Clock } from 'lucide-react'; import CustomModal from './CustomModal'; -import { Smartphone, Save } from 'lucide-react'; interface SupportTicket { id: string; @@ -38,6 +37,61 @@ interface Note { }; } +// Simple emoji picker groups +const EMOJI_LIST = [ + '😊','😁','🎉','🙏','💪','🚀','❤️','✅','⭐','🔥','💡','📚','🎓','👏','🤝', + '💰','💳','🛒','📱','📧','🌟','🏆','✨','🎯','📢','👋','😀','🥳','💎','🌈' +]; + +// Default template texts +const DEFAULT_TEMPLATES: Record = { + whatsappTemplatePayment: `Olá, *{nome}*! 🎉 + +Recebemos a confirmação do seu pagamento com sucesso! + +💰 *Valor:* {valor} +📦 *Plano/Curso:* {plano}{curso} + +Seu acesso já está liberado na plataforma. Qualquer dúvida, estamos à disposição. + +Muito obrigado pela sua confiança! 🙏 +_Equipe MicrotecFlix_`, + + whatsappTemplateWelcome: `Olá, *{nome}*! 👋 + +Seja muito bem-vindo(a) à *MicrotecFlix*! 🎓🚀 + +Estamos felizes em ter você conosco. Agora você tem acesso à nossa plataforma de cursos de informática e pacote Office. + +Acesse: https://estudo.microtecinformaticacurso.com.br + +Se tiver qualquer dúvida, é só chamar. 😊 +_Equipe MicrotecFlix_`, + + whatsappTemplateNewContent: `Oi, *{nome}*! 🎬 + +Temos uma novidade imperdível para você na plataforma! + +⭐ *Novo conteúdo disponível:* Acabamos de publicar um novo curso/módulo/aula. + +Acesse agora mesmo e aproveite: https://estudo.microtecinformaticacurso.com.br + +Bons estudos! 💪 +_Equipe MicrotecFlix_`, + + whatsappTemplatePromo: `Oi, *{nome}*! 🌟 + +Temos uma oferta especial para você hoje! + +🎯 Aproveite nossa promoção exclusiva e acelere seus estudos de informática e pacote Office. + +Não perca essa oportunidade única! Acesse agora: +https://estudo.microtecinformaticacurso.com.br + +Qualquer dúvida, estamos aqui. 🙏 +_Equipe MicrotecFlix_` +}; + export default function AdminMessagesView() { const [activeTab, setActiveTab] = useState<'tickets' | 'notes' | 'whatsapp_templates'>('tickets'); const [tickets, setTickets] = useState([]); @@ -53,29 +107,40 @@ export default function AdminMessagesView() { const [activeTemplateModal, setActiveTemplateModal] = useState(null); const [currentTemplateText, setCurrentTemplateText] = useState(''); const [isSavingTemplate, setIsSavingTemplate] = useState(false); + const [showEmojiPicker, setShowEmojiPicker] = useState(false); + const textareaRef = useRef(null); + + // Broadcast state + const [broadcastText, setBroadcastText] = useState(''); + const [broadcastDelay, setBroadcastDelay] = useState(10); + const [isBroadcasting, setIsBroadcasting] = useState(false); + const [showBroadcastEmojiPicker, setShowBroadcastEmojiPicker] = useState(false); + const broadcastRef = useRef(null); useEffect(() => { - fetchMessages(); + fetchData(); }, []); - const fetchMessages = async () => { + const fetchData = async () => { setIsLoading(true); try { const token = localStorage.getItem('admin_token'); - const res = await fetch('/api/admin/messages', { - headers: { 'Authorization': `Bearer ${token}` } - }); - if (res.ok) { - const data = await res.json(); + const [msgRes, settingsRes] = await Promise.all([ + fetch('/api/admin/messages', { headers: { 'Authorization': `Bearer ${token}` } }), + fetch('/api/admin/settings', { headers: { 'Authorization': `Bearer ${token}` } }) + ]); + if (msgRes.ok) { + const data = await msgRes.json(); setTickets(data.tickets); setNotes(data.notes); } - const resSettings = await fetch('/api/admin/settings', { - headers: { 'Authorization': `Bearer ${token}` } - }); - if (resSettings.ok) { - const data = await resSettings.json(); + if (settingsRes.ok) { + const data = await settingsRes.json(); setSettings(data); + // Pre-fill broadcast from template + if (data.whatsappTemplatePromo) { + setBroadcastText(data.whatsappTemplatePromo); + } } } catch (error) { console.error('Error fetching data:', error); @@ -100,7 +165,7 @@ export default function AdminMessagesView() { if (res.ok) { setReplyText(''); setReplyingTo(null); - fetchMessages(); + fetchData(); setAlertModal({isOpen: true, title: 'Sucesso', message: 'Resposta enviada com sucesso!'}); } else { setAlertModal({isOpen: true, title: 'Erro', message: 'Erro ao enviar resposta.', isError: true}); @@ -125,7 +190,7 @@ export default function AdminMessagesView() { if (res.ok) { setSettings({...settings, [field]: currentTemplateText}); setActiveTemplateModal(null); - setAlertModal({isOpen: true, title: 'Sucesso', message: 'Template salvo com sucesso!'}); + setAlertModal({isOpen: true, title: 'Sucesso', message: 'Template salvo com sucesso! O envio automático já está ativado.'}); } else { setAlertModal({isOpen: true, title: 'Erro', message: 'Erro ao salvar template.', isError: true}); } @@ -137,11 +202,56 @@ export default function AdminMessagesView() { } }; + const insertEmoji = (emoji: string, isForBroadcast = false) => { + const ref = isForBroadcast ? broadcastRef : textareaRef; + const setter = isForBroadcast ? setBroadcastText : setCurrentTemplateText; + const current = isForBroadcast ? broadcastText : currentTemplateText; + if (ref.current) { + const start = ref.current.selectionStart; + const end = ref.current.selectionEnd; + const newText = current.substring(0, start) + emoji + current.substring(end); + setter(newText); + setTimeout(() => { + if (ref.current) { + ref.current.focus(); + ref.current.setSelectionRange(start + emoji.length, start + emoji.length); + } + }, 0); + } else { + setter(current + emoji); + } + if (isForBroadcast) setShowBroadcastEmojiPicker(false); + else setShowEmojiPicker(false); + }; + + const handleBroadcast = async () => { + if (!broadcastText.trim()) return; + setIsBroadcasting(true); + try { + const token = localStorage.getItem('admin_token'); + const res = await fetch('/api/admin/whatsapp/broadcast', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ message: broadcastText, delaySeconds: broadcastDelay }) + }); + const data = await res.json(); + if (res.ok) { + setAlertModal({isOpen: true, title: 'Envio Iniciado! 🚀', message: data.message || 'Broadcast iniciado com sucesso!'}); + } else { + setAlertModal({isOpen: true, title: 'Erro', message: data.error || 'Erro ao iniciar broadcast.', isError: true}); + } + } catch (error) { + console.error('Error broadcasting:', error); + setAlertModal({isOpen: true, title: 'Erro', message: 'Erro interno ao enviar.', isError: true}); + } finally { + setIsBroadcasting(false); + } + }; + const whatsappTemplates = [ - { id: 'whatsappTemplatePayment', title: 'Pagamento Confirmado', desc: 'Enviada quando o aluno paga um plano ou curso.', icon: '💰' }, - { id: 'whatsappTemplateWelcome', title: 'Boas-Vindas (Nova Conta)', desc: 'Enviada quando o aluno cria a conta.', icon: '👋' }, - { id: 'whatsappTemplateNewContent', title: 'Novo Conteúdo (Carrossel)', desc: 'Enviada ao postar curso/módulo/aula.', icon: '🎬' }, - { id: 'whatsappTemplatePromo', title: 'Promoção em Massa', desc: 'Mensagem para todos os clientes.', icon: '🚀' }, + { id: 'whatsappTemplatePayment', title: 'Pagamento Confirmado', desc: 'Enviada automaticamente quando o aluno paga um plano ou curso.', icon: '💰', tags: ['{nome}', '{valor}', '{plano}', '{curso}'] }, + { id: 'whatsappTemplateWelcome', title: 'Boas-Vindas (Nova Conta)', desc: 'Enviada automaticamente quando o aluno cria uma conta.', icon: '👋', tags: ['{nome}'] }, + { id: 'whatsappTemplateNewContent', title: 'Novo Conteúdo (Carrossel)', desc: 'Enviada manualmente ao postar curso/módulo/aula.', icon: '🎬', tags: ['{nome}'] }, ]; return ( @@ -150,9 +260,9 @@ export default function AdminMessagesView() {

- Mensagens e Comentários + Mensagens e Comunicações

-

Gerencie dúvidas, anotações de aulas e chamados de suporte dos alunos.

+

Gerencie dúvidas, anotações e configure as mensagens automáticas do WhatsApp.

@@ -176,7 +286,7 @@ export default function AdminMessagesView() { className={`pb-3 px-1 text-sm font-bold flex items-center space-x-2 border-b-2 transition-colors whitespace-nowrap ${activeTab === 'whatsapp_templates' ? 'border-[#E50914] text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`} > - Modelos de WhatsApp + Modelos WhatsApp @@ -225,33 +335,18 @@ export default function AdminMessagesView() { className="w-full h-24 bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none" />
- +
) : ( - @@ -303,33 +398,18 @@ export default function AdminMessagesView() { className="w-full h-24 bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none" />
- +
) : ( - @@ -340,49 +420,171 @@ export default function AdminMessagesView() { )} {activeTab === 'whatsapp_templates' && ( -
- {whatsappTemplates.map(t => ( -
{ setCurrentTemplateText(settings?.[t.id] || ''); setActiveTemplateModal(t.id); }} - > -
-
- {t.icon} +
+ {/* Automatic Templates */} +
+

+ + Mensagens Automáticas +

+
+ {whatsappTemplates.map(t => ( +
{ + setCurrentTemplateText(settings?.[t.id] || DEFAULT_TEMPLATES[t.id] || ''); + setActiveTemplateModal(t.id); + }} + > +
+ {t.icon} +
+

{t.title}

+

{t.desc}

+
+
+
+ {settings?.[t.id] || DEFAULT_TEMPLATES[t.id] || 'Clique para configurar...'} +
+
+ {t.tags.map(tag => ( + {tag} + ))} +
+
+
+ + {settings?.[t.id] ? 'Ativo' : 'Não configurado'} + +
-
-

{t.title}

-

{t.desc}

-
-
-
- {settings?.[t.id] || 'Clique para configurar esta mensagem...'} + ))} +
+
+ + {/* Mass Broadcast */} +
+
+
🚀
+
+

+ + Promoção em Massa +

+

Envia para todos os alunos que têm WhatsApp cadastrado.

- ))} + +
+

⚠️ Anti-Spam: Configure o intervalo de envio para evitar bloqueio pelo WhatsApp.

+

Tag dinâmica: Use {'{nome}'} para personalizar com o primeiro nome do aluno.

+
+ +
+
+