From 065476df16ecbbc592be2b3046422042338fb226 Mon Sep 17 00:00:00 2001 From: Sidney Date: Tue, 28 Apr 2026 21:33:06 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20automa=C3=A7=C3=A3o=20de=20lembretes=20?= =?UTF-8?q?preventivos,=20refatora=C3=A7=C3=A3o=20de=20disparos=20seletivo?= =?UTF-8?q?s=20e=20padroniza=C3=A7=C3=A3o=20de=20modais?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- GEMINI.md | 4 +- MEMORY.md | 14 +- manager/check_columns.js | 16 ++ manager/components/AttendanceCapture.tsx | 2 +- manager/components/AttendanceQuery.tsx | 2 +- manager/components/Messages.tsx | 179 +++++++++++++++++------ manager/server.selfhosted.js | 98 +++++++++++-- manager/services/database.js | 7 + 8 files changed, 252 insertions(+), 70 deletions(-) create mode 100644 manager/check_columns.js diff --git a/GEMINI.md b/GEMINI.md index b57f685..cf96c85 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -29,4 +29,6 @@ 7. **Frontend Independence**: NEVER import files from `services/` or `server.js` directly into React components to prevent Node.js/SDK leakage (causes White Screen). Physical isolation is enforced: backend-only services (like MinIO/S3 storage) MUST stay outside the `src/` directory in Vite/React projects. Use `helpers.ts` for UI logic and standard `fetch` for API calls. 8. **Login Persistence**: Administrative sessions are persisted via `localStorage` ('edumanager_session'). The main entry point MUST validate the session on mount to ensure UX continuity. 9. **Real-time & Sync**: In self-hosted environments, use **Intelligent Polling (30s)** to synchronize notifications and critical data between Portal and Manager, as standard Supabase Realtime is disabled. -10. **Justification Logic**: Attendance justifications MUST include `fromStudentId` in notification metadata and support both `arquivo` and `arquivo_base64` keys for attachment compatibility. +10. **Justification Logic**: Attendance justifications MUST include `fromStudentId` in notification metadata and support both `arquivo` and `arquivo_base64` keys for attachment compatibility. Notifications SHOULD include the motive text in the `message` field (JSON format: `{text, motivo}`) to allow previews in the manager bell. +11. **Modal Standards**: System modals should utilize `bg-transparent` (no background darkening or blur) with `shadow-2xl` for depth, ensuring a clean and float-like aesthetic. +12. **Messages Automation**: Preventive reminders and overdue settings MUST be managed within their respective template modals (contextual logic), with independent manual triggers for each phase (Overdue vs. Upcoming). diff --git a/MEMORY.md b/MEMORY.md index 943c6ec..cf5ea6f 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -11,12 +11,14 @@ - [x] Correção do Crash 404 no Portal: Injeção da pasta `src/services` no container de produção para permitir o import do `storage.js`. - [x] Correção das Imagens de Prova: Normalização das URLs nas questões de avaliações (Portal e Manager). - [x] Estabilização de CI/CD: Transição para `runs-on: self-hosted` (ARM64 nativo) eliminando lentidão e crashes do QEMU. -- [x] Correção do Sino de Notificações: Botões sempre visíveis e suporte a anexo via chave `arquivo`. -- [x] Persistência de Login (Manager): Login agora persiste no F5 via `localStorage`. -- [x] Polling de Dados (30s): Implementada sincronização automática entre Portal e Manager para notificações instantâneas (Self-Hosted). -- [x] Deep Link de Notificação: Corrigida navegação do Sino para o Histórico de Aluno usando metadados `fromStudentId`. -- [x] Normalização de Anexos: Sincronização de chaves de justificativa entre Portal e página de Frequência. -- [ ] Próximo Passo: Monitorar o log de acesso dos usuários após a ativação da persistência de login. +- [x] Correção do Sino de Notificações: Botões sempre visíveis, suporte a anexo via chave `arquivo` e exibição do **Motivo da Falta** direto na lista do sino. +- [x] **Mensagens e Automação Financeira:** Implementado sistema seletivo de disparos (Atrasados vs Preventivos) com botões independentes e lógica de servidor desacoplada. +- [x] **Configuração Contextual:** Configurações de automação (dias antes/depois, repetições) movidas da sidebar global para dentro dos modais de cada modelo de mensagem. +- [x] **Refinamento de UX/UI:** Correção de modais de Frequência para padrão `bg-transparent` e adição de cor `indigo` para identificação de lembretes preventivos. +- [x] Registro de Frequência: Implementado ícone de olho para abrir modal com texto completo da justificativa e botão de aprovação rápida. +- [x] Padronização Visual: Modais atualizados para `bg-transparent` (sem escurecimento/blur) com `shadow-2xl` para efeito de flutuação premium. +- [x] Backup & Sincronia: Portal agora envia metadados de justificativa filtráveis pelo Gerenciador. +- [ ] Próximo Passo: Analisar logs de comportamento dos disparos preventivos em larga escala. ### 💳 Módulo Financeiro (Portal do Aluno) - **Funcionalidades Implementadas:** diff --git a/manager/check_columns.js b/manager/check_columns.js new file mode 100644 index 0000000..3d7d003 --- /dev/null +++ b/manager/check_columns.js @@ -0,0 +1,16 @@ +import pg from 'pg'; +const DATABASE_URL = process.env.DATABASE_URL || 'postgresql://edumanager:EduManager2026!Seguro@postgres:5432/edumanager'; +const pool = new pg.Pool({ connectionString: DATABASE_URL }); + +async function check() { + try { + const { rows } = await pool.query("SELECT column_name FROM information_schema.columns WHERE table_name = 'alunos_cobrancas'"); + console.log(rows.map(r => r.column_name).join(', ')); + } catch (e) { + console.error(e); + } finally { + await pool.end(); + process.exit(); + } +} +check(); diff --git a/manager/components/AttendanceCapture.tsx b/manager/components/AttendanceCapture.tsx index 61713b2..feb942f 100644 --- a/manager/components/AttendanceCapture.tsx +++ b/manager/components/AttendanceCapture.tsx @@ -348,7 +348,7 @@ const AttendanceCapture: React.FC = ({ data, updateData {/* Confirmation Modal */} {showConfirmModal && capturedImage && detectedStudent && ( -
+
{/* Blue Top Bar */}
diff --git a/manager/components/AttendanceQuery.tsx b/manager/components/AttendanceQuery.tsx index 365d877..b8f0b1f 100644 --- a/manager/components/AttendanceQuery.tsx +++ b/manager/components/AttendanceQuery.tsx @@ -327,7 +327,7 @@ const AttendanceQuery: React.FC = ({ data, updateData, dee {/* === MODAL 1: Lista de Alunos da Turma === */} {showStudentListModal && selectedClass && ( -
+
diff --git a/manager/components/Messages.tsx b/manager/components/Messages.tsx index d91f590..825e189 100644 --- a/manager/components/Messages.tsx +++ b/manager/components/Messages.tsx @@ -14,11 +14,14 @@ const defaultTemplates = { 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! 🎂🎈", automationRules: { sendOnDueDate: true, sendDaysAfter: '1', - repeatEveryDays: '3' + repeatEveryDays: '3', + sendDaysBefore: '3', + maxPreWarnings: '2' } }; @@ -117,6 +120,7 @@ const Messages: React.FC = ({ data, updateData }) => { boletoVencido: normalizeLineBreaks(templates.boletoVencido), cobrancaCancelada: normalizeLineBreaks(templates.cobrancaCancelada), cobrancaAtualizada: normalizeLineBreaks(templates.cobrancaAtualizada), + boletoAVencer: normalizeLineBreaks(templates.boletoAVencer), felizAniversario: normalizeLineBreaks(templates.felizAniversario) }; updateData({ messageTemplates: normalizedTemplates }); @@ -126,11 +130,11 @@ const Messages: React.FC = ({ data, updateData }) => { const handleDispararCobrancas = async () => { showConfirm( 'Disparar Cobranças', - 'Tem certeza que deseja processar e enviar as mensagens para TODOS os alunos com pagamentos atrasados agora?', + 'Tem certeza que deseja processar e enviar as mensagens para TODOS os alunos com pagamentos ATRASADOS agora?', async () => { setIsSending(true); try { - const resp = await fetch('/api/disparar_cobrancas', { method: 'POST' }); + 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'); @@ -146,6 +150,29 @@ const Messages: React.FC = ({ data, updateData }) => { ); }; + 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 () => { + setIsSending(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 { + setIsSending(false); + } + } + ); + }; + const handleMassSend = async () => { if (!messageText.trim()) { return showAlert('Aviso', 'Digite uma mensagem para enviar.', 'warning'); @@ -212,6 +239,7 @@ const Messages: React.FC = ({ data, updateData }) => { { 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}'] } ]; @@ -253,49 +281,6 @@ const Messages: React.FC = ({ data, updateData }) => { {/* Lado Esquerdo - Configurações e Disparos */}
-
-

- Automação -

- -
- - -
- -
- setTemplates(p => ({ ...p, automationRules: { ...p.automationRules, sendDaysAfter: e.target.value } }))} - className="w-16 px-3 py-2 border border-slate-200 rounded-lg text-center bg-white shadow-sm" - /> - dias -
-
- -
- -
- setTemplates(p => ({ ...p, automationRules: { ...p.automationRules, repeatEveryDays: e.target.value } }))} - className="w-16 px-3 py-2 border border-slate-200 rounded-lg text-center bg-white shadow-sm" - /> - dias -
-
-
-

@@ -370,6 +355,24 @@ const Messages: React.FC = ({ data, updateData }) => {

+
+

+ Lembretes Preventivos +

+

+ Envia avisos para boletos que vencem em até {templates.automationRules.sendDaysBefore} dias. +

+ +
+

Inadimplência @@ -433,6 +436,7 @@ const Messages: React.FC = ({ data, updateData }) => { 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 ( @@ -492,12 +496,91 @@ const Messages: React.FC = ({ data, updateData }) => { id="template-editor" value={(templates[editingTemplate.key as keyof typeof templates] as string) || ''} onChange={(e) => setTemplates(p => ({ ...p, [editingTemplate.key]: e.target.value }))} - rows={10} + 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' && ( +
+

+ Configuração de Disparo Preventivo +

+
+
+ +
+ 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" + /> +
+

Dias antes do vencimento para avisar.

+
+
+ +
+ 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" + /> +
+

Limite de avisos recebidos pelo aluno.

+
+
+
+ )} + + {editingTemplate.key === 'boletoVencido' && ( +
+

+ Configuração de Inadimplência +

+ +
+ + +
+
+ +
+ 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" + /> +
+
+
+ +
+ 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" + /> +
+
+
+
+
+ )} + +