Implementado envio automatico de WhatsApp por templates, broadcast com anti-spam e emoji picker
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s
Details
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s
Details
This commit is contained in:
parent
4f4fceab29
commit
4cc23a9e5b
96
server.ts
96
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, string>): 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({
|
||||
|
|
|
|||
|
|
@ -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<string, string> = {
|
||||
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<SupportTicket[]>([]);
|
||||
|
|
@ -53,29 +107,40 @@ export default function AdminMessagesView() {
|
|||
const [activeTemplateModal, setActiveTemplateModal] = useState<string | null>(null);
|
||||
const [currentTemplateText, setCurrentTemplateText] = useState('');
|
||||
const [isSavingTemplate, setIsSavingTemplate] = useState(false);
|
||||
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(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<HTMLTextAreaElement>(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() {
|
|||
<div>
|
||||
<h2 className="text-2xl font-bold text-white flex items-center space-x-2">
|
||||
<Mail className="w-6 h-6 text-[#E50914]" />
|
||||
<span>Mensagens e Comentários</span>
|
||||
<span>Mensagens e Comunicações</span>
|
||||
</h2>
|
||||
<p className="text-zinc-400 text-sm mt-1">Gerencie dúvidas, anotações de aulas e chamados de suporte dos alunos.</p>
|
||||
<p className="text-zinc-400 text-sm mt-1">Gerencie dúvidas, anotações e configure as mensagens automáticas do WhatsApp.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -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'}`}
|
||||
>
|
||||
<Smartphone className="w-4 h-4" />
|
||||
<span>Modelos de WhatsApp</span>
|
||||
<span>Modelos WhatsApp</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -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"
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => { setReplyingTo(null); setReplyText(''); }}
|
||||
className="px-3 py-1.5 text-xs text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button onClick={() => { setReplyingTo(null); setReplyText(''); }} className="px-3 py-1.5 text-xs text-zinc-400 hover:text-white transition-colors">Cancelar</button>
|
||||
<button
|
||||
onClick={() => handleReply(ticket.user.id, ticket.id)}
|
||||
disabled={!replyText.trim() || isSending}
|
||||
className="bg-[#E50914] hover:bg-red-700 text-white px-4 py-1.5 rounded text-xs font-bold transition-colors disabled:opacity-50 flex items-center space-x-2"
|
||||
>
|
||||
{isSending ? (
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-3 h-3" />
|
||||
<span>Enviar Resposta</span>
|
||||
</>
|
||||
)}
|
||||
{isSending ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <><Send className="w-3 h-3" /><span>Enviar Resposta</span></>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => { setReplyingTo(ticket.id); setReplyText(''); }}
|
||||
className="text-xs text-[#E50914] hover:text-red-400 transition-colors font-bold flex items-center space-x-1"
|
||||
>
|
||||
<button onClick={() => { setReplyingTo(ticket.id); setReplyText(''); }} className="text-xs text-[#E50914] hover:text-red-400 transition-colors font-bold flex items-center space-x-1">
|
||||
<Reply className="w-3 h-3" />
|
||||
<span>Responder</span>
|
||||
</button>
|
||||
|
|
@ -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"
|
||||
/>
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button
|
||||
onClick={() => { setReplyingTo(null); setReplyText(''); }}
|
||||
className="px-3 py-1.5 text-xs text-zinc-400 hover:text-white transition-colors"
|
||||
>
|
||||
Cancelar
|
||||
</button>
|
||||
<button onClick={() => { setReplyingTo(null); setReplyText(''); }} className="px-3 py-1.5 text-xs text-zinc-400 hover:text-white transition-colors">Cancelar</button>
|
||||
<button
|
||||
onClick={() => handleReply(note.user.id)}
|
||||
disabled={!replyText.trim() || isSending}
|
||||
className="bg-[#E50914] hover:bg-red-700 text-white px-4 py-1.5 rounded text-xs font-bold transition-colors disabled:opacity-50 flex items-center space-x-2"
|
||||
>
|
||||
{isSending ? (
|
||||
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
||||
) : (
|
||||
<>
|
||||
<Send className="w-3 h-3" />
|
||||
<span>Enviar Resposta</span>
|
||||
</>
|
||||
)}
|
||||
{isSending ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <><Send className="w-3 h-3" /><span>Enviar Resposta</span></>}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => { setReplyingTo(note.id); setReplyText(''); }}
|
||||
className="text-xs text-[#E50914] hover:text-red-400 transition-colors font-bold flex items-center space-x-1"
|
||||
>
|
||||
<button onClick={() => { setReplyingTo(note.id); setReplyText(''); }} className="text-xs text-[#E50914] hover:text-red-400 transition-colors font-bold flex items-center space-x-1">
|
||||
<Reply className="w-3 h-3" />
|
||||
<span>Responder</span>
|
||||
</button>
|
||||
|
|
@ -340,49 +420,171 @@ export default function AdminMessagesView() {
|
|||
)}
|
||||
|
||||
{activeTab === 'whatsapp_templates' && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-6">
|
||||
{whatsappTemplates.map(t => (
|
||||
<div
|
||||
key={t.id}
|
||||
className="bg-zinc-900 border border-white/10 rounded-xl p-5 hover:border-white/20 transition-colors cursor-pointer"
|
||||
onClick={() => { setCurrentTemplateText(settings?.[t.id] || ''); setActiveTemplateModal(t.id); }}
|
||||
>
|
||||
<div className="flex items-center space-x-4 mb-3">
|
||||
<div className="w-12 h-12 bg-black/40 rounded-full flex items-center justify-center text-2xl">
|
||||
{t.icon}
|
||||
<div className="space-y-6 mt-4">
|
||||
{/* Automatic Templates */}
|
||||
<div>
|
||||
<h3 className="text-sm font-bold text-zinc-400 uppercase tracking-wider mb-3 flex items-center gap-2">
|
||||
<Smartphone className="w-4 h-4 text-green-500" />
|
||||
Mensagens Automáticas
|
||||
</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
{whatsappTemplates.map(t => (
|
||||
<div
|
||||
key={t.id}
|
||||
className="bg-zinc-900 border border-white/10 rounded-xl p-5 hover:border-white/25 transition-all cursor-pointer group"
|
||||
onClick={() => {
|
||||
setCurrentTemplateText(settings?.[t.id] || DEFAULT_TEMPLATES[t.id] || '');
|
||||
setActiveTemplateModal(t.id);
|
||||
}}
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<span className="text-2xl">{t.icon}</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="text-sm font-bold text-white group-hover:text-[#E50914] transition-colors">{t.title}</h3>
|
||||
<p className="text-[11px] text-zinc-500 leading-snug mt-0.5">{t.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-black/30 p-2.5 rounded-lg text-[11px] text-zinc-400 line-clamp-3 min-h-[50px] whitespace-pre-wrap mb-2">
|
||||
{settings?.[t.id] || DEFAULT_TEMPLATES[t.id] || 'Clique para configurar...'}
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{t.tags.map(tag => (
|
||||
<span key={tag} className="text-[9px] bg-blue-500/10 text-blue-300 px-1.5 py-0.5 rounded font-mono">{tag}</span>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<div className={`w-2 h-2 rounded-full ${settings?.[t.id] ? 'bg-green-500' : 'bg-zinc-600'}`}></div>
|
||||
<span className={`text-[10px] font-bold ${settings?.[t.id] ? 'text-green-400' : 'text-zinc-500'}`}>
|
||||
{settings?.[t.id] ? 'Ativo' : 'Não configurado'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white">{t.title}</h3>
|
||||
<p className="text-xs text-zinc-500">{t.desc}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-black/30 p-3 rounded-lg text-xs text-zinc-400 line-clamp-2 min-h-[40px] whitespace-pre-wrap">
|
||||
{settings?.[t.id] || 'Clique para configurar esta mensagem...'}
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mass Broadcast */}
|
||||
<div className="bg-zinc-900 border border-amber-500/20 rounded-xl p-6">
|
||||
<div className="flex items-center gap-3 mb-4">
|
||||
<div className="w-10 h-10 bg-amber-500/10 rounded-full flex items-center justify-center text-xl">🚀</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-bold text-white flex items-center gap-2">
|
||||
<Megaphone className="w-5 h-5 text-amber-400" />
|
||||
Promoção em Massa
|
||||
</h3>
|
||||
<p className="text-xs text-zinc-500">Envia para todos os alunos que têm WhatsApp cadastrado.</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="bg-amber-500/5 border border-amber-500/15 p-3 rounded-lg text-xs text-amber-200 mb-4 space-y-1">
|
||||
<p><strong>⚠️ Anti-Spam:</strong> Configure o intervalo de envio para evitar bloqueio pelo WhatsApp.</p>
|
||||
<p><strong>Tag dinâmica:</strong> Use <code className="bg-black/40 px-1 rounded">{'{nome}'}</code> para personalizar com o primeiro nome do aluno.</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={broadcastRef}
|
||||
value={broadcastText}
|
||||
onChange={(e) => setBroadcastText(e.target.value)}
|
||||
className="w-full h-40 bg-black/40 border border-white/10 rounded-lg p-3 pr-12 text-white text-sm focus:outline-none focus:border-amber-500/50 resize-none"
|
||||
placeholder="Escreva a mensagem de promoção aqui..."
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<button
|
||||
onClick={() => setShowBroadcastEmojiPicker(!showBroadcastEmojiPicker)}
|
||||
className="w-8 h-8 flex items-center justify-center text-zinc-400 hover:text-amber-400 transition-colors"
|
||||
title="Inserir emoji"
|
||||
>
|
||||
<Smile className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
{showBroadcastEmojiPicker && (
|
||||
<div className="absolute bottom-12 right-3 bg-zinc-800 border border-white/10 rounded-xl p-3 grid grid-cols-6 gap-1.5 z-10 shadow-2xl">
|
||||
{EMOJI_LIST.map(emoji => (
|
||||
<button key={emoji} onClick={() => insertEmoji(emoji, true)} className="text-xl hover:scale-125 transition-transform p-1 rounded hover:bg-white/10">
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
<Clock className="w-4 h-4 text-amber-400 flex-shrink-0" />
|
||||
<label className="text-xs text-zinc-300 flex-shrink-0">Intervalo entre mensagens:</label>
|
||||
<input
|
||||
type="number"
|
||||
value={broadcastDelay}
|
||||
onChange={e => setBroadcastDelay(Math.max(5, Math.min(120, Number(e.target.value))))}
|
||||
min={5}
|
||||
max={120}
|
||||
className="w-20 bg-black/40 border border-white/10 rounded-lg px-3 py-1.5 text-white text-sm focus:outline-none focus:border-amber-500/50"
|
||||
/>
|
||||
<span className="text-xs text-zinc-500">segundos (mín: 5s / máx: 120s)</span>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
onClick={handleBroadcast}
|
||||
disabled={!broadcastText.trim() || isBroadcasting}
|
||||
className="bg-amber-600 hover:bg-amber-500 text-white px-6 py-2.5 rounded-lg font-bold transition-colors disabled:opacity-50 flex items-center space-x-2"
|
||||
>
|
||||
{isBroadcasting
|
||||
? <><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /><span>Iniciando...</span></>
|
||||
: <><Megaphone className="w-4 h-4" /><span>Enviar para Todos</span></>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CustomModal isOpen={!!activeTemplateModal} onClose={() => setActiveTemplateModal(null)} title={whatsappTemplates.find(t => t.id === activeTemplateModal)?.title || 'Editar Template'} isGlass={true}>
|
||||
{/* Template Edit Modal */}
|
||||
<CustomModal isOpen={!!activeTemplateModal} onClose={() => { setActiveTemplateModal(null); setShowEmojiPicker(false); }} title={whatsappTemplates.find(t => t.id === activeTemplateModal)?.title || 'Editar Template'} isGlass={true}>
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-500/10 border border-blue-500/20 p-3 rounded-lg text-xs text-blue-200 space-y-1">
|
||||
<p><strong>Formatação do WhatsApp:</strong> Use <code className="bg-black/40 px-1 rounded">*negrito*</code>, <code className="bg-black/40 px-1 rounded">_itálico_</code>, <code className="bg-black/40 px-1 rounded">~riscado~</code>.</p>
|
||||
<p><strong>Tags dinâmicas:</strong> Use <code className="bg-black/40 px-1 rounded font-bold text-blue-100">{'{nome}'}</code> para inserir o primeiro nome do aluno.</p>
|
||||
<p>Emojis são suportados nativamente (pressione Win + . no Windows ou Cmd + Ctrl + Espaço no Mac).</p>
|
||||
<p><strong>Tags disponíveis:</strong>{' '}
|
||||
{whatsappTemplates.find(t => t.id === activeTemplateModal)?.tags.map(tag => (
|
||||
<code key={tag} className="bg-black/40 px-1 rounded font-bold text-blue-100 mr-1">{tag}</code>
|
||||
))}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="relative">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
value={currentTemplateText}
|
||||
onChange={(e) => setCurrentTemplateText(e.target.value)}
|
||||
className="w-full h-52 bg-black/40 border border-white/10 rounded-lg p-3 pr-12 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none"
|
||||
placeholder="Escreva a mensagem aqui..."
|
||||
/>
|
||||
<div className="absolute bottom-3 right-3">
|
||||
<button
|
||||
onClick={() => setShowEmojiPicker(!showEmojiPicker)}
|
||||
className="w-8 h-8 flex items-center justify-center text-zinc-400 hover:text-[#E50914] transition-colors"
|
||||
title="Inserir emoji"
|
||||
>
|
||||
<Smile className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
{showEmojiPicker && (
|
||||
<div className="absolute bottom-12 right-3 bg-zinc-800 border border-white/10 rounded-xl p-3 grid grid-cols-6 gap-1.5 z-10 shadow-2xl">
|
||||
{EMOJI_LIST.map(emoji => (
|
||||
<button key={emoji} onClick={() => insertEmoji(emoji, false)} className="text-xl hover:scale-125 transition-transform p-1 rounded hover:bg-white/10">
|
||||
{emoji}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<textarea
|
||||
value={currentTemplateText}
|
||||
onChange={(e) => setCurrentTemplateText(e.target.value)}
|
||||
className="w-full h-48 bg-black/40 border border-white/10 rounded-lg p-3 text-white text-sm focus:outline-none focus:border-[#E50914] resize-none"
|
||||
placeholder="Escreva a mensagem aqui..."
|
||||
/>
|
||||
|
||||
<div className="flex justify-end space-x-2">
|
||||
<button onClick={() => setActiveTemplateModal(null)} className="px-4 py-2 text-zinc-400 hover:text-white transition-colors text-sm">
|
||||
<button onClick={() => { setActiveTemplateModal(null); setShowEmojiPicker(false); }} className="px-4 py-2 text-zinc-400 hover:text-white transition-colors text-sm">
|
||||
Cancelar
|
||||
</button>
|
||||
<button
|
||||
|
|
@ -396,29 +598,18 @@ export default function AdminMessagesView() {
|
|||
</div>
|
||||
</CustomModal>
|
||||
|
||||
<CustomModal
|
||||
isOpen={alertModal.isOpen}
|
||||
onClose={() => setAlertModal({...alertModal, isOpen: false})}
|
||||
title={alertModal.title}
|
||||
isGlass={true}
|
||||
>
|
||||
{/* Alert Modal */}
|
||||
<CustomModal isOpen={alertModal.isOpen} onClose={() => setAlertModal({...alertModal, isOpen: false})} title={alertModal.title} isGlass={true}>
|
||||
<div className="flex items-start gap-4">
|
||||
{alertModal.isError ? (
|
||||
<div className="bg-red-500/10 p-3 rounded-full shrink-0">
|
||||
<AlertCircle className="w-6 h-6 text-red-500" />
|
||||
</div>
|
||||
<div className="bg-red-500/10 p-3 rounded-full shrink-0"><AlertCircle className="w-6 h-6 text-red-500" /></div>
|
||||
) : (
|
||||
<div className="bg-green-500/10 p-3 rounded-full shrink-0">
|
||||
<CheckCircle2 className="w-6 h-6 text-green-500" />
|
||||
</div>
|
||||
<div className="bg-green-500/10 p-3 rounded-full shrink-0"><CheckCircle2 className="w-6 h-6 text-green-500" /></div>
|
||||
)}
|
||||
<div className="flex-1">
|
||||
<p className="text-zinc-300 text-base">{alertModal.message}</p>
|
||||
<div className="mt-6 flex justify-end">
|
||||
<button
|
||||
onClick={() => setAlertModal({...alertModal, isOpen: false})}
|
||||
className="px-6 py-2 bg-zinc-800 hover:bg-zinc-700 text-white rounded-lg font-bold transition-colors"
|
||||
>
|
||||
<button onClick={() => setAlertModal({...alertModal, isOpen: false})} className="px-6 py-2 bg-zinc-800 hover:bg-zinc-700 text-white rounded-lg font-bold transition-colors">
|
||||
OK
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue