feat: melhoria visual no registro de frequência, visualização de motivo no sino e padronização de modais

This commit is contained in:
Sidney 2026-04-27 09:06:55 -03:00
parent 041d31d54a
commit 840a8ee159
5 changed files with 88 additions and 10 deletions

View File

@ -27,3 +27,6 @@
5. **Build & Deploy Stability:** O pipeline de deploy deve obrigatoriamente utilizar `runs-on: self-hosted` e compilar apenas a plataforma `linux/arm64` (sem emulação QEMU). A atualização da stack em produção deve ser automatizada via container transiente do Watchtower. 5. **Build & Deploy Stability:** O pipeline de deploy deve obrigatoriamente utilizar `runs-on: self-hosted` e compilar apenas a plataforma `linux/arm64` (sem emulação QEMU). A atualização da stack em produção deve ser automatizada via container transiente do Watchtower.
6. **Express Compatibility**: Avoid using raw `/*` wildcards in Express 5 routes; use Regex paths (`/^\/route\/(.+)$/`) for compatibility with `path-to-regexp` v8. 6. **Express Compatibility**: Avoid using raw `/*` wildcards in Express 5 routes; use Regex paths (`/^\/route\/(.+)$/`) for compatibility with `path-to-regexp` v8.
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. 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.

View File

