diff --git a/package.json b/package.json index 30ff9f9..a9eb7eb 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "multer": "^1.4.5-lts.1", "@tailwindcss/vite": "^4.1.14", "@vitejs/plugin-react": "^5.0.4", + "axios": "^1.7.9", "bcryptjs": "^3.0.3", "dotenv": "^17.2.3", "express": "^4.21.2", diff --git a/prisma/schema.prisma b/prisma/schema.prisma index d14aa3a..e877d02 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -238,6 +238,12 @@ model SystemSettings { asaasWebhookUrl String @default("") @map("asaas_webhook_url") asaasWebhookSecret String @default("") @map("asaas_webhook_secret") helpText String @db.Text @default("# Central de Ajuda") @map("help_text") + + evolutionApiUrl String @default("https://evolution.microtecinformaticacurso.com.br") @map("evolution_api_url") + evolutionApiKey String @default("") @map("evolution_api_key") + evolutionInstance String @default("microtecflix") @map("evolution_instance") + whatsappConnected Boolean @default(false) @map("whatsapp_connected") + whatsappPhoneNumber String? @map("whatsapp_phone_number") @@map("system_settings") } diff --git a/server.ts b/server.ts index 8893db3..c6c7226 100644 --- a/server.ts +++ b/server.ts @@ -3,6 +3,7 @@ import path from 'path'; import jwt from 'jsonwebtoken'; import bcrypt from 'bcryptjs'; import multer from 'multer'; +import axios from 'axios'; import { S3Client, PutObjectCommand, CreateBucketCommand, HeadBucketCommand, PutBucketPolicyCommand } from '@aws-sdk/client-s3'; import { createServer as createViteServer } from 'vite'; import { prisma } from './src/db/prisma'; @@ -986,7 +987,12 @@ app.get('/api/admin/settings', authenticateToken, requireAdmin, async (req: Auth asaasApiKey: settings?.asaasApiKey || '', asaasWebhookUrl: settings?.asaasWebhookUrl || 'https://admin-estudo.microtecinformaticacurso.com.br/api/webhooks/asaas', asaasWebhookSecret: settings?.asaasWebhookSecret || 'devflix_webhook_secret_key', - helpText: settings?.helpText || '' + helpText: settings?.helpText || '', + evolutionApiUrl: settings?.evolutionApiUrl || 'https://evolution.microtecinformaticacurso.com.br', + evolutionApiKey: settings?.evolutionApiKey || '', + evolutionInstance: settings?.evolutionInstance || 'microtecflix', + whatsappConnected: settings?.whatsappConnected || false, + whatsappPhoneNumber: settings?.whatsappPhoneNumber || '' }); }); @@ -1019,6 +1025,119 @@ app.post('/api/admin/test-asaas', authenticateToken, requireAdmin, async (req: A } }); +// --- EVOLUTION API WHATSAPP --- +app.get('/api/admin/whatsapp/qrcode', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { + try { + const settings = await prisma.systemSettings.findFirst(); + if (!settings || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return res.status(400).json({ error: 'Credenciais da Evolution API não configuradas.' }); + } + + const apiUrl = settings.evolutionApiUrl.replace(/\/$/, ''); + const instance = settings.evolutionInstance || 'microtecflix'; + const apiKey = settings.evolutionApiKey; + + const headers = { apikey: apiKey }; + + // Check connection state + try { + const stateRes = await axios.get(`${apiUrl}/instance/connectionState/${instance}`, { headers }); + + if (stateRes.data && stateRes.data.instance && stateRes.data.instance.state === 'open') { + if (!settings.whatsappConnected) { + await prisma.systemSettings.update({ + where: { id: settings.id }, + data: { whatsappConnected: true } + }); + } + return res.json({ connected: true, status: 'open' }); + } + } catch (err: any) { + if (err.response?.status === 404) { + // Instance does not exist, create it + await axios.post(`${apiUrl}/instance/create`, { + instanceName: instance, + qrcode: true + }, { headers }); + } else { + throw err; + } + } + + // Get QR Code + const qrRes = await axios.get(`${apiUrl}/instance/connect/${instance}`, { headers }); + + // Some versions of evolution return { base64: "...", pairingCode: "..." } + res.json({ + connected: false, + qrCode: qrRes.data.base64 || qrRes.data.qrcode || null, + pairingCode: qrRes.data.pairingCode || null + }); + + } catch (err: any) { + console.error('Erro na Evolution API (QR Code):', err.message); + res.status(500).json({ error: 'Falha ao comunicar com Evolution API.' }); + } +}); + +app.post('/api/admin/whatsapp/logout', authenticateToken, requireAdmin, async (req: AuthenticatedRequest, res: Response) => { + try { + const settings = await prisma.systemSettings.findFirst(); + if (!settings || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return res.status(400).json({ error: 'Credenciais não configuradas.' }); + } + + const apiUrl = settings.evolutionApiUrl.replace(/\/$/, ''); + const instance = settings.evolutionInstance || 'microtecflix'; + const apiKey = settings.evolutionApiKey; + + try { + await axios.delete(`${apiUrl}/instance/logout/${instance}`, { headers: { apikey: apiKey } }); + } catch (err: any) { + // ignore 404 or others during logout + } + + await prisma.systemSettings.update({ + where: { id: settings.id }, + data: { whatsappConnected: false } + }); + + res.json({ success: true }); + } catch (err: any) { + console.error('Erro na Evolution API (Logout):', err.message); + res.status(500).json({ error: 'Falha ao desconectar Evolution API.' }); + } +}); + +export async function sendWhatsappNotification(phone: string, text: string) { + try { + const settings = await prisma.systemSettings.findFirst(); + if (!settings?.whatsappConnected || !settings.evolutionApiUrl || !settings.evolutionApiKey) { + return false; // Not configured or not connected + } + + // Clean phone number (leave only digits) + const cleanPhone = phone.replace(/\D/g, ''); + if (!cleanPhone) return false; + + const apiUrl = settings.evolutionApiUrl.replace(/\/$/, ''); + const instance = settings.evolutionInstance || 'microtecflix'; + + await axios.post(`${apiUrl}/message/sendText/${instance}`, { + number: cleanPhone, + options: { delay: 1200 }, + textMessage: { text } + }, { + headers: { apikey: settings.evolutionApiKey } + }); + + return true; + } catch (err) { + console.error('Falha ao enviar 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; diff --git a/src/components/AdminDashboard.tsx b/src/components/AdminDashboard.tsx index 9cba89e..42c64db 100644 --- a/src/components/AdminDashboard.tsx +++ b/src/components/AdminDashboard.tsx @@ -6,6 +6,7 @@ import { import { LineChart, Line, BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip as RechartsTooltip, ResponsiveContainer, Legend, Cell, PieChart, Pie } from 'recharts'; import { SubscriptionStatus } from '../types'; import { AdminPricingManager } from './AdminPricingManager'; +import { WhatsappConnectCard } from './WhatsappConnectCard'; interface WebhookLog { id: string; @@ -999,6 +1000,62 @@ export default function AdminDashboard({ token }: AdminDashboardProps) { + + {/* Integração Evolution API */} +
Escaneie o QR Code para conectar a API e enviar notificações automáticas.
+{error}
+Processando conexão...
+A plataforma já pode enviar notificações automáticas.
++ Abra o WhatsApp no seu celular, vá em "Aparelhos Conectados" e escaneie o código acima. +
+ +Nenhuma instância conectada.
+ +