Rollback to stable frontend services commit
All checks were successful
Build and Deploy / build-and-push (push) Successful in 1m40s

This commit is contained in:
Cauê Faleiros
2026-06-09 16:00:48 -03:00
parent ed969b8c2c
commit fd0a076bc3
30 changed files with 1912 additions and 1957 deletions

View File

@@ -7,10 +7,11 @@ import {
} from 'lucide-react';
import {
getAttendances, getUsers, getUserById, logout, searchGlobal,
getActivityEvents, returnToSuperAdmin
getNotifications, markNotificationAsRead, markAllNotificationsAsRead,
deleteNotification, clearAllNotifications, returnToSuperAdmin
} from '../services/dataService';
import { User } from '../types';
import type { ActivityEvent } from '../services/activityService';
import notificationSound from '../assets/audio/notification.mp3';
const SidebarItem = ({ to, icon: Icon, label, collapsed }: { to: string, icon: any, label: string, collapsed: boolean }) => (
<NavLink
@@ -51,19 +52,53 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
const [isSearching, setIsSearching] = useState(false);
const [showSearchResults, setShowSearchResults] = useState(false);
// Activity Feed State
const [activityEvents, setActivityEvents] = useState<ActivityEvent[]>([]);
const [showActivity, setShowActivity] = useState(false);
// Notifications State
const [notifications, setNotifications] = useState<any[]>([]);
const [showNotifications, setShowNotifications] = useState(false);
const unreadCount = notifications.filter(n => !n.is_read).length;
const previousUnreadCountRef = React.useRef(0);
const isInitialLoadRef = React.useRef(true);
// Pre-initialize audio to ensure it's loaded and ready
const audioRef = React.useRef<HTMLAudioElement | null>(null);
const loadActivityEvents = async () => {
const data = await getActivityEvents();
setActivityEvents(data);
const playNotificationSound = () => {
if (currentUser?.sound_enabled !== false && audioRef.current) {
// Reset time to 0 to allow rapid replays
audioRef.current.currentTime = 0;
const playPromise = audioRef.current.play();
if (playPromise !== undefined) {
playPromise.catch(e => console.log('Audio play blocked by browser policy:', e));
}
}
};
const handleActivityClick = async () => {
const willOpen = !showActivity;
setShowActivity(willOpen);
if (willOpen) await loadActivityEvents();
const loadNotifications = async () => {
const data = await getNotifications();
const newUnreadCount = data.filter((n: any) => !n.is_read).length;
// Only play sound if it's NOT the first load AND the count actually increased
if (!isInitialLoadRef.current && newUnreadCount > previousUnreadCountRef.current) {
playNotificationSound();
}
setNotifications(data);
previousUnreadCountRef.current = newUnreadCount;
isInitialLoadRef.current = false;
};
const handleBellClick = async () => {
const willOpen = !showNotifications;
setShowNotifications(willOpen);
if (willOpen && unreadCount > 0) {
// Optimistic update
setNotifications(prev => prev.map(n => ({ ...n, is_read: true })));
previousUnreadCountRef.current = 0;
await markAllNotificationsAsRead();
loadNotifications();
}
};
useEffect(() => {
@@ -109,7 +144,9 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
}
};
fetchCurrentUser();
loadActivityEvents();
loadNotifications();
const interval = setInterval(loadNotifications, 10000);
return () => clearInterval(interval);
}, [navigate]);
const handleLogout = () => {
@@ -462,52 +499,94 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
{/* Notifications */}
<div className="relative">
<button
onClick={handleActivityClick}
onClick={handleBellClick}
className="p-2 text-zinc-500 dark:text-dark-muted hover:bg-zinc-100 dark:hover:bg-dark-border rounded-full relative transition-colors"
title="Atividades"
>
<Bell size={20} />
> <Bell size={20} />
{unreadCount > 0 && (
<span className="absolute top-1.5 right-2 w-2.5 h-2.5 bg-brand-yellow rounded-full border-2 border-white dark:border-dark-header"></span>
)}
</button>
{showActivity && (
{showNotifications && (
<div className="absolute top-full mt-2 right-0 w-80 bg-white dark:bg-dark-card border border-zinc-200 dark:border-dark-border rounded-2xl shadow-2xl overflow-hidden z-50 animate-in fade-in slide-in-from-top-2 duration-200">
<div className="p-4 border-b border-zinc-100 dark:border-dark-border flex justify-between items-center bg-zinc-50/50 dark:bg-dark-bg/50">
<h3 className="font-bold text-zinc-900 dark:text-dark-text">Atividades</h3>
<h3 className="font-bold text-zinc-900 dark:text-dark-text">Notificações</h3>
<div className="flex gap-3">
{unreadCount > 0 && (
<button
onClick={async (e) => {
e.stopPropagation();
await markAllNotificationsAsRead();
loadNotifications();
}}
className="text-xs text-brand-yellow hover:underline"
>
Marcar lidas
</button>
)}
{notifications.length > 0 && (
<button
onClick={async (e) => {
e.stopPropagation();
await clearAllNotifications();
loadNotifications();
}}
className="text-xs text-zinc-400 hover:text-red-500 hover:underline transition-colors"
>
Limpar tudo
</button>
)}
</div>
</div>
<div className="max-h-96 overflow-y-auto">
{activityEvents.length > 0 ? (
activityEvents.map(event => (
{notifications.length > 0 ? (
notifications.map(n => (
<div
key={event.id}
className="w-full relative group p-4 text-left hover:bg-zinc-50 dark:hover:bg-dark-border transition-colors border-b border-zinc-50 dark:border-dark-border/50 last:border-0"
key={n.id}
className={`w-full relative group p-4 text-left hover:bg-zinc-50 dark:hover:bg-dark-border transition-colors border-b border-zinc-50 dark:border-dark-border/50 last:border-0 ${!n.is_read ? 'bg-brand-yellow/5 dark:bg-brand-yellow/5' : ''}`}
>
<div
className={event.link ? 'cursor-pointer' : ''}
onClick={() => {
if (event.link) navigate(event.link);
setShowActivity(false);
className="cursor-pointer pr-6"
onClick={async () => {
if (!n.is_read) await markNotificationAsRead(n.id);
if (n.link) navigate(n.link);
setShowNotifications(false);
loadNotifications();
}}
>
<div className="flex justify-between items-start mb-1">
<span className={`text-xs font-bold uppercase tracking-wider ${
event.type === 'success' ? 'text-green-500' :
event.type === 'warning' ? 'text-orange-500' :
event.type === 'error' ? 'text-red-500' : 'text-blue-500'
n.type === 'success' ? 'text-green-500' :
n.type === 'warning' ? 'text-orange-500' :
n.type === 'error' ? 'text-red-500' : 'text-blue-500'
}`}>
{event.type === 'success' ? 'SUCESSO' : event.type === 'warning' ? 'AVISO' : event.type === 'error' ? 'ERRO' : 'INFO'}
{n.type === 'success' ? 'SUCESSO' : n.type === 'warning' ? 'AVISO' : n.type === 'error' ? 'ERRO' : 'INFO'}
</span>
<span className="text-[10px] text-zinc-400 dark:text-dark-muted">
{new Date(event.created_at).toLocaleDateString('pt-BR')}
{new Date(n.created_at).toLocaleDateString('pt-BR')}
</span>
</div>
<div className="text-sm font-bold text-zinc-900 dark:text-dark-text mb-0.5">{event.title}</div>
<p className="text-xs text-zinc-500 dark:text-dark-muted line-clamp-2">{event.message}</p>
<div className="text-sm font-bold text-zinc-900 dark:text-dark-text mb-0.5">{n.title}</div>
<p className="text-xs text-zinc-500 dark:text-dark-muted line-clamp-2">{n.message}</p>
</div>
{/* Delete Button */}
<button
onClick={async (e) => {
e.stopPropagation();
await deleteNotification(n.id);
loadNotifications();
}}
className="absolute top-4 right-4 p-1 text-zinc-300 hover:text-red-500 transition-all rounded-md hover:bg-red-50 dark:hover:bg-red-900/30"
title="Remover notificação"
>
<X size={14} />
</button>
</div>
))
) : (
<div className="p-8 text-center text-zinc-500 dark:text-dark-muted text-sm">
Nenhuma atividade por enquanto.
Nenhuma notificação por enquanto.
</div>
)}
</div>
@@ -515,7 +594,8 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
)}
</div>
{showActivity && <div className="fixed inset-0 z-40" onClick={() => setShowActivity(false)} />}
{/* Close notifications when clicking outside */}
{showNotifications && <div className="fixed inset-0 z-40" onClick={() => setShowNotifications(false)} />}
</div>
</header>
@@ -533,6 +613,12 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
/>
)}
{/* Hidden Audio Player for Notifications */}
<audio
ref={audioRef}
src={notificationSound}
preload="auto"
/>
</div>
);
};