diff --git a/GEMINI.md b/GEMINI.md index 91db9d1..f72f396 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -62,3 +62,4 @@ 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. +36. **Frequency Visibility & Justification Tracking**: All attendance records of type `absence` MUST be excluded from the "Presence Time" calculation in the Portal, showing a dash (`—`) instead. Justifications MUST store the `submittedAt` timestamp in both SQL and JSON formats to allow auditing and clear display of submission time in the UI. diff --git a/MEMORY.md b/MEMORY.md index edd40a9..a6597a3 100644 --- a/MEMORY.md +++ b/MEMORY.md @@ -41,13 +41,17 @@ - [x] **Feedback Real de Exclusão:** Implementado `showAlert` detalhado no Financeiro e confirmação real via **Sino de Notificações** (Admin) após o processamento do Webhook. - [x] **Refinamento do Sino (AdminBell):** O botão "Ver Anexo" agora é exclusivo de notificações de justificativa que possuem arquivo físico, ocultando-se em notificações de sistema que usam o campo anexo para metadados JSON. - [x] **Blindagem de UI (Financeiro):** Corrigido potencial erro de referência no parse de erro do Asaas que causava "White Screen" em exclusões negadas. +- [x] **Correção de Visibilidade (Frequência):** Resolvido bug onde faltas verificadas apareciam como "Presente" no Portal devido a lógica de filtro incompleta. +- [x] **Hora Presença Inteligente:** Coluna de horário no Portal agora exibe `—` para faltas e justificativas, eliminando a confusão com horários de aula. +- [x] **Rastreamento de Justificativas:** Implementado campo `submittedAt` para gravar o momento exato do envio da justificativa (JSON e SQL). +- [x] **Detalhamento de Envio:** Portal agora exibe "Enviada em: DD/MM às HH:MM" na lista de frequência para transparência do aluno. ## 📋 Próximos Passos - [ ] Iniciar a migração do módulo Financeiro para 100% SQL seguindo o padrão do Boletim. - [ ] Módulo Financeiro SQL: Iniciar a migração total do financeiro para PostgreSQL (padrão `notas_boletim`). -- [x] **Correção de Notas (Boletim):** Resolvido bug de visibilidade onde as notas não apareciam devido a race conditions no carregamento e mapeamento incompleto do `examId`. -- [x] **Git Push Realizado:** Correções de deploy e visibilidade de notas enviadas ao repositório. +- [x] **Correção de Notas (Boletim):** Resolvido `ReferenceError: subsMap is not defined` que impedia o carregamento das notas individuais. +- [x] **Git Push Realizado:** Correções de escopo enviadas ao repositório. - [ ] **Monitoramento:** Validar a exibição das notas após o reinício dos containers. - [ ] Otimização de Build: Re-explorar o cache do Docker. diff --git a/portal/server.selfhosted.js b/portal/server.selfhosted.js index 8be4e1d..4a71308 100644 --- a/portal/server.selfhosted.js +++ b/portal/server.selfhosted.js @@ -322,16 +322,17 @@ app.post('/api/portal/frequencia/justificar', authMiddleware, upload.single('arq const fullDateStr = date; const justificationPayload = JSON.stringify({ motivo: motivo.trim(), arquivo: publicUrl }); - let recordIndex = attendance.findIndex(a => a.studentId === req.user.studentId && a.date === fullDateStr); + const submittedAt = new Date().toISOString(); if (recordIndex !== -1) { const existing = attendance[recordIndex]; if (existing.type === 'presence') return res.status(400).json({ error: 'Não é possível justificar uma presença' }); - attendance[recordIndex] = { ...existing, justification: justificationPayload }; + attendance[recordIndex] = { ...existing, justification: justificationPayload, submittedAt }; } else { const newRecord = { id: `att-just-${Date.now()}`, studentId: req.user.studentId, classId: student?.classId || '', date: fullDateStr, verified: false, type: 'absence', justification: justificationPayload, + submittedAt }; attendance.push(newRecord); recordIndex = attendance.length - 1; @@ -365,10 +366,10 @@ app.post('/api/portal/frequencia/justificar', authMiddleware, upload.single('arq // Sincronização Imediata com Tabela Relacional try { await pool.query( - `INSERT INTO frequencias (id, aluno_id, turma_id, data, verificado, tipo, justificativa) - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT (id) DO UPDATE SET justificativa = EXCLUDED.justificativa, justificativa_aceita = FALSE`, - [attendance[recordIndex].id, req.user.studentId, student?.classId || '', fullDateStr, false, 'absence', justificationPayload] + `INSERT INTO frequencias (id, aluno_id, turma_id, data, verificado, tipo, justificativa, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) + ON CONFLICT (id) DO UPDATE SET justificativa = EXCLUDED.justificativa, justificativa_aceita = FALSE, created_at = EXCLUDED.created_at`, + [attendance[recordIndex].id, req.user.studentId, student?.classId || '', fullDateStr, false, 'absence', justificationPayload, submittedAt] ); } catch (dbErr) { console.error('[Portal:Justificação] Erro ao sincronizar tabela relacional:', dbErr.message); diff --git a/portal/src/pages/Frequencia.tsx b/portal/src/pages/Frequencia.tsx index 5b263a7..38c8004 100644 --- a/portal/src/pages/Frequencia.tsx +++ b/portal/src/pages/Frequencia.tsx @@ -448,8 +448,8 @@ export default function Frequencia() { const isCancelled = lesson.status === 'cancelled'; const isRescheduled = lesson.status === 'rescheduled'; - // PREREQUISITE: 'presence' type OR verified status counts as real presence - const isPresent = atts.some(a => a.type === 'presence' || a.verified === true); + // PREREQUISITE: 'presence' type OR verified (but NOT absence) counts as real presence + const isPresent = atts.some(a => a.type === 'presence' || (a.verified === true && a.type !== 'absence')); const hasJustification = atts.some(a => !!a.justification); const activeJustification = atts.find(a => !!a.justification); const justText = parseJustification(activeJustification?.justification); @@ -571,7 +571,7 @@ export default function Frequencia() {