fix: total stability patch - session persistence, auto-polling and notification fixes

This commit is contained in:
Sidney 2026-04-24 09:39:08 -03:00
parent 5e864dfcd6
commit 041d31d54a
5 changed files with 78 additions and 47 deletions

View File

@ -63,8 +63,9 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
if (!notif.read) handleMarkAsRead(notif.id);
if (notif.title.toLowerCase().includes('justificativa') || notif.message.toLowerCase().includes('justificativa')) {
if (onNavigateToStudent) {
onNavigateToStudent(notif.studentId);
const targetId = (notif as any).fromStudentId || notif.studentId;
if (onNavigateToStudent && targetId !== 'admin') {
onNavigateToStudent(targetId);
} else {
setView(View.AttendanceQuery);
}
@ -94,8 +95,11 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
);
if (pendingAbsences.length > 0) {
// Tenta achar pelo studentId mencionado na mensagem ou aceita o mais recente
const matchedAbsence = pendingAbsences[0]; // aceita o mais recente pendente
// Tenta achar pelo studentId mencionado ou associado à notificação
const targetId = (notif as any).fromStudentId;
const matchedAbsence = targetId
? pendingAbsences.find(a => a.studentId === targetId) || pendingAbsences[0]
: pendingAbsences[0];
const updatedAttendance = (data.attendance || []).map(a =>
a.id === matchedAbsence.id ? { ...a, justificationAccepted: true } : a
@ -168,7 +172,7 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
try {
const parsed = JSON.parse(notif.message);
displayMessage = parsed.motivo || displayMessage;
attachmentFromMessage = parsed.arquivo_base64 || null;
attachmentFromMessage = parsed.arquivo || parsed.arquivo_base64 || null;
} catch(e) {}
}
@ -189,13 +193,14 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
{displayMessage}
</p>
{(!notif.read) && (
<div className="flex justify-end mt-2 gap-2 opacity-0 group-hover:opacity-100 transition-opacity">
<div className="flex justify-end mt-2 gap-2 transition-opacity">
{isJustificativa && (
<button
onClick={(e) => {
e.stopPropagation();
if (onNavigateToStudent) {
onNavigateToStudent(notif.studentId);
const targetId = (notif as any).fromStudentId || notif.studentId;
if (onNavigateToStudent && targetId !== 'admin') {
onNavigateToStudent(targetId);
} else {
setView(View.AttendanceQuery);
}

View File

@ -111,6 +111,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
try {
const parsed = JSON.parse(attendanceForAttachment.justification);
delete parsed.arquivo_base64;
delete parsed.arquivo;
const updatedJustification = JSON.stringify(parsed);
const updatedAttendance = (data.attendance || []).map(a =>
@ -593,7 +594,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
try {
const parsed = JSON.parse(justMotivo);
justMotivo = parsed.motivo || justMotivo;
justAttachment = parsed.arquivo_base64 || null;
justAttachment = parsed.arquivo || parsed.arquivo_base64 || null;
} catch(e) {}
}

View File

@ -32,9 +32,10 @@ interface SidebarProps {
setView: (view: View) => void;
user: User | null;
logo?: string;
onLogout: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ currentView, setView, user, logo }) => {
const Sidebar: React.FC<SidebarProps> = ({ currentView, setView, user, logo, onLogout }) => {
const [isCollapsed, setIsCollapsed] = useState(false);
const [isMobileOpen, setIsMobileOpen] = useState(false);
@ -147,7 +148,7 @@ const Sidebar: React.FC<SidebarProps> = ({ currentView, setView, user, logo }) =
)}
{!isCollapsed && (
<button
onClick={() => window.location.reload()}
onClick={onLogout}
className="p-1.5 text-slate-400 hover:text-red-500 rounded-lg transition-all"
title="Sair"
>

View File

@ -28,6 +28,7 @@ import { DialogProvider } from './DialogContext';
const App = () => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [currentUser, setCurrentUser] = useState<User | null>(null);
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
const [currentView, setCurrentView] = useState<View>(View.Dashboard);
const [deepLinkStudentId, setDeepLinkStudentId] = useState<string | null>(null);
const [deepLinkClassId, setDeepLinkClassId] = useState<string | null>(null);
@ -41,11 +42,23 @@ const App = () => {
// 0. Load from IndexedDB on mount
useEffect(() => {
const loadLocal = async () => {
const localData = await dbService.initData();
setData(prev => ({ ...prev, ...localData }));
const loadSessionAndData = async () => {
try {
const savedSession = localStorage.getItem('edumanager_session');
if (savedSession) {
const user = JSON.parse(savedSession);
setCurrentUser(user);
setIsAuthenticated(true);
}
const localData = await dbService.initData();
setData(prev => ({ ...prev, ...localData }));
} catch (e) {
console.error("Erro ao carregar sessão:", e);
} finally {
setIsCheckingAuth(false);
}
};
loadLocal();
loadSessionAndData();
}, []);
// 1. Initial Cloud Fetch (Sync on Load)
@ -122,37 +135,28 @@ const App = () => {
dataRef.current = data;
}, [data]);
// 4. Polling Inteligente (Substitui o Realtime do Supabase no ambiente Self-Hosted)
// Verifica mudanças no servidor a cada 30 segundos
useEffect(() => {
if (isCloudEnabled) {
console.log("📡 Iniciando escuta em tempo real para school_data...");
// Cria um canal de escuta para a tabela school_data
const channel = supabase
.channel('school_data_changes')
.on(
'postgres_changes',
{ event: 'UPDATE', schema: 'public', table: 'school_data', filter: 'id=eq.1' },
(payload) => {
// Quando houver um UPDATE (ex: Portal enviou justificativa)
const newData = payload.new.data as SchoolData;
const pollInterval = setInterval(async () => {
// Pequena validação para não interromper ações do usuário
if (syncStatus === 'syncing') return;
// Só atualiza se for uma mudança externa (evita loops)
if (newData.lastUpdated !== dataRef.current.lastUpdated) {
console.log("🔔 Nova mudança externa detectada em tempo real!");
setData(newData);
dbService.saveData(newData); // Sincroniza cache local
}
}
)
.subscribe((status) => {
console.log("🔌 Status da conexão Realtime:", status);
});
try {
const cloudData = await dbService.fetchFromCloud();
if (cloudData && cloudData.lastUpdated !== dataRef.current.lastUpdated) {
console.log("🔔 Polling: Novos dados detectados no servidor!");
setData(cloudData);
dbService.saveData(cloudData);
setSyncStatus('saved');
}
} catch (e) {
// Silencioso em caso de erro de rede temporário
}
}, 30000); // 30 segundos
return () => {
console.log("⚰️ Encerrando canal de Realtime");
supabase.removeChannel(channel);
};
}
}, [isCloudEnabled]);
return () => clearInterval(pollInterval);
}, [syncStatus]);
const updateData = (newData: Partial<SchoolData>) => {
setData(prev => ({
@ -228,16 +232,32 @@ const App = () => {
}
};
if (isCheckingAuth) {
return (
<div className="min-h-screen bg-slate-50 flex items-center justify-center">
<RefreshCw size={48} className="text-indigo-600 animate-spin" />
</div>
);
}
if (!isAuthenticated) {
return <Auth data={data} onLogin={(user) => {
localStorage.setItem('edumanager_session', JSON.stringify(user));
setCurrentUser(user);
setIsAuthenticated(true);
}} onUpdateUsers={handleUpdateUsers} />;
}
const handleLogout = () => {
localStorage.removeItem('edumanager_session');
setIsAuthenticated(false);
setCurrentUser(null);
setCurrentView(View.Dashboard);
};
return (
<div className="flex min-h-screen bg-slate-50 relative">
<Sidebar currentView={currentView} setView={setCurrentView} user={currentUser} logo={data.logo} />
<Sidebar currentView={currentView} setView={setCurrentView} user={currentUser} logo={data.logo} onLogout={handleLogout} />
<main className="flex-1 w-full overflow-y-auto max-h-screen pt-16 md:pt-0 relative">
{/* Sync Indicator - Green Strip on the Right */}
{syncStatus === 'syncing' && (

View File

@ -308,10 +308,14 @@ app.post('/api/portal/frequencia/justificar', authMiddleware, upload.single('arq
}
notifications.push({
id: `notif-${Date.now()}`, studentId: 'admin',
id: `notif-${Date.now()}`,
studentId: 'admin',
fromStudentId: req.user.studentId, // Identificador para navegação no Manager
title: 'Nova Justificativa de Falta',
message: `${student?.name || 'Aluno'} enviou uma justificativa para a aula de ${date}.`,
attachment: publicUrl, read: false, createdAt: new Date().toISOString(),
attachment: publicUrl,
read: false,
createdAt: new Date().toISOString(),
});
schoolData.attendance = attendance;