From 88eee2726514b131beda927f5da2fc9c0e64be2a Mon Sep 17 00:00:00 2001 From: Sidney Date: Thu, 14 May 2026 09:06:22 -0300 Subject: [PATCH] feat: enhance mass send (first name, dual dispatch, emojis, attachments) and refine UI --- GEMINI.md | 1 + MEMORY.md | 3 + manager/components/Finance.tsx | 37 +++++----- manager/components/Messages.tsx | 112 +++++++++++++++++++++++++----- manager/components/ReportCard.tsx | 6 +- manager/server.selfhosted.js | 39 +++++++++-- 6 files changed, 152 insertions(+), 46 deletions(-) diff --git a/GEMINI.md b/GEMINI.md index 4575822..91db9d1 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -61,3 +61,4 @@ 32. **Numerical Type Enforcement**: When serving data from PostgreSQL `NUMERIC` columns, the backend MUST map the result to ensure amounts are sent as `Number` (not String) to prevent string concatenation bugs in the frontend. 33. **Reverse Sync Requirement (SQL -> JSON)**: To ensure visual consistency and avoid automation errors (like double billing), the system MUST perform a reverse sync from `alunos_cobrancas` (SQL) to `school_data.payments` (JSON) during server initialization and on-demand via the `/api/admin/sync-finance-json` endpoint. 34. **Deletion & Notification Order (Async)**: When processing deletions that trigger notifications (e.g., WhatsApp via Asaas Webhook), local database deletion MUST happen only AFTER the notification dispatch. Manual deletion routes should only trigger the external API delete and wait for the webhook to finalize local cleanup, ensuring data availability for message variables. +35. **Mass Send Standard (V3)**: The mass send feature MUST use the student's or guardian's first name for the {nome} variable (via `.split(' ')[0]`). It MUST support dual dispatch (sending to both student and guardian if phones are distinct) and allow attachments (Image/PDF) handled via `multipart/form-data` and the Evolution API `sendMedia` endpoint. diff --git a/MEMORY.md b/MEMORY.md index 05d125c..7c8e89a 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -119,6 +119,9 @@ ### 📢 Automação de Mensagens - [x] **Estabilização de Lembretes (V2):** Resolvido bug de deslocamento de fuso horário (-1 dia) nas datas de vencimento. Implementadas ferramentas de debug (ignorar trava diária e reset de contadores) e **controle manual de delay para disparos em massa** para facilitar testes e garantir segurança contra banimento. +- [x] **Filtragem Inteligente de Boletim:** Refatorada a aba de Boletim no Manager para exibir provas e atividades apenas quando houver submissão do aluno, eliminando a poluição visual de outras turmas. +- [x] **Otimização Financeira (Mobile):** Cabeçalho da aba Financeiro redesenhado para ser 100% responsivo, com botões compactos e ícones inteligentes que economizam espaço em dispositivos móveis. +- [x] **Disparo em Massa V3:** Implementada lógica de primeiro nome (`.split(' ')[0]`), envio simultâneo para Aluno e Responsável (quando contatos forem diferentes), painel de 25 emojis temáticos e suporte a anexos (Imagem/PDF) via Evolution API. ## 📋 Próximos Passos Pendentes diff --git a/manager/components/Finance.tsx b/manager/components/Finance.tsx index e7234a9..e95c33a 100644 --- a/manager/components/Finance.tsx +++ b/manager/components/Finance.tsx @@ -861,40 +861,41 @@ const Finance: React.FC = ({ data, updateData }) => {

Financeiro

Gestão de mensalidades vinculadas a contratos e cursos.

-
- -
- -
+
+ + +
diff --git a/manager/components/Messages.tsx b/manager/components/Messages.tsx index a7e1a85..8d74436 100644 --- a/manager/components/Messages.tsx +++ b/manager/components/Messages.tsx @@ -1,7 +1,7 @@ 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 } from 'lucide-react'; +import { MessageSquare, Save, Info, Settings, Send, Clock, AlertTriangle, FileText, CheckCircle, Cake, X, Power, BookOpen, Smile, Paperclip } from 'lucide-react'; interface MessagesProps { data: SchoolData; @@ -68,7 +68,10 @@ const Messages: React.FC = ({ data, updateData }) => { 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<{ @@ -217,38 +220,50 @@ const Messages: React.FC = ({ data, updateData }) => { return showAlert('Erro', 'Nenhum aluno com telefone cadastrado foi selecionado.', 'error'); } - const payloadAlunos = validStudents.map(a => { - let nome = a.name; - let telefone = a.phone; + const payloadAlunos = validStudents.flatMap(a => { + const entries = []; - if (a.birthDate) { - const birthDate = new Date(a.birthDate); - const age = Math.abs(new Date(Date.now() - birthDate.getTime()).getUTCFullYear() - 1970); - if (age < 18) { - nome = a.guardianName || a.name; - telefone = a.guardianPhone || a.phone; - } + // Entrada para o Aluno + if (a.phone) { + entries.push({ + nome: a.name.split(' ')[0], + telefone: a.phone, + matricula: a.enrollmentNumber || '—' + }); } - return { nome, telefone, 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', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - alunos: payloadAlunos, - mensagem: normalizeLineBreaks(messageText), - delay: parseInt(massDelay) || 60 - }) + 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'); @@ -342,7 +357,7 @@ const Messages: React.FC = ({ data, updateData }) => {
-
+
{['{nome}', '{matricula}'].map(v => ( ))} + + + + {showMassEmojis && ( +
+ {commonEmojis.map(emoji => ( + + ))} +
+ )}