fix: total stability patch - session persistence, auto-polling and notification fixes
This commit is contained in:
parent
5e864dfcd6
commit
041d31d54a
|
|
@ -63,8 +63,9 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
|
||||||
if (!notif.read) handleMarkAsRead(notif.id);
|
if (!notif.read) handleMarkAsRead(notif.id);
|
||||||
|
|
||||||
if (notif.title.toLowerCase().includes('justificativa') || notif.message.toLowerCase().includes('justificativa')) {
|
if (notif.title.toLowerCase().includes('justificativa') || notif.message.toLowerCase().includes('justificativa')) {
|
||||||
if (onNavigateToStudent) {
|
const targetId = (notif as any).fromStudentId || notif.studentId;
|
||||||
onNavigateToStudent(notif.studentId);
|
if (onNavigateToStudent && targetId !== 'admin') {
|
||||||
|
onNavigateToStudent(targetId);
|
||||||
} else {
|
} else {
|
||||||
setView(View.AttendanceQuery);
|
setView(View.AttendanceQuery);
|
||||||
}
|
}
|
||||||
|
|
@ -94,8 +95,11 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
|
||||||
);
|
);
|
||||||
|
|
||||||
if (pendingAbsences.length > 0) {
|
if (pendingAbsences.length > 0) {
|
||||||
// Tenta achar pelo studentId mencionado na mensagem ou aceita o mais recente
|
// Tenta achar pelo studentId mencionado ou associado à notificação
|
||||||
const matchedAbsence = pendingAbsences[0]; // aceita o mais recente pendente
|
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 =>
|
const updatedAttendance = (data.attendance || []).map(a =>
|
||||||
a.id === matchedAbsence.id ? { ...a, justificationAccepted: true } : a
|
a.id === matchedAbsence.id ? { ...a, justificationAccepted: true } : a
|
||||||
|
|
@ -168,7 +172,7 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(notif.message);
|
const parsed = JSON.parse(notif.message);
|
||||||
displayMessage = parsed.motivo || displayMessage;
|
displayMessage = parsed.motivo || displayMessage;
|
||||||
attachmentFromMessage = parsed.arquivo_base64 || null;
|
attachmentFromMessage = parsed.arquivo || parsed.arquivo_base64 || null;
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -189,13 +193,14 @@ const AdminNotifications: React.FC<Props> = ({ data, updateData, setView, onNavi
|
||||||
{displayMessage}
|
{displayMessage}
|
||||||
</p>
|
</p>
|
||||||
{(!notif.read) && (
|
{(!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 && (
|
{isJustificativa && (
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (onNavigateToStudent) {
|
const targetId = (notif as any).fromStudentId || notif.studentId;
|
||||||
onNavigateToStudent(notif.studentId);
|
if (onNavigateToStudent && targetId !== 'admin') {
|
||||||
|
onNavigateToStudent(targetId);
|
||||||
} else {
|
} else {
|
||||||
setView(View.AttendanceQuery);
|
setView(View.AttendanceQuery);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(attendanceForAttachment.justification);
|
const parsed = JSON.parse(attendanceForAttachment.justification);
|
||||||
delete parsed.arquivo_base64;
|
delete parsed.arquivo_base64;
|
||||||
|
delete parsed.arquivo;
|
||||||
const updatedJustification = JSON.stringify(parsed);
|
const updatedJustification = JSON.stringify(parsed);
|
||||||
|
|
||||||
const updatedAttendance = (data.attendance || []).map(a =>
|
const updatedAttendance = (data.attendance || []).map(a =>
|
||||||
|
|
@ -593,7 +594,7 @@ const AttendanceQuery: React.FC<AttendanceQueryProps> = ({ data, updateData, dee
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(justMotivo);
|
const parsed = JSON.parse(justMotivo);
|
||||||
justMotivo = parsed.motivo || justMotivo;
|
justMotivo = parsed.motivo || justMotivo;
|
||||||
justAttachment = parsed.arquivo_base64 || null;
|
justAttachment = parsed.arquivo || parsed.arquivo_base64 || null;
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -32,9 +32,10 @@ interface SidebarProps {
|
||||||
setView: (view: View) => void;
|
setView: (view: View) => void;
|
||||||
user: User | null;
|
user: User | null;
|
||||||
logo?: string;
|
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 [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
const [isMobileOpen, setIsMobileOpen] = useState(false);
|
||||||
|
|
||||||
|
|
@ -147,7 +148,7 @@ const Sidebar: React.FC<SidebarProps> = ({ currentView, setView, user, logo }) =
|
||||||
)}
|
)}
|
||||||
{!isCollapsed && (
|
{!isCollapsed && (
|
||||||
<button
|
<button
|
||||||
onClick={() => window.location.reload()}
|
onClick={onLogout}
|
||||||
className="p-1.5 text-slate-400 hover:text-red-500 rounded-lg transition-all"
|
className="p-1.5 text-slate-400 hover:text-red-500 rounded-lg transition-all"
|
||||||
title="Sair"
|
title="Sair"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ import { DialogProvider } from './DialogContext';
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
const [isAuthenticated, setIsAuthenticated] = useState(false);
|
||||||
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
const [currentUser, setCurrentUser] = useState<User | null>(null);
|
||||||
|
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||||
const [currentView, setCurrentView] = useState<View>(View.Dashboard);
|
const [currentView, setCurrentView] = useState<View>(View.Dashboard);
|
||||||
const [deepLinkStudentId, setDeepLinkStudentId] = useState<string | null>(null);
|
const [deepLinkStudentId, setDeepLinkStudentId] = useState<string | null>(null);
|
||||||
const [deepLinkClassId, setDeepLinkClassId] = useState<string | null>(null);
|
const [deepLinkClassId, setDeepLinkClassId] = useState<string | null>(null);
|
||||||
|
|
@ -41,11 +42,23 @@ const App = () => {
|
||||||
|
|
||||||
// 0. Load from IndexedDB on mount
|
// 0. Load from IndexedDB on mount
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadLocal = async () => {
|
const loadSessionAndData = async () => {
|
||||||
const localData = await dbService.initData();
|
try {
|
||||||
setData(prev => ({ ...prev, ...localData }));
|
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)
|
// 1. Initial Cloud Fetch (Sync on Load)
|
||||||
|
|
@ -122,37 +135,28 @@ const App = () => {
|
||||||
dataRef.current = data;
|
dataRef.current = data;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
|
// 4. Polling Inteligente (Substitui o Realtime do Supabase no ambiente Self-Hosted)
|
||||||
|
// Verifica mudanças no servidor a cada 30 segundos
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isCloudEnabled) {
|
const pollInterval = setInterval(async () => {
|
||||||
console.log("📡 Iniciando escuta em tempo real para school_data...");
|
// Pequena validação para não interromper ações do usuário
|
||||||
// Cria um canal de escuta para a tabela school_data
|
if (syncStatus === 'syncing') return;
|
||||||
const channel = supabase
|
|
||||||
.channel('school_data_changes')
|
try {
|
||||||
.on(
|
const cloudData = await dbService.fetchFromCloud();
|
||||||
'postgres_changes',
|
if (cloudData && cloudData.lastUpdated !== dataRef.current.lastUpdated) {
|
||||||
{ event: 'UPDATE', schema: 'public', table: 'school_data', filter: 'id=eq.1' },
|
console.log("🔔 Polling: Novos dados detectados no servidor!");
|
||||||
(payload) => {
|
setData(cloudData);
|
||||||
// Quando houver um UPDATE (ex: Portal enviou justificativa)
|
dbService.saveData(cloudData);
|
||||||
const newData = payload.new.data as SchoolData;
|
setSyncStatus('saved');
|
||||||
|
}
|
||||||
// Só atualiza se for uma mudança externa (evita loops)
|
} catch (e) {
|
||||||
if (newData.lastUpdated !== dataRef.current.lastUpdated) {
|
// Silencioso em caso de erro de rede temporário
|
||||||
console.log("🔔 Nova mudança externa detectada em tempo real!");
|
}
|
||||||
setData(newData);
|
}, 30000); // 30 segundos
|
||||||
dbService.saveData(newData); // Sincroniza cache local
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.subscribe((status) => {
|
|
||||||
console.log("🔌 Status da conexão Realtime:", status);
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => {
|
return () => clearInterval(pollInterval);
|
||||||
console.log("⚰️ Encerrando canal de Realtime");
|
}, [syncStatus]);
|
||||||
supabase.removeChannel(channel);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}, [isCloudEnabled]);
|
|
||||||
|
|
||||||
const updateData = (newData: Partial<SchoolData>) => {
|
const updateData = (newData: Partial<SchoolData>) => {
|
||||||
setData(prev => ({
|
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) {
|
if (!isAuthenticated) {
|
||||||
return <Auth data={data} onLogin={(user) => {
|
return <Auth data={data} onLogin={(user) => {
|
||||||
|
localStorage.setItem('edumanager_session', JSON.stringify(user));
|
||||||
setCurrentUser(user);
|
setCurrentUser(user);
|
||||||
setIsAuthenticated(true);
|
setIsAuthenticated(true);
|
||||||
}} onUpdateUsers={handleUpdateUsers} />;
|
}} onUpdateUsers={handleUpdateUsers} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem('edumanager_session');
|
||||||
|
setIsAuthenticated(false);
|
||||||
|
setCurrentUser(null);
|
||||||
|
setCurrentView(View.Dashboard);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen bg-slate-50 relative">
|
<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">
|
<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 */}
|
{/* Sync Indicator - Green Strip on the Right */}
|
||||||
{syncStatus === 'syncing' && (
|
{syncStatus === 'syncing' && (
|
||||||
|
|
|
||||||
|
|
@ -308,10 +308,14 @@ app.post('/api/portal/frequencia/justificar', authMiddleware, upload.single('arq
|
||||||
}
|
}
|
||||||
|
|
||||||
notifications.push({
|
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',
|
title: 'Nova Justificativa de Falta',
|
||||||
message: `${student?.name || 'Aluno'} enviou uma justificativa para a aula de ${date}.`,
|
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;
|
schoolData.attendance = attendance;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue