import React, { useState, useEffect } from 'react'; import { NavLink, useLocation, useNavigate } from 'react-router-dom'; import { LayoutDashboard, Users, UserCircle, Bell, Search, Menu, X, LogOut, Hexagon, Settings, Building2, Sun, Moon, Loader2 } from 'lucide-react'; import { getAttendances, getUsers, getUserById, logout, searchGlobal, getNotifications, markNotificationAsRead, markAllNotificationsAsRead, deleteNotification, clearAllNotifications } from '../services/dataService'; import { User } from '../types'; const SidebarItem = ({ to, icon: Icon, label, collapsed }: { to: string, icon: any, label: string, collapsed: boolean }) => ( `flex items-center gap-3 px-4 py-3 rounded-xl transition-all duration-200 group ${ isActive ? 'bg-brand-yellow text-zinc-950 font-semibold shadow-md shadow-brand-yellow/20' : 'text-zinc-500 dark:text-dark-muted hover:bg-zinc-100 dark:hover:bg-dark-border hover:text-zinc-900 dark:hover:text-dark-text' }` } > {!collapsed && {label}} ); export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isDark, setIsDark] = useState(document.documentElement.classList.contains('dark')); const location = useLocation(); const navigate = useNavigate(); const [currentUser, setCurrentUser] = useState(null); // Search State const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState<{ members: User[], teams: any[], attendances: any[], organizations?: any[] }>({ members: [], teams: [], attendances: [], organizations: [] }); const [isSearching, setIsSearching] = useState(false); const [showSearchResults, setShowSearchResults] = useState(false); // Notifications State const [notifications, setNotifications] = useState([]); const [showNotifications, setShowNotifications] = useState(false); const unreadCount = notifications.filter(n => !n.is_read).length; const previousUnreadCountRef = React.useRef(unreadCount); const playNotificationSound = () => { if (currentUser?.sound_enabled !== false) { const audio = new Audio('/audio/notification.mp3'); audio.volume = 0.5; audio.play().catch(e => console.log('Audio play failed (browser policy):', e)); } }; const loadNotifications = async () => { const data = await getNotifications(); setNotifications(data); // Check if there are new unread notifications const newUnreadCount = data.filter((n: any) => !n.is_read).length; if (newUnreadCount > previousUnreadCountRef.current) { playNotificationSound(); } previousUnreadCountRef.current = newUnreadCount; }; useEffect(() => { const delayDebounceFn = setTimeout(async () => { if (searchQuery.length >= 2) { setIsSearching(true); const results = await searchGlobal(searchQuery); setSearchResults(results); setIsSearching(false); setShowSearchResults(true); } else { setSearchResults({ members: [], teams: [], attendances: [], organizations: [] }); setShowSearchResults(false); } }, 300); return () => clearTimeout(delayDebounceFn); }, [searchQuery]); const getSearchPlaceholder = () => { if (currentUser?.role === 'super_admin') return 'Buscar membros, equipes, atendimentos ou organizações...'; if (currentUser?.role === 'agent') return 'Buscar atendimentos...'; return 'Buscar membros, equipes ou atendimentos...'; }; useEffect(() => { const fetchCurrentUser = async () => { const storedUserId = localStorage.getItem('ctms_user_id'); if (!storedUserId) { navigate('/login'); return; } try { const user = await getUserById(storedUserId); if (user) { setCurrentUser(user); } else { navigate('/login'); } } catch (err) { console.error("Layout fetch failed:", err); } }; fetchCurrentUser(); loadNotifications(); const interval = setInterval(loadNotifications, 60000); return () => clearInterval(interval); }, [navigate]); const handleLogout = () => { logout(); navigate('/login'); }; const toggleDarkMode = () => { const newDark = !isDark; setIsDark(newDark); if (newDark) { document.documentElement.classList.add('dark'); document.cookie = "dark_mode=1; path=/; max-age=31536000"; } else { document.documentElement.classList.remove('dark'); document.cookie = "dark_mode=0; path=/; max-age=31536000"; } }; // Simple title mapping based on route const getPageTitle = () => { if (location.pathname === '/') return 'Dashboard'; if (location.pathname.includes('/admin/users')) return 'Membros'; if (location.pathname.includes('/admin/teams')) return 'Times'; if (location.pathname.includes('/users/')) return 'Histórico do Usuário'; if (location.pathname.includes('/attendances')) return 'Detalhes do Atendimento'; if (location.pathname.includes('/super-admin')) return 'Gestão de Organizações'; if (location.pathname.includes('/profile')) return 'Meu Perfil'; return 'CTMS'; }; if (!currentUser) return null; const isSuperAdmin = currentUser.role === 'super_admin'; return ( {/* Sidebar */} {/* Main Content */} {/* Header */} setIsMobileMenuOpen(true)} className="lg:hidden text-zinc-500 hover:text-zinc-900 dark:hover:text-white"> {/* Search Bar - Moved to left/center and made wider */} {isSearching ? : } setSearchQuery(e.target.value)} onFocus={() => searchQuery.length >= 2 && setShowSearchResults(true)} className="bg-transparent border-none outline-none text-sm ml-3 w-full text-zinc-700 dark:text-dark-text placeholder-zinc-400 dark:placeholder-dark-muted" /> {/* Search Results Dropdown */} {showSearchResults && ( {/* Organizations Section (Super Admin only) */} {searchResults.organizations && searchResults.organizations.length > 0 && ( Organizações {searchResults.organizations.map(o => ( { navigate(`/super-admin`); setShowSearchResults(false); setSearchQuery(''); }} className="w-full flex items-center gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-dark-border rounded-xl transition-colors text-left" > {o.name} {o.slug} • {o.status} ))} )} {/* Members Section */} {searchResults.members.length > 0 && ( Membros {searchResults.members.map(m => { const backendUrl = import.meta.env.PROD ? '' : 'http://localhost:3001'; const avatarSrc = m.avatar_url ? (m.avatar_url.startsWith('http') ? m.avatar_url : `${backendUrl}${m.avatar_url}`) : `https://ui-avatars.com/api/?name=${encodeURIComponent(m.name)}&background=random`; return ( { navigate(`/users/${m.slug || m.id}`); setShowSearchResults(false); setSearchQuery(''); }} className="w-full flex items-center gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-dark-border rounded-xl transition-colors text-left" > { (e.target as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(m.name)}&background=random`; }} /> {m.name} {m.email} ); })} )} {/* Teams Section */} {searchResults.teams.length > 0 && ( Equipes {searchResults.teams.map(t => ( { navigate(`/admin/teams`); setShowSearchResults(false); setSearchQuery(''); }} className="w-full flex items-center gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-dark-border rounded-xl transition-colors text-left" > {t.name} {t.description || 'Sem descrição'} ))} )} {/* Attendances Section */} {searchResults.attendances.length > 0 && ( Atendimentos {searchResults.attendances.map(a => ( { navigate(`/attendances/${a.id}`); setShowSearchResults(false); setSearchQuery(''); }} className="w-full flex items-center gap-3 p-2 hover:bg-zinc-50 dark:hover:bg-dark-border rounded-xl transition-colors text-left" > KPI {a.summary} {a.user_name} {new Date(a.created_at).toLocaleDateString()} ))} )} {searchResults.members.length === 0 && searchResults.teams.length === 0 && searchResults.attendances.length === 0 && (!searchResults.organizations || searchResults.organizations.length === 0) && ( Nenhum resultado encontrado para "{searchQuery}" )} )} {/* Dark Mode Toggle */} {isDark ? : } {/* Notifications */} setShowNotifications(!showNotifications)} className="p-2 text-zinc-500 dark:text-dark-muted hover:bg-zinc-100 dark:hover:bg-dark-border rounded-full relative transition-colors" > {unreadCount > 0 && ( )} {showNotifications && ( Notificações {unreadCount > 0 && ( { e.stopPropagation(); await markAllNotificationsAsRead(); loadNotifications(); }} className="text-xs text-brand-yellow hover:underline" > Marcar lidas )} {notifications.length > 0 && ( { e.stopPropagation(); await clearAllNotifications(); loadNotifications(); }} className="text-xs text-zinc-400 hover:text-red-500 hover:underline transition-colors" > Limpar tudo )} {notifications.length > 0 ? ( notifications.map(n => ( { if (!n.is_read) await markNotificationAsRead(n.id); if (n.link) navigate(n.link); setShowNotifications(false); loadNotifications(); }} > {n.type} {new Date(n.created_at).toLocaleDateString()} {n.title} {n.message} {/* Delete Button */} { e.stopPropagation(); await deleteNotification(n.id); loadNotifications(); }} className="absolute top-4 right-4 p-1 text-zinc-300 hover:text-red-500 opacity-0 group-hover:opacity-100 transition-all rounded-md hover:bg-red-50 dark:hover:bg-red-900/30" title="Remover notificação" > )) ) : ( Nenhuma notificação por enquanto. )} )} {/* Close notifications when clicking outside */} {showNotifications && setShowNotifications(false)} />} {/* Scrollable Content Area */} {children} {/* Overlay for mobile */} {isMobileMenuOpen && ( setIsMobileMenuOpen(false)} /> )} ); };
{n.message}