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) => 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 = ({ data, updateData }) => { const { showAlert, showConfirm } = useDialog(); const [dbClasses, setDbClasses] = useState(data?.classes || []); const [dbCourses, setDbCourses] = useState(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(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 (

Mensagens

Configure modelos e rotinas de notificação via WhatsApp.

{/* Lado Esquerdo - Configurações e Disparos */}

Disparo em Massa

{targetType !== 'todos' && ( )}
{['{nome}', '{matricula}'].map(v => ( ))} {showMassEmojis && (
{commonEmojis.map(emoji => ( ))}
)}