diff --git a/backend/index.js b/backend/index.js index 9ae4887..491396b 100644 --- a/backend/index.js +++ b/backend/index.js @@ -399,11 +399,11 @@ apiRouter.post('/users', requireRole(['admin', 'manager', 'super_admin']), async }); apiRouter.put('/users/:id', async (req, res) => { - const { name, bio, role, team_id, status, email } = req.body; + const { name, bio, role, team_id, status, email, sound_enabled } = req.body; try { const [existing] = await pool.query('SELECT * FROM users WHERE id = ?', [req.params.id]); if (existing.length === 0) return res.status(404).json({ error: 'Not found' }); - + const isSelf = req.user.id === req.params.id; const isManagerOrAdmin = ['admin', 'owner', 'manager', 'super_admin'].includes(req.user.role); @@ -419,6 +419,7 @@ apiRouter.put('/users/:id', async (req, res) => { const finalTeamId = isManagerOrAdmin && team_id !== undefined ? team_id : existing[0].team_id; const finalStatus = isManagerOrAdmin && status !== undefined ? status : existing[0].status; const finalEmail = email !== undefined ? email : existing[0].email; + const finalSoundEnabled = isSelf && sound_enabled !== undefined ? sound_enabled : (existing[0].sound_enabled ?? true); if (finalEmail !== existing[0].email) { const [emailCheck] = await pool.query('SELECT id FROM users WHERE email = ? AND id != ?', [finalEmail, req.params.id]); @@ -426,12 +427,11 @@ apiRouter.put('/users/:id', async (req, res) => { } await pool.query( - 'UPDATE users SET name = ?, bio = ?, email = ?, role = ?, team_id = ?, status = ? WHERE id = ?', - [name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, req.params.id] + 'UPDATE users SET name = ?, bio = ?, email = ?, role = ?, team_id = ?, status = ?, sound_enabled = ? WHERE id = ?', + [name || existing[0].name, bio !== undefined ? bio : existing[0].bio, finalEmail, finalRole, finalTeamId || null, finalStatus, finalSoundEnabled, req.params.id] ); res.json({ message: 'User updated successfully.' }); - } catch (error) { - console.error('Update user error:', error); + } catch (error) { console.error('Update user error:', error); res.status(500).json({ error: error.message }); } }); @@ -510,6 +510,30 @@ apiRouter.put('/notifications/read-all', async (req, res) => { } }); +apiRouter.delete('/notifications/:id', async (req, res) => { + try { + await pool.query( + 'DELETE FROM notifications WHERE id = ? AND user_id = ?', + [req.params.id, req.user.id] + ); + res.json({ message: 'Notification deleted' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + +apiRouter.delete('/notifications', async (req, res) => { + try { + await pool.query( + 'DELETE FROM notifications WHERE user_id = ?', + [req.user.id] + ); + res.json({ message: 'All notifications deleted' }); + } catch (error) { + res.status(500).json({ error: error.message }); + } +}); + // --- Global Search --- apiRouter.get('/search', async (req, res) => { const { q } = req.query; @@ -887,6 +911,13 @@ const provisionSuperAdmin = async (retries = 10, delay = 10000) => { console.log('Schema update note (populate slugs):', err.message); } + // Add sound_enabled column if it doesn't exist + try { + await connection.query('ALTER TABLE users ADD COLUMN sound_enabled BOOLEAN DEFAULT true'); + } catch (err) { + if (err.code !== 'ER_DUP_FIELDNAME') console.log('Schema update note (sound_enabled):', err.message); + } + // Update origin enum try { await connection.query("ALTER TABLE attendances MODIFY COLUMN origin ENUM('WhatsApp','Instagram','Website','LinkedIn','Indicação') NOT NULL"); diff --git a/components/Layout.tsx b/components/Layout.tsx index 74305a2..c44185b 100644 --- a/components/Layout.tsx +++ b/components/Layout.tsx @@ -4,7 +4,11 @@ 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 } from '../services/dataService'; +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 }) => ( @@ -40,10 +44,26 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => 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(() => { @@ -389,48 +409,80 @@ export const Layout: React.FC<{ children: React.ReactNode }> = ({ children }) => {showNotifications && (
-
+

Notificações

- {unreadCount > 0 && ( - - )} +
+ {unreadCount > 0 && ( + + )} + {notifications.length > 0 && ( + + )} +
{notifications.length > 0 ? ( notifications.map(n => ( - + + {/* Delete Button */} + +
)) ) : (
diff --git a/pages/UserProfile.tsx b/pages/UserProfile.tsx index 3fbf09f..8ee1878 100644 --- a/pages/UserProfile.tsx +++ b/pages/UserProfile.tsx @@ -1,5 +1,5 @@ import React, { useState, useEffect, useRef } from 'react'; -import { Camera, Save, Mail, User as UserIcon, Building, Shield, Loader2, CheckCircle2 } from 'lucide-react'; +import { Camera, Save, Mail, User as UserIcon, Building, Shield, Loader2, CheckCircle2, Bell } from 'lucide-react'; import { getUserById, getTenants, getTeams, updateUser, uploadAvatar } from '../services/dataService'; import { User, Tenant } from '../types'; @@ -264,6 +264,32 @@ export const UserProfile: React.FC = () => {

{bio.length}/500 caracteres

+
+
+
+

+ Notificações Sonoras +

+

+ Reproduzir um som quando você receber uma nova notificação. +

+
+ +
+
+