edumanagerpro2/manager/components/Messages.tsx

1092 lines
58 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { SchoolData } from '../types';
import { useDialog } from '../DialogContext';
import { MessageSquare, Save, Info, Settings, Send, Clock, AlertTriangle, FileText, CheckCircle, Cake, X, Power, BookOpen, Smile, Paperclip } from 'lucide-react';
interface MessagesProps {
data: SchoolData;
updateData: (newData: Partial<SchoolData>) => void;
}
const defaultTemplates = {
boletoGerado: "Olá {nome}, sua cobrança referente a {descricao} no valor de R$ {valor} foi gerada. Vencimento: {vencimento}.",
pagamentoConfirmado: "Olá {nome}, confirmamos o pagamento de R$ {valor} referente a {descricao}. Muito obrigado!",
boletoVencido: "Olá {nome}, o boleto referente a {descricao} de R$ {valor} venceu em {vencimento}. Segue o PDF da 2ª via atualizada abaixo:",
cobrancaCancelada: "Olá {nome}, a cobrança referente a {descricao} foi cancelada.",
cobrancaAtualizada: "Olá {nome}, o boleto de {descricao} foi atualizado. Segue a nova versão:",
boletoAVencer: "Olá {nome}, lembramos que sua cobrança referente a {descricao} no valor de R$ {valor} vencerá em {vencimento}. Segue o PDF abaixo:",
felizAniversario: "Olá {nome}, a equipe da {escola} passa para te desejar um Feliz Aniversário! Muita saúde, paz e conquistas neste novo ciclo! 🎂🎈",
novaAvaliacao: "Olá {nome}, uma nova {tipo_avaliacao} ({titulo_avaliacao}) de {materia} foi publicada no portal do aluno. Acesse e realize o mais breve possível!",
automationRules: {
sendOnDueDate: true,
sendDaysAfter: '1',
repeatEveryDays: '3',
sendDaysBefore: '3',
maxPreWarnings: '2'
}
};
const Messages: React.FC<MessagesProps> = ({ data, updateData }) => {
const { showAlert, showConfirm } = useDialog();
const [dbClasses, setDbClasses] = useState<any[]>(data?.classes || []);
const [dbCourses, setDbCourses] = useState<any[]>(data?.courses || []);
React.useEffect(() => {
Promise.all([
fetch('/api/turmas').catch(() => ({ ok: false, json: async () => ({}) })),
fetch('/api/cursos').catch(() => ({ ok: false, json: async () => ({}) })),
]).then(async (responses) => {
const [resT, resC] = responses;
if (resT && resT.ok) {
const jsonT = await resT.json();
if (jsonT.turmas) setDbClasses(jsonT.turmas.map((t: any) => ({
id: t.id, name: t.nome, courseId: t.curso_id, maxStudents: Number(t.max_alunos || 0)
})));
}
if (resC && resC.ok) {
const jsonC = await resC.json();
if (jsonC.cursos) setDbCourses(jsonC.cursos.map((c: any) => ({
id: c.id, name: c.nome, monthlyFee: Number(c.mensalidade || 0), registrationFee: Number(c.taxa_matricula || 0)
})));
}
}).catch(console.error);
}, []);
const defaultVars = data.messageTemplates || defaultTemplates;
const initRules = defaultVars.automationRules || defaultTemplates.automationRules;
const [templates, setTemplates] = useState({
...defaultTemplates,
...defaultVars,
automationRules: {
...defaultTemplates.automationRules,
...initRules
}
});
const [isSendingPreventive, setIsSendingPreventive] = useState(false);
const [isSendingOverdue, setIsSendingOverdue] = useState(false);
// Estado do Agendamento Automático - Preventivo
const [scheduleEnabled, setScheduleEnabled] = useState(!!initRules.autoScheduleEnabled);
const [scheduleTime, setScheduleTime] = useState(initRules.autoScheduleTime || '09:00');
const [isSavingSchedule, setIsSavingSchedule] = useState(false);
const [cronActive, setCronActive] = useState(false);
// Estado do Agendamento Automático - Inadimplência
const [scheduleOverdueEnabled, setScheduleOverdueEnabled] = useState(!!initRules.autoScheduleOverdueEnabled);
const [scheduleOverdueTime, setScheduleOverdueTime] = useState(initRules.autoScheduleOverdueTime || '10:00');
const [isSavingScheduleOverdue, setIsSavingScheduleOverdue] = useState(false);
const [cronOverdueActive, setCronOverdueActive] = useState(false);
// Estado do Agendamento Automático - Aniversário
const [scheduleBirthdayEnabled, setScheduleBirthdayEnabled] = useState(!!initRules.autoScheduleBirthdayEnabled);
const [scheduleBirthdayTime, setScheduleBirthdayTime] = useState(initRules.autoScheduleBirthdayTime || '09:00');
const [isSavingScheduleBirthday, setIsSavingScheduleBirthday] = useState(false);
const [cronBirthdayActive, setCronBirthdayActive] = useState(false);
useEffect(() => {
fetch('/api/cron/status').then(r => r.json()).then(d => {
setCronActive(d.preventive);
setCronOverdueActive(d.overdue);
setCronBirthdayActive(d.birthday);
}).catch(() => {});
}, []);
// Estados WhatsApp em Massa
const [targetType, setTargetType] = useState('todos');
const [targetId, setTargetId] = useState('');
const [messageText, setMessageText] = useState('');
const [isSendingMass, setIsSendingMass] = useState(false);
const [massDelay, setMassDelay] = useState('60');
const [massAttachment, setMassAttachment] = useState<File | null>(null);
const [isSendingBdays, setIsSendingBdays] = useState(false);
const [showMassEmojis, setShowMassEmojis] = useState(false);
const commonEmojis = ['😀', '😂', '🥰', '😎', '🎉', '👍', '🙏', '❤️', '🔥', '🚀', '✅', '❌', '⚠️', '💡', '🎓', '🏫', '📚', '📖', '✏️', '📝', '🎒', '💻', '🧠', '🤓', '🥇'];
// Modal de Edição de Modelo
const [editingTemplate, setEditingTemplate] = useState<{
key: keyof typeof defaultTemplates | 'felizAniversario' | 'novaAvaliacao',
label: string,
desc: string,
color: string,
icon: any,
vars: string[]
} | null>(null);
const normalizeLineBreaks = (text: string) => text.replace(/\r\n/g, '\n');
const birthdayStudents = (data.students || []).filter(s => {
if (!s.birthDate || s.status !== 'active') return false;
const bdayParts = s.birthDate.split('-');
const bdayDay = parseInt(bdayParts[2]);
const bdayMonth = parseInt(bdayParts[1]);
const today = new Date();
return bdayDay === today.getDate() && bdayMonth === (today.getMonth() + 1);
});
const handleSendBirthdays = async () => {
if (birthdayStudents.length === 0) return;
showConfirm(
'Enviar Felicitações',
`Deseja enviar a mensagem de aniversário para os ${birthdayStudents.length} alunos que fazem aniversário hoje?`,
async () => {
setIsSendingBdays(true);
try {
const payloadAlunos = birthdayStudents.map(s => {
const nome = s.name.split(' ')[0];
const telefone = s.phone;
return { nome, telefone };
}).filter(a => a.telefone);
if (payloadAlunos.length === 0) {
showAlert('Aviso', 'Nenhum dos aniversariantes possui telefone cadastrado.', 'warning');
return;
}
const msgTemplate = normalizeLineBreaks(templates.felizAniversario).replace(/{escola}/g, data.profile.name);
const resp = await fetch('/api/enviar-massa', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ alunos: payloadAlunos, mensagem: msgTemplate })
});
if (resp.ok) {
showAlert('Sucesso', 'O disparo das mensagens de aniversário foi iniciado!', 'success');
} else {
const resData = await resp.json();
showAlert('Erro', resData.error || 'Erro ao iniciar disparo.', 'error');
}
} catch (e) {
showAlert('Erro', 'Erro de conexão.', 'error');
} finally {
setIsSendingBdays(false);
}
}
);
};
const handleSave = () => {
const normalizedTemplates = {
...templates,
boletoGerado: normalizeLineBreaks(templates.boletoGerado),
pagamentoConfirmado: normalizeLineBreaks(templates.pagamentoConfirmado),
boletoVencido: normalizeLineBreaks(templates.boletoVencido),
cobrancaCancelada: normalizeLineBreaks(templates.cobrancaCancelada),
cobrancaAtualizada: normalizeLineBreaks(templates.cobrancaAtualizada),
boletoAVencer: normalizeLineBreaks(templates.boletoAVencer),
felizAniversario: normalizeLineBreaks(templates.felizAniversario),
novaAvaliacao: normalizeLineBreaks(templates.novaAvaliacao || "Olá {nome}, uma nova {tipo_avaliacao} ({titulo_avaliacao}) de {materia} foi publicada no portal do aluno. Acesse e realize o mais breve possível!")
};
updateData({ messageTemplates: normalizedTemplates });
showAlert('Sucesso', 'Configurações de mensagens salvas com sucesso!', 'success');
};
const handleDispararCobrancas = async () => {
showConfirm(
'Disparar Cobranças',
'Tem certeza que deseja processar e enviar as mensagens para TODOS os alunos com pagamentos ATRASADOS agora?',
async () => {
setIsSendingOverdue(true);
try {
const resp = await fetch('/api/disparar_cobrancas?tipo=atrasado', { method: 'POST' });
const resData = await resp.json();
if (resp.ok) {
showAlert('Sucesso', resData.message || 'Cobranças processadas e disparadas com sucesso!', 'success');
} else {
showAlert('Erro', resData.error || 'Erro ao disparar cobranças', 'error');
}
} catch (e: any) {
showAlert('Erro', 'Erro de conexão ao disparar cobranças.', 'error');
} finally {
setIsSendingOverdue(false);
}
}
);
};
const handleDispararPreventivos = async () => {
showConfirm(
'Lembretes Preventivos',
'Tem certeza que deseja iniciar o envio dos LEMBRETES PREVENTIVOS para os boletos próximos do vencimento agora?',
async () => {
setIsSendingPreventive(true);
try {
const resp = await fetch('/api/disparar_cobrancas?tipo=preventivo', { method: 'POST' });
const resData = await resp.json();
if (resp.ok) {
showAlert('Sucesso', resData.message || 'Lembretes disparados com sucesso!', 'success');
} else {
showAlert('Erro', resData.error || 'Erro ao disparar lembretes', 'error');
}
} catch (e: any) {
showAlert('Erro', 'Erro de conexão.', 'error');
} finally {
setIsSendingPreventive(false);
}
}
);
};
const handleMassSend = async () => {
if (!messageText.trim()) {
return showAlert('Aviso', 'Digite uma mensagem para enviar.', 'warning');
}
let targetStudents = [];
if (targetType === 'todos') {
targetStudents = data.students || [];
} else if (targetType === 'turma') {
if (!targetId) return showAlert('Aviso', 'Selecione uma turma.', 'warning');
targetStudents = (data.students || []).filter(s => s.classId === targetId);
} else if (targetType === 'aluno') {
if (!targetId) return showAlert('Aviso', 'Selecione um aluno.', 'warning');
targetStudents = (data.students || []).filter(s => s.id === targetId);
}
const validStudents = targetStudents.filter(a => a.phone || a.guardianPhone);
if (validStudents.length === 0) {
return showAlert('Erro', 'Nenhum aluno com telefone cadastrado foi selecionado.', 'error');
}
const payloadAlunos = validStudents.flatMap(a => {
const entries = [];
// Entrada para o Aluno
if (a.phone) {
entries.push({
nome: a.name.split(' ')[0],
telefone: a.phone,
matricula: a.enrollmentNumber || '—'
});
}
// Entrada para o Responsável (apenas se for um número diferente ou se o aluno não tiver número)
if (a.guardianPhone && a.guardianPhone !== a.phone) {
entries.push({
nome: (a.guardianName || a.name).split(' ')[0],
telefone: a.guardianPhone,
matricula: a.enrollmentNumber || '—'
});
}
return entries;
});
setIsSendingMass(true);
try {
const formData = new FormData();
formData.append('alunos', JSON.stringify(payloadAlunos));
formData.append('mensagem', normalizeLineBreaks(messageText));
formData.append('delay', String(parseInt(massDelay) || 60));
if (massAttachment) {
formData.append('attachment', massAttachment);
}
const resp = await fetch('/api/enviar-massa', {
method: 'POST',
body: formData
});
const resData = await resp.json();
if (resp.ok) {
setMessageText('');
setTargetId('');
setMassAttachment(null);
showAlert('Sucesso', 'Disparo iniciado no servidor! Você já pode fechar esta tela ou continuar usando o sistema.', 'success');
} else {
showAlert('Erro', resData.error || 'Erro ao iniciar disparo.', 'error');
}
} catch (e) {
showAlert('Erro', 'Erro de conexão.', 'error');
} finally {
setIsSendingMass(false);
}
};
const templateCards = [
{ key: 'boletoGerado', label: 'Boleto Gerado / Novo Carnê', desc: 'Enviado assim que a cobrança é criada no sistema.', color: 'blue', icon: FileText, vars: ['{nome}', '{matricula}', '{descricao}', '{valor}', '{vencimento}', '{link_boleto}', '{escola}'] },
{ key: 'pagamentoConfirmado', label: 'Pagamento Confirmado', desc: 'Enviado quando o sistema (Asaas) compensa o pagamento.', color: 'emerald', icon: CheckCircle, vars: ['{nome}', '{matricula}', '{descricao}', '{valor}', '{escola}'] },
{ key: 'boletoVencido', label: 'Boleto Vencido', desc: 'Enviado conforme automação ou disparo manual de atrasados.', color: 'red', icon: AlertTriangle, vars: ['{nome}', '{matricula}', '{descricao}', '{valor}', '{vencimento}', '{link_boleto}', '{escola}'] },
{ key: 'cobrancaCancelada', label: 'Cobrança Cancelada', desc: 'Enviado quando o boleto for cancelado no sistema.', color: 'slate', icon: AlertTriangle, vars: ['{nome}', '{matricula}', '{descricao}', '{escola}'] },
{ key: 'cobrancaAtualizada', label: 'Cobrança Atualizada', desc: 'Enviado quando houver edição/atualização da cobrança.', color: 'amber', icon: Settings, vars: ['{nome}', '{matricula}', '{descricao}', '{valor}', '{vencimento}', '{link_boleto}', '{escola}'] },
{ key: 'boletoAVencer', label: 'Boleto a Vencer', desc: 'Aviso preventivo enviado dias antes do vencimento.', color: 'indigo', icon: Clock, vars: ['{nome}', '{matricula}', '{descricao}', '{valor}', '{vencimento}', '{link_boleto}', '{escola}'] },
{ key: 'felizAniversario', label: 'Feliz Aniversário', desc: 'Mensagem carinhosa para os aniversariantes do dia.', color: 'pink', icon: Cake, vars: ['{nome}', '{escola}'] },
{ key: 'novaAvaliacao', label: 'Nova Avaliação', desc: 'Enviado ao aluno quando uma prova ou atividade é publicada.', color: 'indigo', icon: BookOpen, vars: ['{nome}', '{matricula}', '{tipo_avaliacao}', '{titulo_avaliacao}', '{materia}', '{escola}'] }
];
const insertVariable = (variable: string) => {
if (!editingTemplate) return;
const textarea = document.getElementById('template-editor') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const text = (templates[editingTemplate.key as keyof typeof templates] as string) || '';
const newText = text.substring(0, start) + variable + text.substring(end);
setTemplates(p => ({ ...p, [editingTemplate.key]: newText }));
setTimeout(() => {
textarea.focus();
textarea.setSelectionRange(start + variable.length, start + variable.length);
}, 10);
};
return (
<div className="space-y-8 animate-in fade-in duration-300 pb-20">
<header className="flex justify-between items-end">
<div>
<h2 className="text-3xl font-extrabold text-slate-900 tracking-tight">Mensagens</h2>
<p className="text-slate-500 font-medium mt-1">Configure modelos e rotinas de notificação via WhatsApp.</p>
</div>
<button
onClick={handleSave}
className="bg-indigo-600 hover:bg-indigo-700 text-white px-6 py-2.5 rounded-xl font-bold flex items-center gap-2 shadow-lg transition-all"
>
<Save size={18} /> Salvar Tudo
</button>
</header>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Lado Esquerdo - Configurações e Disparos */}
<div className="space-y-6">
<div className="bg-emerald-50 border border-emerald-100 p-6 rounded-2xl shadow-lg">
<h3 className="font-black text-emerald-800 flex items-center gap-2 mb-4 text-sm uppercase tracking-widest">
<MessageSquare size={18} /> Disparo em Massa
</h3>
<div className="space-y-4">
<select
className="w-full px-3 py-2.5 border border-emerald-200 rounded-xl text-sm bg-white font-bold"
value={targetType}
onChange={(e) => { setTargetType(e.target.value); setTargetId(''); }}
>
<option value="todos">Todos os Alunos</option>
<option value="turma">Uma Turma</option>
<option value="aluno">Um Aluno</option>
</select>
{targetType !== 'todos' && (
<select
className="w-full px-3 py-2.5 border border-emerald-200 rounded-xl text-sm bg-white font-bold"
value={targetId}
onChange={(e) => setTargetId(e.target.value)}
>
<option value="">-- Selecione --</option>
{targetType === 'turma'
? dbClasses?.map(c => <option key={c.id} value={c.id}>{c.name}</option>)
: data.students?.map(s => <option key={s.id} value={s.id}>{s.name}</option>)
}
</select>
)}
<div>
<label className="block text-[10px] font-black text-emerald-600 uppercase mb-2 ml-1">Mensagem Personalizada</label>
<div className="flex flex-wrap gap-1 mb-2 relative">
{['{nome}', '{matricula}'].map(v => (
<button
key={v}
onClick={() => {
const textarea = document.getElementById('mass-editor') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newText = messageText.substring(0, start) + v + messageText.substring(end);
setMessageText(newText);
setTimeout(() => { textarea.focus(); textarea.setSelectionRange(start + v.length, start + v.length); }, 10);
}}
className="text-[9px] bg-emerald-100/50 text-emerald-700 px-2 py-1 rounded-md border border-emerald-200 hover:bg-emerald-600 hover:text-white transition-all shadow-sm"
>
{v}
</button>
))}
<button
onClick={() => setShowMassEmojis(!showMassEmojis)}
className="flex items-center justify-center text-[9px] bg-emerald-100/50 text-emerald-700 px-2 py-1 rounded-md border border-emerald-200 hover:bg-emerald-600 hover:text-white transition-all shadow-sm ml-auto"
title="Inserir Emoji"
>
<Smile size={12} />
</button>
{showMassEmojis && (
<div className="absolute right-0 top-8 z-10 bg-white border border-emerald-200 rounded-xl shadow-xl p-2 grid grid-cols-5 gap-1 w-48 animate-in fade-in zoom-in duration-200">
{commonEmojis.map(emoji => (
<button
key={emoji}
onClick={() => {
const textarea = document.getElementById('mass-editor') as HTMLTextAreaElement;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const newText = messageText.substring(0, start) + emoji + messageText.substring(end);
setMessageText(newText);
setShowMassEmojis(false);
setTimeout(() => { textarea.focus(); textarea.setSelectionRange(start + emoji.length, start + emoji.length); }, 10);
}}
className="hover:bg-emerald-50 text-lg rounded flex items-center justify-center p-1 transition-colors"
>
{emoji}
</button>
))}
</div>
)}
</div>
<textarea
id="mass-editor"
rows={4}
className="w-full px-3 py-3 border border-emerald-200 rounded-xl text-sm bg-white focus:ring-emerald-500 font-medium shadow-sm"
placeholder="Escreva sua mensagem..."
value={messageText}
onChange={(e) => setMessageText(e.target.value)}
/>
<div className="mt-2 flex items-center justify-between">
<div className="flex-1">
{massAttachment ? (
<div className="flex items-center gap-2 bg-emerald-50 text-emerald-700 text-[10px] font-bold px-2 py-1.5 rounded-lg border border-emerald-100">
<span className="truncate max-w-[150px]">{massAttachment.name}</span>
<button onClick={() => setMassAttachment(null)} className="text-emerald-500 hover:text-red-500" title="Remover anexo"><X size={12} /></button>
</div>
) : (
<div className="text-[9px] text-slate-400 font-medium">Nenhum anexo</div>
)}
</div>
<label className="flex items-center gap-1.5 px-3 py-1.5 bg-emerald-100 text-emerald-700 rounded-lg border border-emerald-200 text-[10px] font-black uppercase cursor-pointer hover:bg-emerald-600 hover:text-white transition-all shadow-sm">
<Paperclip size={12} />
<span>Anexar Arquivo</span>
<input
type="file"
className="hidden"
accept="image/*,application/pdf"
onChange={(e) => {
if (e.target.files && e.target.files[0]) {
setMassAttachment(e.target.files[0]);
}
}}
/>
</label>
</div>
</div>
<div className="bg-white/50 p-3 rounded-xl border border-emerald-100 mb-2">
<label className="block text-[9px] font-black text-emerald-600 uppercase tracking-widest mb-1.5 ml-1">Intervalo Mínimo (Segundos)</label>
<div className="flex items-center gap-2">
<input
type="number"
min="10" max="600"
value={massDelay}
onChange={(e) => setMassDelay(e.target.value)}
className="w-full px-3 py-2 border border-emerald-200 rounded-lg text-sm font-bold text-center focus:ring-emerald-500 focus:outline-none"
/>
<div className="text-[10px] text-emerald-400 font-bold whitespace-nowrap">seg/msg</div>
</div>
<p className="text-[8px] text-emerald-400 mt-1 font-medium leading-tight">Recomendado: 60s ou mais para evitar banimento.</p>
</div>
<button
onClick={handleMassSend}
disabled={isSendingMass || !data.evolutionConfig?.apiUrl}
className={`w-full flex items-center justify-center gap-2 py-3.5 px-4 rounded-xl font-black text-sm text-white transition-all shadow-lg active:scale-95 ${
isSendingMass || !data.evolutionConfig?.apiUrl ? 'bg-slate-400' : 'bg-emerald-600 hover:bg-emerald-700'
}`}
>
{isSendingMass ? 'Iniciando...' : 'Iniciar Disparo'}
</button>
</div>
</div>
<div className="bg-indigo-50 border border-indigo-200 p-6 rounded-2xl shadow-lg mb-6">
<h3 className="font-black text-indigo-800 flex items-center gap-2 mb-3 text-sm uppercase tracking-widest">
<Clock size={18} /> Lembretes Preventivos
</h3>
<p className="text-[10px] text-indigo-600 font-medium mb-4">
Envia avisos para boletos que vencem em até {templates.automationRules.sendDaysBefore} dias.
</p>
<div className="space-y-3 mb-4">
<button
onClick={handleDispararPreventivos}
disabled={isSendingPreventive || !data.evolutionConfig?.apiUrl}
className={`w-full py-3.5 px-4 rounded-xl font-black text-sm text-white shadow-lg transition-all active:scale-95 ${
isSendingPreventive || !data.evolutionConfig?.apiUrl ? 'bg-slate-400' : 'bg-indigo-600 hover:bg-indigo-700'
}`}
>
{isSendingPreventive ? 'Processando...' : 'Enviar Lembretes Agora'}
</button>
<div className="flex flex-col gap-2 p-3 bg-white/50 rounded-xl border border-indigo-100">
<label className="flex items-center gap-2 cursor-pointer group">
<input
type="checkbox"
checked={!!templates.automationRules.ignoreDailyLock}
onChange={(e) => {
const newRules = { ...templates.automationRules, ignoreDailyLock: e.target.checked };
setTemplates(prev => ({ ...prev, automationRules: newRules }));
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
}}
className="w-4 h-4 rounded border-indigo-300 text-indigo-600 focus:ring-indigo-500"
/>
<span className="text-[10px] font-black text-indigo-700 uppercase tracking-tight group-hover:text-indigo-900 transition-colors">
Ignorar trava de envio diário (Debug)
</span>
</label>
<button
onClick={async () => {
showConfirm('Zerar Contadores', 'Tem certeza que deseja zerar todos os contadores de avisos (preventivos e atrasos) de todos os alunos? Isso permitirá que eles recebam os avisos novamente.', async () => {
try {
const resp = await fetch('/api/admin/reset-cobrancas-counters', { method: 'POST' });
if (resp.ok) showAlert('Sucesso', 'Todos os contadores foram zerados!', 'success');
else showAlert('Erro', 'Falha ao zerar contadores.', 'error');
} catch (e) {
showAlert('Erro', 'Erro de conexão.', 'error');
}
});
}}
className="w-full py-2 px-3 bg-white border border-indigo-200 text-indigo-600 rounded-lg font-black text-[9px] uppercase tracking-widest hover:bg-indigo-50 hover:border-indigo-300 transition-all active:scale-95"
>
Zerar Contadores de Avisos
</button>
</div>
</div>
{/* Agendamento Automático */}
<div className="mt-5 pt-5 border-t border-indigo-200">
<div className="flex items-center justify-between mb-4">
<label className="text-[10px] font-black text-indigo-700 uppercase tracking-widest flex items-center gap-1.5">
<Power size={13} /> Rotina Automática
</label>
<button
onClick={async () => {
const newEnabled = !scheduleEnabled;
setScheduleEnabled(newEnabled);
setIsSavingSchedule(true);
try {
const resp = await fetch('/api/cron/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: newEnabled, time: scheduleTime, tipo: 'preventivo' })
});
const d = await resp.json();
setCronActive(d.preventive);
// Persistir no estado global para não perder ao trocar de aba
const newRules = { ...templates.automationRules, autoScheduleEnabled: newEnabled, autoScheduleTime: scheduleTime };
setTemplates(prev => ({ ...prev, automationRules: newRules }));
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
showAlert('Sucesso', newEnabled ? `Rotina ativada para ${scheduleTime}!` : 'Rotina automática desativada.', 'success');
} catch {
showAlert('Erro', 'Erro ao salvar agendamento.', 'error');
setScheduleEnabled(!newEnabled);
} finally {
setIsSavingSchedule(false);
}
}}
disabled={isSavingSchedule}
className={`relative w-12 h-6 rounded-full transition-all duration-300 ${
scheduleEnabled ? 'bg-indigo-600' : 'bg-slate-300'
}`}
>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-300 ${
scheduleEnabled ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
{scheduleEnabled && (
<div className="space-y-3 animate-in fade-in slide-in-from-top-2 duration-300">
<div>
<label className="block text-[10px] font-black text-indigo-500 uppercase tracking-widest mb-1.5 ml-1">Horário do Disparo</label>
<div className="flex gap-2">
<input
type="time"
value={scheduleTime}
onChange={(e) => setScheduleTime(e.target.value)}
className="flex-1 px-4 py-2.5 border border-indigo-200 rounded-xl text-sm font-bold text-center bg-white focus:ring-2 focus:ring-indigo-500 focus:outline-none shadow-sm"
/>
<button
onClick={async () => {
setIsSavingSchedule(true);
try {
const resp = await fetch('/api/cron/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: true, time: scheduleTime, tipo: 'preventivo' })
});
const d = await resp.json();
setCronActive(d.preventive);
// Persistir no estado global
const newRules = { ...templates.automationRules, autoScheduleEnabled: true, autoScheduleTime: scheduleTime };
setTemplates(prev => ({ ...prev, automationRules: newRules }));
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
showAlert('Sucesso', `Horário atualizado para ${scheduleTime}!`, 'success');
} catch {
showAlert('Erro', 'Erro ao atualizar horário.', 'error');
} finally {
setIsSavingSchedule(false);
}
}}
disabled={isSavingSchedule}
className="px-4 py-2.5 bg-indigo-600 text-white rounded-xl font-black text-xs hover:bg-indigo-700 transition-all active:scale-95 shadow-md"
>
{isSavingSchedule ? '...' : 'Salvar'}
</button>
</div>
</div>
<div className={`flex items-center gap-2 text-[10px] font-bold px-3 py-2 rounded-lg ${
cronActive ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-500'
}`}>
<span className={`w-2 h-2 rounded-full ${
cronActive ? 'bg-emerald-500 animate-pulse' : 'bg-slate-400'
}`} />
{cronActive ? `Ativo — Próximo disparo às ${scheduleTime}` : 'Inativo'}
</div>
</div>
)}
</div>
</div>
<div className="bg-amber-50 border border-amber-200 p-6 rounded-2xl shadow-lg">
<h3 className="font-black text-amber-800 flex items-center gap-2 mb-3 text-sm uppercase tracking-widest">
<AlertTriangle size={18} /> Inadimplência
</h3>
<p className="text-[10px] text-amber-600 font-medium mb-4">
Envia cobranças para boletos com status atrasado.
</p>
<button
onClick={handleDispararCobrancas}
disabled={isSendingOverdue || !data.evolutionConfig?.apiUrl}
className={`w-full py-3.5 px-4 rounded-xl font-black text-sm text-white shadow-lg transition-all active:scale-95 ${
isSendingOverdue || !data.evolutionConfig?.apiUrl ? 'bg-slate-400' : 'bg-amber-500 hover:bg-amber-600'
}`}
>
{isSendingOverdue ? 'Processando...' : 'Disparar Cobranças Agora'}
</button>
{/* Agendamento Automático - Inadimplência */}
<div className="mt-5 pt-5 border-t border-amber-200">
<div className="flex items-center justify-between mb-4">
<label className="text-[10px] font-black text-amber-700 uppercase tracking-widest flex items-center gap-1.5">
<Power size={13} /> Rotina Automática
</label>
<button
onClick={async () => {
const newEnabled = !scheduleOverdueEnabled;
setScheduleOverdueEnabled(newEnabled);
setIsSavingScheduleOverdue(true);
try {
const resp = await fetch('/api/cron/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: newEnabled, time: scheduleOverdueTime, tipo: 'atrasado' })
});
const d = await resp.json();
setCronOverdueActive(d.overdue);
// Persistir no estado global
const newRules = { ...templates.automationRules, autoScheduleOverdueEnabled: newEnabled, autoScheduleOverdueTime: scheduleOverdueTime };
setTemplates(prev => ({ ...prev, automationRules: newRules }));
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
showAlert('Sucesso', newEnabled ? `Rotina de inadimplência ativada para ${scheduleOverdueTime}!` : 'Rotina de inadimplência desativada.', 'success');
} catch {
showAlert('Erro', 'Erro ao salvar agendamento.', 'error');
setScheduleOverdueEnabled(!newEnabled);
} finally {
setIsSavingScheduleOverdue(false);
}
}}
disabled={isSavingScheduleOverdue}
className={`relative w-12 h-6 rounded-full transition-all duration-300 ${
scheduleOverdueEnabled ? 'bg-amber-500' : 'bg-slate-300'
}`}
>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-300 ${
scheduleOverdueEnabled ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
{scheduleOverdueEnabled && (
<div className="space-y-3 animate-in fade-in slide-in-from-top-2 duration-300">
<div>
<label className="block text-[10px] font-black text-amber-500 uppercase tracking-widest mb-1.5 ml-1">Horário do Disparo</label>
<div className="flex gap-2">
<input
type="time"
value={scheduleOverdueTime}
onChange={(e) => setScheduleOverdueTime(e.target.value)}
className="flex-1 px-4 py-2.5 border border-amber-200 rounded-xl text-sm font-bold text-center bg-white focus:ring-2 focus:ring-amber-500 focus:outline-none shadow-sm"
/>
<button
onClick={async () => {
setIsSavingScheduleOverdue(true);
try {
const resp = await fetch('/api/cron/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: true, time: scheduleOverdueTime, tipo: 'atrasado' })
});
const d = await resp.json();
setCronOverdueActive(d.overdue);
// Persistir no estado global
const newRules = { ...templates.automationRules, autoScheduleOverdueEnabled: true, autoScheduleOverdueTime: scheduleOverdueTime };
setTemplates(prev => ({ ...prev, automationRules: newRules }));
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
showAlert('Sucesso', `Horário atualizado para ${scheduleOverdueTime}!`, 'success');
} catch {
showAlert('Erro', 'Erro ao atualizar horário.', 'error');
} finally {
setIsSavingScheduleOverdue(false);
}
}}
disabled={isSavingScheduleOverdue}
className="px-4 py-2.5 bg-amber-500 text-white rounded-xl font-black text-xs hover:bg-amber-600 transition-all active:scale-95 shadow-md"
>
{isSavingScheduleOverdue ? '...' : 'Salvar'}
</button>
</div>
</div>
<div className={`flex items-center gap-2 text-[10px] font-bold px-3 py-2 rounded-lg ${
cronOverdueActive ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-500'
}`}>
<span className={`w-2 h-2 rounded-full ${
cronOverdueActive ? 'bg-emerald-500 animate-pulse' : 'bg-slate-400'
}`} />
{cronOverdueActive ? `Ativo — Próximo disparo às ${scheduleOverdueTime}` : 'Inativo'}
</div>
</div>
)}
</div>
</div>
<div className="bg-pink-50 border border-pink-200 p-6 rounded-2xl shadow-lg">
<h3 className="font-black text-pink-800 flex items-center gap-2 mb-3 text-sm uppercase tracking-widest">
<Cake size={18} /> Aniversariantes
</h3>
<button
onClick={handleSendBirthdays}
disabled={isSendingBdays || birthdayStudents.length === 0 || !data.evolutionConfig?.apiUrl}
className={`w-full flex items-center justify-center gap-2 py-3.5 px-4 rounded-xl font-black text-sm text-white shadow-xl transition-all active:scale-95 mb-4 ${
isSendingBdays || birthdayStudents.length === 0 || !data.evolutionConfig?.apiUrl ? 'bg-slate-400' : 'bg-pink-500 hover:bg-pink-600'
}`}
>
{isSendingBdays ? 'Enviando...' : 'Parabenizar Todos'}
</button>
{/* Agendamento Automático - Aniversário */}
<div className="mt-4 pt-4 border-t border-pink-200">
<div className="flex items-center justify-between mb-4">
<label className="text-[10px] font-black text-pink-700 uppercase tracking-widest flex items-center gap-1.5">
<Power size={13} /> Rotina Automática
</label>
<button
onClick={async () => {
const newEnabled = !scheduleBirthdayEnabled;
setScheduleBirthdayEnabled(newEnabled);
setIsSavingScheduleBirthday(true);
try {
const resp = await fetch('/api/cron/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: newEnabled, time: scheduleBirthdayTime, tipo: 'aniversario' })
});
const d = await resp.json();
setCronBirthdayActive(d.birthday);
// Persistir no estado global
const newRules = { ...templates.automationRules, autoScheduleBirthdayEnabled: newEnabled, autoScheduleBirthdayTime: scheduleBirthdayTime };
setTemplates(prev => ({ ...prev, automationRules: newRules }));
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
showAlert('Sucesso', newEnabled ? `Rotina de aniversário ativada para ${scheduleBirthdayTime}!` : 'Rotina automática desativada.', 'success');
} catch {
showAlert('Erro', 'Erro ao salvar agendamento.', 'error');
setScheduleBirthdayEnabled(!newEnabled);
} finally {
setIsSavingScheduleBirthday(false);
}
}}
disabled={isSavingScheduleBirthday}
className={`relative w-12 h-6 rounded-full transition-all duration-300 ${
scheduleBirthdayEnabled ? 'bg-pink-600' : 'bg-slate-300'
}`}
>
<span className={`absolute top-0.5 left-0.5 w-5 h-5 bg-white rounded-full shadow-md transition-transform duration-300 ${
scheduleBirthdayEnabled ? 'translate-x-6' : 'translate-x-0'
}`} />
</button>
</div>
{scheduleBirthdayEnabled && (
<div className="space-y-3 animate-in fade-in slide-in-from-top-2 duration-300 mb-4">
<div>
<label className="block text-[10px] font-black text-pink-500 uppercase tracking-widest mb-1.5 ml-1">Horário do Disparo</label>
<div className="flex gap-2">
<input
type="time"
value={scheduleBirthdayTime}
onChange={(e) => setScheduleBirthdayTime(e.target.value)}
className="flex-1 px-4 py-2.5 border border-pink-200 rounded-xl text-sm font-bold text-center bg-white focus:ring-2 focus:ring-pink-500 focus:outline-none shadow-sm"
/>
<button
onClick={async () => {
setIsSavingScheduleBirthday(true);
try {
const resp = await fetch('/api/cron/schedule', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ enabled: true, time: scheduleBirthdayTime, tipo: 'aniversario' })
});
const d = await resp.json();
setCronBirthdayActive(d.birthday);
// Persistir no estado global
const newRules = { ...templates.automationRules, autoScheduleBirthdayEnabled: true, autoScheduleBirthdayTime: scheduleBirthdayTime };
setTemplates(prev => ({ ...prev, automationRules: newRules }));
updateData({ messageTemplates: { ...templates, automationRules: newRules } });
showAlert('Sucesso', `Horário atualizado para ${scheduleBirthdayTime}!`, 'success');
} catch {
showAlert('Erro', 'Erro ao atualizar horário.', 'error');
} finally {
setIsSavingScheduleBirthday(false);
}
}}
disabled={isSavingScheduleBirthday}
className="px-4 py-2.5 bg-pink-600 text-white rounded-xl font-black text-xs hover:bg-pink-700 transition-all active:scale-95 shadow-md"
>
{isSavingScheduleBirthday ? '...' : 'Salvar'}
</button>
</div>
</div>
<div className={`flex items-center gap-2 text-[10px] font-bold px-3 py-2 rounded-lg ${
cronBirthdayActive ? 'bg-emerald-100 text-emerald-700' : 'bg-slate-100 text-slate-500'
}`}>
<span className={`w-2 h-2 rounded-full ${
cronBirthdayActive ? 'bg-emerald-500 animate-pulse' : 'bg-slate-400'
}`} />
{cronBirthdayActive ? `Ativo — Próximo disparo às ${scheduleBirthdayTime}` : 'Inativo'}
</div>
</div>
)}
</div>
<div className="pt-4 border-t border-pink-100">
<label className="block text-[10px] font-black text-pink-400 uppercase tracking-widest mb-3">Próximos do Mês</label>
<div className="space-y-2 max-h-40 overflow-y-auto pr-2 custom-scrollbar">
{(data.students || []).filter(s => {
if (!s.birthDate || s.status !== 'active') return false;
return parseInt(s.birthDate.split('-')[1]) === (new Date().getMonth() + 1);
}).sort((a,b) => parseInt(a.birthDate!.split('-')[2]) - parseInt(b.birthDate!.split('-')[2])).map(s => {
const day = s.birthDate?.split('-')[2];
return (
<div key={s.id} className="flex justify-between items-center text-[10px] font-bold text-pink-700 bg-white/40 p-2 rounded-lg border border-pink-100/50">
<div className="flex items-center gap-2">
<span className="w-5 h-5 bg-pink-100 rounded-full flex items-center justify-center text-[9px]">{day}</span>
<span className="truncate max-w-[100px]">{s.name}</span>
</div>
<span className="opacity-60">{s.phone || 'S/ Tel'}</span>
</div>
);
})}
</div>
</div>
</div>
</div>
{/* Lado Direito - Cards de Modelos */}
<div className="lg:col-span-2 grid grid-cols-1 md:grid-cols-2 gap-6">
{templateCards.map((card) => {
const Icon = card.icon;
const colors: any = {
blue: 'bg-blue-50 text-blue-600',
emerald: 'bg-emerald-50 text-emerald-600',
red: 'bg-red-50 text-red-600',
slate: 'bg-slate-50 text-slate-600',
amber: 'bg-amber-50 text-amber-600',
pink: 'bg-pink-50 text-pink-600',
indigo: 'bg-indigo-50 text-indigo-600',
};
return (
<div
key={card.key}
onClick={() => setEditingTemplate(card as any)}
className="bg-white border border-slate-200 rounded-3xl p-6 cursor-pointer transition-all hover:shadow-2xl hover:-translate-y-1 group relative overflow-hidden active:scale-95"
>
<div className={`w-12 h-12 rounded-2xl ${colors[card.color]} flex items-center justify-center mb-5 group-hover:scale-110 transition-transform shadow-sm`}>
<Icon size={24} />
</div>
<h4 className="font-black text-slate-800 text-lg mb-2">{card.label}</h4>
<p className="text-xs text-slate-500 font-medium leading-relaxed">{card.desc}</p>
<div className="mt-6 flex items-center gap-2 text-[10px] font-black text-indigo-600 uppercase tracking-widest border-t border-slate-50 pt-4">
Editar Modelo <Settings size={12} className="group-hover:rotate-45 transition-transform" />
</div>
</div>
);
})}
</div>
</div>
{/* MODAL DE EDIÇÃO */}
{editingTemplate && (
<div className="fixed inset-0 bg-transparent z-50 flex items-center justify-center p-4 animate-in fade-in duration-400">
<div className="bg-white rounded-[2.5rem] w-full max-w-2xl shadow-2xl relative overflow-hidden animate-slide-up duration-400 border border-slate-100">
<div className="p-8 border-b border-slate-100 flex justify-between items-center bg-slate-50/50">
<div>
<h3 className="text-xl font-black text-slate-800">{editingTemplate.label}</h3>
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest">{editingTemplate.key}</p>
</div>
<button
onClick={() => setEditingTemplate(null)}
className="p-3 bg-white text-slate-400 hover:text-red-500 rounded-2xl shadow-md transition-all hover:rotate-90 border border-slate-100"
>
<X size={20} />
</button>
</div>
<div className="p-8 space-y-6">
<div>
<label className="block text-[10px] font-black text-slate-400 uppercase tracking-widest mb-3 ml-1">Clique para inserir variável</label>
<div className="flex flex-wrap gap-2 text-[10px] font-black">
{editingTemplate.vars.map(v => (
<button
key={v}
onClick={() => insertVariable(v)}
className="px-3 py-1.5 bg-indigo-50 text-indigo-700 rounded-lg hover:bg-indigo-600 hover:text-white transition-all active:scale-95 border border-indigo-100 shadow-sm"
>
{v}
</button>
))}
</div>
</div>
<textarea
id="template-editor"
value={(templates[editingTemplate.key as keyof typeof templates] as string) || ''}
onChange={(e) => setTemplates(p => ({ ...p, [editingTemplate.key]: e.target.value }))}
rows={['boletoAVencer', 'boletoVencido'].includes(editingTemplate.key) ? 6 : 10}
className="w-full px-6 py-5 bg-slate-50 border-2 border-slate-100 rounded-[2rem] focus:border-indigo-500 focus:bg-white focus:outline-none transition-all text-slate-700 font-medium shadow-inner resize-none"
placeholder="Escreva sua mensagem..."
/>
{editingTemplate.key === 'boletoAVencer' && (
<div className="bg-indigo-50 border border-indigo-100 rounded-2xl p-6 mt-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<h4 className="font-black text-indigo-800 text-sm flex items-center gap-2 uppercase tracking-widest mb-4">
<Clock size={16} /> Configuração de Disparo Preventivo
</h4>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-[10px] font-black text-indigo-500 uppercase tracking-widest mb-2 ml-1">Antecedência (Dias)</label>
<div className="flex items-center bg-white border border-indigo-100 rounded-xl overflow-hidden shadow-sm focus-within:ring-2 focus-within:ring-indigo-500 transition-all">
<input
type="number" min="1" max="30"
value={templates.automationRules.sendDaysBefore}
onChange={(e) => setTemplates(p => ({ ...p, automationRules: { ...p.automationRules, sendDaysBefore: e.target.value } }))}
className="w-full px-4 py-3 text-sm font-bold text-slate-700 focus:outline-none text-center"
/>
</div>
<p className="text-[9px] text-indigo-400 mt-1.5 ml-1 font-medium">Dias antes do vencimento para avisar.</p>
</div>
<div>
<label className="block text-[10px] font-black text-indigo-500 uppercase tracking-widest mb-2 ml-1">Repetições Max.</label>
<div className="flex items-center bg-white border border-indigo-100 rounded-xl overflow-hidden shadow-sm focus-within:ring-2 focus-within:ring-indigo-500 transition-all">
<input
type="number" min="1" max="10"
value={templates.automationRules.maxPreWarnings}
onChange={(e) => setTemplates(p => ({ ...p, automationRules: { ...p.automationRules, maxPreWarnings: e.target.value } }))}
className="w-full px-4 py-3 text-sm font-bold text-slate-700 focus:outline-none text-center"
/>
</div>
<p className="text-[9px] text-indigo-400 mt-1.5 ml-1 font-medium">Limite de avisos recebidos pelo aluno.</p>
</div>
</div>
</div>
)}
{editingTemplate.key === 'boletoVencido' && (
<div className="bg-red-50 border border-red-100 rounded-2xl p-6 mt-4 animate-in fade-in slide-in-from-bottom-2 duration-300">
<h4 className="font-black text-red-800 text-sm flex items-center gap-2 uppercase tracking-widest mb-4">
<AlertTriangle size={16} /> Configuração de Inadimplência
</h4>
<div className="space-y-4">
<label className="flex items-center gap-3 cursor-pointer">
<input
type="checkbox"
checked={templates.automationRules.sendOnDueDate}
onChange={(e) => setTemplates(p => ({ ...p, automationRules: { ...p.automationRules, sendOnDueDate: e.target.checked } }))}
className="w-5 h-5 rounded border-slate-300 text-red-600 focus:ring-red-500"
/>
<span className="text-sm font-bold text-red-900">Aviso no dia do vencimento</span>
</label>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-[10px] font-black text-red-500 uppercase tracking-widest mb-2 ml-1">1º aviso (Dias após)</label>
<div className="flex items-center bg-white border border-red-100 rounded-xl overflow-hidden shadow-sm focus-within:ring-2 focus-within:ring-red-500 transition-all">
<input
type="number" min="1" max="30"
value={templates.automationRules.sendDaysAfter}
onChange={(e) => setTemplates(p => ({ ...p, automationRules: { ...p.automationRules, sendDaysAfter: e.target.value } }))}
className="w-full px-4 py-3 text-sm font-bold text-slate-700 focus:outline-none text-center"
/>
</div>
</div>
<div>
<label className="block text-[10px] font-black text-red-500 uppercase tracking-widest mb-2 ml-1">Repetir a cada (Dias)</label>
<div className="flex items-center bg-white border border-red-100 rounded-xl overflow-hidden shadow-sm focus-within:ring-2 focus-within:ring-red-500 transition-all">
<input
type="number" min="1" max="30"
value={templates.automationRules.repeatEveryDays}
onChange={(e) => setTemplates(p => ({ ...p, automationRules: { ...p.automationRules, repeatEveryDays: e.target.value } }))}
className="w-full px-4 py-3 text-sm font-bold text-slate-700 focus:outline-none text-center"
/>
</div>
</div>
</div>
</div>
</div>
)}
<div className="flex gap-4 pt-2">
<button
onClick={() => setEditingTemplate(null)}
className="flex-1 py-4 bg-slate-100 text-slate-500 rounded-2xl font-black text-sm hover:bg-slate-200 transition-all active:scale-95"
>
Cancelar
</button>
<button
onClick={() => { handleSave(); setEditingTemplate(null); }}
className="flex-1 py-4 bg-indigo-600 text-white rounded-2xl font-black text-sm hover:bg-indigo-700 shadow-xl shadow-indigo-100 transition-all active:scale-95"
>
Salvar Modelo
</button>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default Messages;