diff --git a/server.ts b/server.ts index e5c41ca..268ee94 100644 --- a/server.ts +++ b/server.ts @@ -1607,6 +1607,50 @@ export async function sendWhatsappNotification(phone: string, text: string) { } } +// Sends media (image, video, PDF) via Evolution API v2 +export async function sendWhatsappMedia(phone: string, mediaBase64: string, mediaType: string, caption: string, filename?: string) { + try { + const settings = await prisma.systemSettings.findFirst(); + if (!settings?.whatsappConnected || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return false; + } + + let cleanPhone = phone.replace(/\D/g, ''); + if (!cleanPhone) return false; + if (cleanPhone.length <= 11) cleanPhone = '55' + cleanPhone; + + const apiUrl = settings.evolutionApiUrl.replace(/\/$/, ''); + const instance = settings.evolutionInstance || 'microtecflix'; + + // Determine mediatype for Evolution API: image, video, document, audio + let evolutionMediaType: string; + if (mediaType.startsWith('image/')) evolutionMediaType = 'image'; + else if (mediaType.startsWith('video/')) evolutionMediaType = 'video'; + else if (mediaType === 'application/pdf') evolutionMediaType = 'document'; + else evolutionMediaType = 'document'; + + const payload: any = { + number: cleanPhone, + mediatype: evolutionMediaType, + mimetype: mediaType, + media: mediaBase64, + delay: 1200 + }; + + if (caption) payload.caption = caption; + if (filename) payload.fileName = filename; + + await axios.post(`${apiUrl}/message/sendMedia/${instance}`, payload, { + headers: { apikey: settings.evolutionApiKey, 'Content-Type': 'application/json' } + }); + + return true; + } catch (err) { + console.error('Falha ao enviar mídia WhatsApp:', err); + return false; + } +} + // Admin manually unlock or lock a course for a student app.post('/api/admin/students/:id/unlock-course', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { const { courseId, unlocked } = req.body; @@ -2616,6 +2660,27 @@ app.post('/api/admin/whatsapp/test-template', authenticateToken, requireAdmin, a } }); +// Admin: Send media file along with WhatsApp message +app.post('/api/admin/whatsapp/send-media', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { + try { + const { phone, caption, mediaBase64, mediaType, fileName } = req.body; + if (!phone || !mediaBase64 || !mediaType) { + return res.status(400).json({ error: 'phone, mediaBase64 e mediaType são obrigatórios.' }); + } + + const success = await sendWhatsappMedia(phone, mediaBase64, mediaType, caption || '', fileName); + + if (success) { + res.json({ success: true, message: 'Mídia enviada com sucesso!' }); + } else { + res.status(500).json({ error: 'Falha ao enviar mídia. Verifique a conexão da Evolution API.' }); + } + } catch (error) { + console.error('Error sending media:', 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 { diff --git a/src/components/AdminMessagesView.tsx b/src/components/AdminMessagesView.tsx index d1d34e4..873cd1b 100644 --- a/src/components/AdminMessagesView.tsx +++ b/src/components/AdminMessagesView.tsx @@ -1,5 +1,5 @@ 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 { Mail, MessageSquare, BookOpen, Send, User, Reply, CheckCircle2, AlertCircle, Smartphone, Save, Smile, Megaphone, Clock, Paperclip } from 'lucide-react'; import CustomModal from './CustomModal'; interface SupportTicket { @@ -108,8 +108,11 @@ export default function AdminMessagesView() { const [currentTemplateText, setCurrentTemplateText] = useState(''); const [isSavingTemplate, setIsSavingTemplate] = useState(false); const [isTestingTemplate, setIsTestingTemplate] = useState(null); + const [isSendingMedia, setIsSendingMedia] = useState(null); + const [activeMediaTemplate, setActiveMediaTemplate] = useState(null); const [showEmojiPicker, setShowEmojiPicker] = useState(false); const textareaRef = useRef(null); + const fileInputRef = useRef(null); // Broadcast state const [broadcastText, setBroadcastText] = useState(''); @@ -227,6 +230,77 @@ export default function AdminMessagesView() { } }; + const triggerMediaSelect = (templateId: string, e: React.MouseEvent) => { + e.stopPropagation(); + setActiveMediaTemplate(templateId); + if (fileInputRef.current) { + fileInputRef.current.click(); + } + }; + + const handleMediaSelect = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file || !activeMediaTemplate) return; + + if (fileInputRef.current) fileInputRef.current.value = ''; // Reset input + + // In a real app, this should test with admin's profile phone number. + // For now we will rely on the backend checking settings for adminWhatsapp? No, the backend route expects `phone`. + // Let's use the admin's whatsapp saved in settings, or prompt if missing. + setIsSendingMedia(activeMediaTemplate); + + try { + // First get admin's phone from their profile or settings. We'll fetch profile since it's the admin. + const token = localStorage.getItem('admin_token'); + const profileRes = await fetch('/api/users/profile', { + headers: { 'Authorization': `Bearer ${token}` } + }); + const profileData = await profileRes.json(); + + const adminPhone = profileData.whatsapp; + if (!adminPhone) { + setAlertModal({isOpen: true, title: 'Atenção', message: 'Configure seu número de WhatsApp no Perfil do Admin antes de testar mídias.', isError: true}); + setIsSendingMedia(null); + setActiveMediaTemplate(null); + return; + } + + const reader = new FileReader(); + reader.onloadend = async () => { + const result = reader.result as string; + const mediaBase64 = result.split(',')[1]; + const mediaType = file.type; + + const res = await fetch('/api/admin/whatsapp/send-media', { + method: 'POST', + headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' }, + body: JSON.stringify({ + phone: adminPhone, + caption: 'Teste de anexo', + mediaBase64, + mediaType, + fileName: file.name + }) + }); + + const data = await res.json(); + if (res.ok) { + setAlertModal({isOpen: true, title: 'Mídia Enviada! 📎📱', message: data.message}); + } else { + setAlertModal({isOpen: true, title: 'Erro no Teste', message: data.error, isError: true}); + } + setIsSendingMedia(null); + setActiveMediaTemplate(null); + }; + reader.readAsDataURL(file); + } catch (error) { + console.error('Error sending media:', error); + setAlertModal({isOpen: true, title: 'Erro', message: 'Erro interno ao processar e enviar mídia.', isError: true}); + setIsSendingMedia(null); + setActiveMediaTemplate(null); + } + }; + const insertEmoji = (emoji: string, isForBroadcast = false) => { const ref = isForBroadcast ? broadcastRef : textareaRef; const setter = isForBroadcast ? setBroadcastText : setCurrentTemplateText; @@ -280,7 +354,15 @@ export default function AdminMessagesView() { ]; return ( -
+
+ +

@@ -486,20 +568,34 @@ export default function AdminMessagesView() {

{settings?.[t.id] && ( - +
+ + +
)}
@@ -707,6 +803,7 @@ export default function AdminMessagesView() {
+ ); }