Implementada nova aba de Modelos de Mensagem WhatsApp no painel Admin
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
9c5143bea6
commit
4f4fceab29
|
|
@ -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")
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue