feat: melhoria visual no registro de frequência, visualização de motivo no sino e padronização de modais
This commit is contained in:
parent
041d31d54a
commit
840a8ee159
|
|
@ -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.
|
||||||
|
|
|
||||||
11
MEMORY.md
11
MEMORY.md
|
|
@ -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:**
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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} /> Já Aceita
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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(),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue