Adicionado endpoint e botao para Testar Envio de Template do WhatsApp
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 58s Details

This commit is contained in:
Sidney Gomes 2026-07-25 21:19:48 -03:00
parent 4cc23a9e5b
commit 3b0313d5c0
2 changed files with 103 additions and 5 deletions

View File

@ -2562,6 +2562,60 @@ app.post('/api/admin/reply-message', authenticateToken, requireAdmin, async (req
}
});
// Admin: Test WhatsApp template message
app.post('/api/admin/whatsapp/test-template', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => {
try {
const { templateId } = req.body;
if (!templateId) return res.status(400).json({ error: 'templateId é obrigatório' });
const adminUser = await prisma.user.findUnique({ where: { id: req.user!.id } });
if (!adminUser?.whatsapp) {
return res.status(400).json({ error: 'Você precisa ter um número de WhatsApp configurado no seu perfil para receber o teste.' });
}
const settings = await prisma.systemSettings.findFirst();
let templateStr = '';
switch(templateId) {
case 'whatsappTemplatePayment':
templateStr = settings?.whatsappTemplatePayment || '';
break;
case 'whatsappTemplateWelcome':
templateStr = settings?.whatsappTemplateWelcome || '';
break;
case 'whatsappTemplateNewContent':
templateStr = settings?.whatsappTemplateNewContent || '';
break;
case 'whatsappTemplatePromo':
templateStr = settings?.whatsappTemplatePromo || '';
break;
}
if (!templateStr) {
return res.status(400).json({ error: 'Este modelo de mensagem ainda não foi configurado ou está vazio.' });
}
const firstName = adminUser.name.split(' ')[0];
const msg = processTemplate(templateStr, {
nome: firstName,
curso: 'Curso de Informática (TESTE)',
plano: 'Plano Premium (TESTE)',
valor: 'R$ 49,90'
});
const success = await sendWhatsappNotification(adminUser.whatsapp, msg);
if (success) {
res.json({ success: true, message: 'Mensagem de teste enviada com sucesso para o seu número!' });
} else {
res.status(500).json({ error: 'Falha ao enviar mensagem pela Evolution API. Verifique a conexão e o status da instância.' });
}
} catch (error) {
console.error('Error testing template:', error);
res.status(500).json({ error: 'Internal server error' });
}
});
// 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 {

View File

@ -107,6 +107,7 @@ export default function AdminMessagesView() {
const [activeTemplateModal, setActiveTemplateModal] = useState<string | null>(null);
const [currentTemplateText, setCurrentTemplateText] = useState('');
const [isSavingTemplate, setIsSavingTemplate] = useState(false);
const [isTestingTemplate, setIsTestingTemplate] = useState<string | null>(null);
const [showEmojiPicker, setShowEmojiPicker] = useState(false);
const textareaRef = useRef<HTMLTextAreaElement>(null);
@ -202,6 +203,30 @@ export default function AdminMessagesView() {
}
};
const handleTestTemplate = async (templateId: string, e: React.MouseEvent) => {
e.stopPropagation();
setIsTestingTemplate(templateId);
try {
const token = localStorage.getItem('admin_token');
const res = await fetch('/api/admin/whatsapp/test-template', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ templateId })
});
const data = await res.json();
if (res.ok) {
setAlertModal({isOpen: true, title: 'Enviado! 📱', message: data.message});
} else {
setAlertModal({isOpen: true, title: 'Erro no Teste', message: data.error, isError: true});
}
} catch (error) {
console.error('Error testing template:', error);
setAlertModal({isOpen: true, title: 'Erro', message: 'Erro interno ao testar template.', isError: true});
} finally {
setIsTestingTemplate(null);
}
};
const insertEmoji = (emoji: string, isForBroadcast = false) => {
const ref = isForBroadcast ? broadcastRef : textareaRef;
const setter = isForBroadcast ? setBroadcastText : setCurrentTemplateText;
@ -452,12 +477,31 @@ export default function AdminMessagesView() {
<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="mt-3 flex items-center justify-between">
<div className="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>
{settings?.[t.id] && (
<button
onClick={(e) => handleTestTemplate(t.id, e)}
disabled={isTestingTemplate === t.id}
className="bg-zinc-800 hover:bg-zinc-700 text-zinc-300 px-3 py-1 rounded text-[10px] font-bold flex items-center gap-1 transition-colors"
>
{isTestingTemplate === t.id ? (
<div className="w-3 h-3 border-2 border-white/30 border-t-white rounded-full animate-spin" />
) : (
<>
<Send className="w-3 h-3" />
<span>Testar Envio</span>
</>
)}
</button>
)}
</div>
</div>
))}
</div>