@ -11,11 +11,12 @@
- [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 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] 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] Estabilização de CI/CD: Transição para `runs-on: self-hosted` (ARM64 nativo) eliminando lentidão e crashes do QEMU.
- [x] Fix Tela Branca (Portal): Isolamento Físico absoluto do `storage.js` (SDK Backend) para pasta fora do `src` (server-only). Isso impede vazamentos de Node.js no navegador. - [x] Correção do Sino de Notificações: Botões sempre visíveis e suporte a anexo via chave `arquivo`.
- [x] Correção Financeiro (Portal): Resolvido erros de renderização em `Financeiro.tsx` e inconsistência de tipos em `Notifications.ts`. - [x] Persistência de Login (Manager): Login agora persiste no F5 via `localStorage`.
- [x] Pipeline Deploy: Ajustado volume do Docker no GitHub Actions de `~/.docker` para `$DOCKER_CONFIG` para compatibilidade total com o Runner. - [x] Polling de Dados (30s): Implementada sincronização automática entre Portal e Manager para notificações instantâneas (Self-Hosted).
- [x] Normalização de Imagens: Todas as fotos de alunos no Portal agora passam pela vacina `normalizePhotoUrl`. - [x] Deep Link de Notificação: Corrigida navegação do Sino para o Histórico de Aluno usando metadados `fromStudentId`.
- [ ] Próximo Passo: Verificar se o Watchtower sincronizou as imagens corretamente na produção. - [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.
### 💳 Módulo Financeiro (Portal do Aluno) ### 💳 Módulo Financeiro (Portal do Aluno)
- **Funcionalidades Implementadas:** - **Funcionalidades Implementadas:**

View File

@ -166,12 +166,14 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
const isJustificativa = notif.title.toLowerCase().includes('justificativa') || notif.message.toLowerCase().includes('justificativa'); const isJustificativa = notif.title.toLowerCase().includes('justificativa') || notif.message.toLowerCase().includes('justificativa');
let displayMessage = notif.message; let displayMessage = notif.message;
let justificationMotive = '';
let attachmentFromMessage = null; let attachmentFromMessage = null;
if (notif.message.startsWith('{')) { if (notif.message.startsWith('{')) {
try { try {
const parsed = JSON.parse(notif.message); const parsed = JSON.parse(notif.message);
displayMessage = parsed.motivo || displayMessage; displayMessage = parsed.text || parsed.motivo || displayMessage;
justificationMotive = parsed.motivo || '';
attachmentFromMessage = parsed.arquivo || parsed.arquivo_base64 || null; attachmentFromMessage = parsed.arquivo || parsed.arquivo_base64 || null;
} catch(e) {} } catch(e) {}
} }
@ -192,6 +194,12 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
<p className={`text-sm font-medium leading-relaxed mb-2 ${notif.read ? 'text-slate-400' : 'text-emerald-600/90'}`}> <p className={`text-sm font-medium leading-relaxed mb-2 ${notif.read ? 'text-slate-400' : 'text-emerald-600/90'}`}>
{displayMessage} {displayMessage}
</p> </p>
{isJustificativa && justificationMotive && (
<div className="bg-amber-50 p-2 rounded-lg border border-amber-100 mb-3">
<p className="text-[11px] font-bold text-amber-800 italic uppercase mb-1">Motivo enviado:</p>
<p className="text-xs text-amber-700 font-medium">"{justificationMotive}"</p>
</div>
)}
{(!notif.read) && ( {(!notif.read) && (
<div className="flex justify-end mt-2 gap-2 transition-opacity"> <div className="flex justify-end mt-2 gap-2 transition-opacity">
{isJustificativa && ( {isJustificativa && (
@ -248,7 +256,7 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
)} )}
{viewingAttachment && ( {viewingAttachment && (
<div className="fixed inset-0 bg-transparent z-[100] flex items-center justify-center p-4"> <div className="fixed inset-0 z-[100] flex items-center justify-center p-4">
<div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden shadow-2xl animate-in zoom-in-95 duration-200"> <div className="bg-white rounded-2xl w-full max-w-4xl max-h-[90vh] flex flex-col overflow-hidden shadow-2xl animate-in zoom-in-95 duration-200">
<div className="p-4 border-b flex items-center justify-between bg-slate-50"> <div className="p-4 border-b flex items-center justify-between bg-slate-50">
<h3 className="font-black text-slate-800 flex items-center gap-2"> <h3 className="font-black text-slate-800 flex items-center gap-2">

View File

@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { SchoolData, Attendance, Class, Student } from '../types'; import { SchoolData, Attendance, Class, Student } from '../types';
import { dbService } from '../services/dbService'; import { dbService } from '../services/dbService';
import { useDialog } from '../DialogContext'; import { useDialog } from '../DialogContext';
import { Search, Calendar, User, Clock, CheckCircle, XCircle, FileDown, BookOpen, Plus, X, AlertCircle, RefreshCw, ChevronRight, Trash2, FileSignature, Paperclip } from 'lucide-react'; import { Search, Calendar, User, Clock, CheckCircle, XCircle, FileDown, BookOpen, Plus, X, AlertCircle, RefreshCw, ChevronRight, Trash2, FileSignature, Paperclip, Eye } from 'lucide-react';
import jsPDF from 'jspdf'; import jsPDF from 'jspdf';
import 'jspdf-autotable'; import 'jspdf-autotable';
import { addHeader } from '../services/pdfService'; import { addHeader } from '../services/pdfService';
@ -48,6 +48,9 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
const [absenceLessonId, setAbsenceLessonId] = useState(''); const [absenceLessonId, setAbsenceLessonId] = useState('');
const [viewingAttachment, setViewingAttachment] = useState<string | null>(null); const [viewingAttachment, setViewingAttachment] = useState<string | null>(null);
const [attendanceForAttachment, setAttendanceForAttachment] = useState<Attendance | null>(null); const [attendanceForAttachment, setAttendanceForAttachment] = useState<Attendance | null>(null);
const [showJustificationTextModal, setShowJustificationTextModal] = useState(false);
const [currentJustificationText, setCurrentJustificationText] = useState('');
const [currentRecordForJustification, setCurrentRecordForJustification] = useState<Attendance | null>(null);
// Helper para normalizar URLs de fotos (vacina contra cache antigo) // Helper para normalizar URLs de fotos (vacina contra cache antigo)
const normalizePhotoUrl = (url?: string) => { const normalizePhotoUrl = (url?: string) => {
@ -654,7 +657,18 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
{isAwaiting || isPendente ? ( {isAwaiting || isPendente ? (
<span className="text-xs italic text-indigo-400">Aguardando registro ou justificativa...</span> <span className="text-xs italic text-indigo-400">Aguardando registro ou justificativa...</span>
) : justMotivo ? ( ) : justMotivo ? (
<p className="text-sm text-slate-600 truncate max-w-[200px]" title={justMotivo}>{justMotivo}</p> <button
onClick={() => {
setCurrentJustificationText(justMotivo);
setCurrentRecordForJustification(record);
setShowJustificationTextModal(true);
}}
className="p-2 text-amber-600 bg-amber-50 hover:bg-amber-100 rounded-xl transition-all shadow-sm border border-amber-100 flex items-center gap-2 group"
title="Ver Justificativa Completa"
>
<Eye size={18} className="group-hover:scale-110 transition-transform" />
<span className="text-[10px] font-bold uppercase tracking-wider">Ver Motivo</span>
</button>
) : ( ) : (
<span className="text-sm text-slate-300"></span> <span className="text-sm text-slate-300"></span>
)} )}
@ -860,6 +874,55 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
</div> </div>
</div> </div>
)} )}
{showJustificationTextModal && (
<div className="fixed inset-0 bg-transparent z-[110] flex items-center justify-center p-4 animate-in fade-in duration-300">
<div className="bg-white rounded-3xl w-full max-w-sm overflow-hidden shadow-2xl animate-in zoom-in-95 slide-in-from-bottom-4 duration-300 relative border border-slate-100">
{/* Design header */}
<div className="bg-amber-500 h-1.5 w-full absolute top-0 left-0"></div>
<div className="p-6 border-b border-slate-100 flex items-center justify-between bg-amber-50/30">
<h3 className="text-lg font-black text-amber-800 flex items-center gap-2">
<AlertCircle size={22} /> Motivo da Falta
</h3>
<button
onClick={() => setShowJustificationTextModal(false)}
className="p-2 text-amber-400 hover:text-amber-600 hover:bg-amber-100 rounded-lg transition-colors"
>
<X size={20} />
</button>
</div>
<div className="p-8">
<div className="bg-slate-50 p-6 rounded-2xl border border-slate-100 mb-6 max-h-[300px] overflow-y-auto">
<p className="text-slate-700 font-medium leading-relaxed italic text-center">
"{currentJustificationText}"
</p>
</div>
{currentRecordForJustification && !currentRecordForJustification.justificationAccepted && (
<button
onClick={() => {
const updated = (data.attendance || []).map(a => a.id === currentRecordForJustification.id ? { ...a, justificationAccepted: true } : a);
updateData({ attendance: updated });
dbService.saveData({ ...data, attendance: updated });
showAlert('Sucesso', 'Justificativa aceita com sucesso.', 'success');
setShowJustificationTextModal(false);
}}
className="w-full py-4 bg-indigo-600 text-white rounded-2xl font-black text-base hover:bg-indigo-700 shadow-lg shadow-indigo-100 flex items-center justify-center gap-2 transition-all active:scale-95"
>
<CheckCircle size={20} /> Aceitar Justificativa
</button>
)}
{currentRecordForJustification?.justificationAccepted && (
<div className="flex items-center justify-center gap-2 py-4 bg-emerald-50 text-emerald-700 rounded-2xl font-black uppercase text-xs border border-emerald-100">
<CheckCircle size={18} /> Aceita
</div>
)}
</div>
</div>
</div>
)}
</div> </div>
); );
}; };

View File

@ -312,7 +312,10 @@ app.post('/api/portal/frequencia/justificar', authMiddleware, upload.single('arq
studentId: 'admin', studentId: 'admin',
fromStudentId: req.user.studentId, // Identificador para navegação no Manager fromStudentId: req.user.studentId, // Identificador para navegação no Manager
title: 'Nova Justificativa de Falta', title: 'Nova Justificativa de Falta',
message: `${student?.name || 'Aluno'} enviou uma justificativa para a aula de ${date}.`, message: JSON.stringify({
text: `${student?.name || 'Aluno'} enviou uma justificativa para a aula de ${date}.`,
motivo: motivo.trim()
}),
attachment: publicUrl, attachment: publicUrl,
read: false, read: false,
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),