diff --git a/backend/index.js b/backend/index.js index 79a4efe..226cbdb 100644 --- a/backend/index.js +++ b/backend/index.js @@ -551,7 +551,7 @@ apiRouter.delete('/notifications/:id', async (req, res) => { } }); -apiRouter.delete('/notifications', async (req, res) => { +apiRouter.delete('/notifications/clear-all', async (req, res) => { try { await pool.query( 'DELETE FROM notifications WHERE user_id = ?', @@ -918,6 +918,25 @@ apiRouter.put('/teams/:id', requireRole(['admin', 'owner', 'super_admin']), asyn } }); +apiRouter.delete('/teams/:id', requireRole(['admin', 'owner', 'super_admin']), async (req, res) => { + try { + const [existing] = await pool.query('SELECT tenant_id FROM teams WHERE id = ?', [req.params.id]); + if (existing.length === 0) return res.status(404).json({ error: 'Not found' }); + if (req.user.role !== 'super_admin' && existing[0].tenant_id !== req.user.tenant_id) { + return res.status(403).json({ error: 'Acesso negado.' }); + } + + // Set users team_id to NULL to prevent orphan foreign key issues if constrained + await pool.query('UPDATE users SET team_id = NULL WHERE team_id = ?', [req.params.id]); + await pool.query('DELETE FROM teams WHERE id = ?', [req.params.id]); + + res.json({ message: 'Team deleted successfully.' }); + } catch (error) { + console.error('Delete team error:', error); + res.status(500).json({ error: error.message }); + } +}); + apiRouter.post('/tenants', requireRole(['super_admin']), async (req, res) => { diff --git a/components/Layout.tsx b/components/Layout.tsx index 86e8e56..73e9a13 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -2,7 +2,8 @@ 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, Layers + Hexagon, Settings, Building2, Sun, Moon, Loader2, Layers, + ChevronLeft, ChevronRight } from 'lucide-react'; import { getAttendances, getUsers, getUserById, logout, searchGlobal, @@ -29,11 +30,20 @@ const SidebarItem = ({ to, icon: Icon, label, collapsed }: { to: string, icon: a export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); + const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(() => { + return localStorage.getItem('ctms_sidebar_collapsed') === 'true'; + }); const [isDark, setIsDark] = useState(document.documentElement.classList.contains('dark')); const location = useLocation(); const navigate = useNavigate(); const [currentUser, setCurrentUser] = useState(null); + const toggleSidebar = () => { + const newState = !isSidebarCollapsed; + setIsSidebarCollapsed(newState); + localStorage.setItem('ctms_sidebar_collapsed', String(newState)); + }; + // Search State const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState<{ members: User[], teams: any[], attendances: any[], organizations?: any[] }>({ members: [], teams: [], attendances: [], organizations: [] }); @@ -49,12 +59,6 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => // Pre-initialize audio to ensure it's loaded and ready const audioRef = React.useRef(null); - useEffect(() => { - // Determine base path correctly whether in prod or dev - const basePath = import.meta.env.PROD ? '' : 'http://localhost:3001'; - audioRef.current = new Audio(`${basePath}/audio/notification.mp3`); - audioRef.current.volume = 0.5; - }, []); const playNotificationSound = () => { if (currentUser?.sound_enabled !== false && audioRef.current) { @@ -69,7 +73,6 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => const loadNotifications = async () => { const data = await getNotifications(); - setNotifications(data); const newUnreadCount = data.filter((n: any) => !n.is_read).length; @@ -78,10 +81,24 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => 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(() => { const delayDebounceFn = setTimeout(async () => { if (searchQuery.length >= 2) { @@ -167,92 +184,125 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) =>
{/* Sidebar */} @@ -424,11 +474,10 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {/* Notifications */}
- +
+ + +
)}

{t.name}

diff --git a/services/dataService.ts b/services/dataService.ts index bbe56ec..400abb2 100644 --- a/services/dataService.ts +++ b/services/dataService.ts @@ -71,7 +71,7 @@ export const deleteNotification = async (id: string): Promise => { export const clearAllNotifications = async (): Promise => { try { - const response = await fetch(`${API_URL}/notifications`, { + const response = await fetch(`${API_URL}/notifications/clear-all`, { method: 'DELETE', headers: getHeaders() }); @@ -406,6 +406,19 @@ export const updateTeam = async (id: string, teamData: any): Promise => } }; +export const deleteTeam = async (id: string): Promise => { + try { + const response = await fetch(`${API_URL}/teams/${id}`, { + method: 'DELETE', + headers: getHeaders() + }); + return response.ok; + } catch (error) { + console.error("API Error (deleteTeam):", error); + return false; + } +}; + export const createTenant = async (tenantData: any): Promise<{ success: boolean; message?: string }> => { try { const response = await fetch(`${API_URL}/tenants`, {