Implementada nova aba de Modelos de Mensagem WhatsApp no painel Admin
Build and Deploy (Gitea) / build-and-deploy (push) Successful in 59s Details

This commit is contained in:
Sidney Gomes 2026-07-25 20:49:17 -03:00
parent 9c5143bea6
commit 4f4fceab29
3 changed files with 119 additions and 6 deletions

View File

@ -293,6 +293,11 @@ model SystemSettings {
whatsappConnected Boolean @default(false) @map("whatsapp_connected") whatsappConnected Boolean @default(false) @map("whatsapp_connected")
whatsappPhoneNumber String? @map("whatsapp_phone_number") whatsappPhoneNumber String? @map("whatsapp_phone_number")
whatsappTemplatePayment String? @map("whatsapp_template_payment") @db.Text
whatsappTemplateWelcome String? @map("whatsapp_template_welcome") @db.Text
whatsappTemplateNewContent String? @map("whatsapp_template_new_content") @db.Text
whatsappTemplatePromo String? @map("whatsapp_template_promo") @db.Text
brandName String @default("MICROTEC") @map("brand_name") brandName String @default("MICROTEC") @map("brand_name")
brandSlogan String @default("Acelere seus estudos em informática e pacote Office no estilo Netflix.") @map("brand_slogan") brandSlogan String @default("Acelere seus estudos em informática e pacote Office no estilo Netflix.") @map("brand_slogan")
cnpj String @default("") @map("cnpj") cnpj String @default("") @map("cnpj")

View File

@ -1407,7 +1407,8 @@ app.post('/api/admin/settings', authenticateToken, requireAdmin, async (req: Aut
'maxBoletoInstallments', 'evolutionApiUrl', 'evolutionApiKey', 'evolutionInstance', 'maxBoletoInstallments', 'evolutionApiUrl', 'evolutionApiKey', 'evolutionInstance',
'whatsappConnected', 'whatsappPhoneNumber', 'brandName', 'brandSlogan', 'cnpj', 'whatsappConnected', 'whatsappPhoneNumber', 'brandName', 'brandSlogan', 'cnpj',
'address', 'smtpHost', 'smtpPort', 'smtpUser', 'smtpPass', 'smtpFrom', 'smtpSecure', 'address', 'smtpHost', 'smtpPort', 'smtpUser', 'smtpPass', 'smtpFrom', 'smtpSecure',
'termsOfUse', 'privacyPolicy', 'featuredCourseId', 'heroImageUrl', 'heroTitle', 'heroDescription' 'termsOfUse', 'privacyPolicy', 'featuredCourseId', 'heroImageUrl', 'heroTitle', 'heroDescription',
'whatsappTemplatePayment', 'whatsappTemplateWelcome', 'whatsappTemplateNewContent', 'whatsappTemplatePromo'
]; ];
const cleanedSettings: any = {}; const cleanedSettings: any = {};
for (const key of allowedKeys) { for (const key of allowedKeys) {

View File

@ -1,6 +1,7 @@
import React, { useState, useEffect } from 'react'; import React, { useState, useEffect } from 'react';
import { Mail, MessageSquare, BookOpen, Send, User, Reply, CheckCircle2, AlertCircle } from 'lucide-react'; import { Mail, MessageSquare, BookOpen, Send, User, Reply, CheckCircle2, AlertCircle } from 'lucide-react';
import CustomModal from './CustomModal'; import CustomModal from './CustomModal';
import { Smartphone, Save } from 'lucide-react';
interface SupportTicket { interface SupportTicket {
id: string; id: string;
@ -38,7 +39,7 @@ interface Note {
} }
export default function AdminMessagesView() { export default function AdminMessagesView() {
const [activeTab, setActiveTab] = useState<'tickets' | 'notes'>('tickets'); const [activeTab, setActiveTab] = useState<'tickets' | 'notes' | 'whatsapp_templates'>('tickets');
const [tickets, setTickets] = useState<SupportTicket[]>([]); const [tickets, setTickets] = useState<SupportTicket[]>([]);
const [notes, setNotes] = useState<Note[]>([]); const [notes, setNotes] = useState<Note[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
@ -47,6 +48,12 @@ export default function AdminMessagesView() {
const [isSending, setIsSending] = useState(false); const [isSending, setIsSending] = useState(false);
const [alertModal, setAlertModal] = useState<{isOpen: boolean, title: string, message: string, isError?: boolean}>({isOpen: false, title: '', message: ''}); const [alertModal, setAlertModal] = useState<{isOpen: boolean, title: string, message: string, isError?: boolean}>({isOpen: false, title: '', message: ''});
// WhatsApp Templates State
const [settings, setSettings] = useState<any>(null);
const [activeTemplateModal, setActiveTemplateModal] = useState<string | null>(null);
const [currentTemplateText, setCurrentTemplateText] = useState('');
const [isSavingTemplate, setIsSavingTemplate] = useState(false);
useEffect(() => { useEffect(() => {
fetchMessages(); fetchMessages();
}, []); }, []);
@ -63,8 +70,15 @@ export default function AdminMessagesView() {
setTickets(data.tickets); setTickets(data.tickets);
setNotes(data.notes); setNotes(data.notes);
} }
const resSettings = await fetch('/api/admin/settings', {
headers: { 'Authorization': `Bearer ${token}` }
});
if (resSettings.ok) {
const data = await resSettings.json();
setSettings(data);
}
} catch (error) { } catch (error) {
console.error('Error fetching messages:', error); console.error('Error fetching data:', error);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
} }
@ -99,6 +113,37 @@ export default function AdminMessagesView() {
} }
}; };
const handleSaveTemplate = async (field: string) => {
setIsSavingTemplate(true);
try {
const token = localStorage.getItem('admin_token');
const res = await fetch('/api/admin/settings', {
method: 'POST',
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
body: JSON.stringify({ [field]: currentTemplateText })
});
if (res.ok) {
setSettings({...settings, [field]: currentTemplateText});
setActiveTemplateModal(null);
setAlertModal({isOpen: true, title: 'Sucesso', message: 'Template salvo com sucesso!'});
} else {
setAlertModal({isOpen: true, title: 'Erro', message: 'Erro ao salvar template.', isError: true});
}
} catch (error) {
console.error('Error saving template:', error);
setAlertModal({isOpen: true, title: 'Erro', message: 'Erro interno ao salvar template.', isError: true});
} finally {
setIsSavingTemplate(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: '🚀' },
];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4"> <div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
@ -111,21 +156,28 @@ export default function AdminMessagesView() {
</div> </div>
</div> </div>
<div className="flex space-x-4 border-b border-white/10"> <div className="flex space-x-4 border-b border-white/10 overflow-x-auto">
<button <button
onClick={() => setActiveTab('tickets')} onClick={() => setActiveTab('tickets')}
className={`pb-3 px-1 text-sm font-bold flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'tickets' ? 'border-[#E50914] text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`} className={`pb-3 px-1 text-sm font-bold flex items-center space-x-2 border-b-2 transition-colors whitespace-nowrap ${activeTab === 'tickets' ? 'border-[#E50914] text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
> >
<MessageSquare className="w-4 h-4" /> <MessageSquare className="w-4 h-4" />
<span>Comentários / Ajuda ({tickets.length})</span> <span>Comentários / Ajuda ({tickets.length})</span>
</button> </button>
<button <button
onClick={() => setActiveTab('notes')} onClick={() => setActiveTab('notes')}
className={`pb-3 px-1 text-sm font-bold flex items-center space-x-2 border-b-2 transition-colors ${activeTab === 'notes' ? 'border-[#E50914] text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`} className={`pb-3 px-1 text-sm font-bold flex items-center space-x-2 border-b-2 transition-colors whitespace-nowrap ${activeTab === 'notes' ? 'border-[#E50914] text-white' : 'border-transparent text-zinc-500 hover:text-zinc-300'}`}
> >
<BookOpen className="w-4 h-4" /> <BookOpen className="w-4 h-4" />
<span>Anotações de Aulas ({notes.length})</span> <span>Anotações de Aulas ({notes.length})</span>
</button> </button>
<button
onClick={() => setActiveTab('whatsapp_templates')}
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>
</button>
</div> </div>
{isLoading ? ( {isLoading ? (
@ -286,8 +338,63 @@ 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>
<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>
))}
</div> </div>
)} )}
</div>
)}
<CustomModal isOpen={!!activeTemplateModal} onClose={() => setActiveTemplateModal(null)} 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>
</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">
Cancelar
</button>
<button
onClick={() => handleSaveTemplate(activeTemplateModal!)}
disabled={isSavingTemplate}
className="bg-[#E50914] hover:bg-red-700 text-white px-6 py-2 rounded-lg font-bold transition-colors disabled:opacity-50 flex items-center space-x-2"
>
{isSavingTemplate ? <div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" /> : <><Save className="w-4 h-4" /><span>Salvar Template</span></>}
</button>
</div>
</div>
</CustomModal>
<CustomModal <CustomModal
isOpen={alertModal.isOpen} isOpen={alertModal.isOpen}