@@ -12,6 +12,7 @@ import {
1212 ListItemIcon ,
1313 ListItemText ,
1414 Tooltip ,
15+ Badge ,
1516} from '@mui/material'
1617import {
1718 Home as HomeIcon ,
@@ -31,25 +32,37 @@ import {
3132 Edit as EditIcon ,
3233 CameraAlt as CameraAltIcon ,
3334 Chat as ChatIcon ,
35+ Notifications as NotificationsIcon ,
3436} from '@mui/icons-material'
3537import { useNavigate , useLocation } from 'react-router-dom'
3638import { useState , useEffect } from 'react'
3739import ChatbotButton from '~/components/Chatbot/ChatbotButton'
3840import ChatbotWindow from '../Chatbot/ChatbotWindow'
3941import { useDispatch , useSelector } from 'react-redux'
4042import { logoutUserAPI , selectCurrentUser } from '~/redux/user/userSlice'
43+ import {
44+ fetchNotificationsAPI ,
45+ addNotification ,
46+ setNotificationRead ,
47+ setAllNotificationsRead ,
48+ setUnreadCount ,
49+ selectUnreadCount ,
50+ } from '~/redux/notification/notificationSlice'
4151import { useColorScheme } from '@mui/material/styles'
4252import { COLORS } from '~/theme'
4353import { useChatbot } from '~/context/ChatbotContext'
4454import { getInitials } from '~/utils/formatters'
4555import LogoRounded from '~/assets/Splitly-Rounded.png'
4656import LogoFull from '~/assets/Splitly-Full.png'
57+ import { socketIoInstance } from '~/main'
58+ import { toast } from 'react-toastify'
4759
4860const SIDEBAR_WIDTH_EXPANDED = 256
4961const SIDEBAR_WIDTH_COLLAPSED = 88
5062
5163const Layout = ( { children } ) => {
5264 const currentUser = useSelector ( selectCurrentUser )
65+ const unreadNotificationCount = useSelector ( selectUnreadCount )
5366 const navigate = useNavigate ( )
5467 const location = useLocation ( )
5568 const theme = useTheme ( )
@@ -71,6 +84,61 @@ const Layout = ({ children }) => {
7184 // Use Chatbot Context
7285 const { chatbotWindowOpen, setChatbotWindowOpen, numberOfNotifications, newMessage, setNewMessage } = useChatbot ( )
7386
87+ // Fetch initial notifications and setup socket listeners
88+ useEffect ( ( ) => {
89+ if ( ! currentUser ?. _id ) return
90+
91+ // Fetch initial notifications
92+ dispatch ( fetchNotificationsAPI ( { limit : 10 , offset : 0 } ) )
93+
94+ // Join notification room
95+ socketIoInstance . emit ( 'FE_JOIN_NOTIFICATION_ROOM' , currentUser . _id )
96+
97+ // Listen for new notifications
98+ const handleNewNotification = ( notification ) => {
99+ if ( notification ?. recipientId === currentUser . _id ) {
100+ dispatch ( addNotification ( notification ) )
101+ // Show toast notification
102+ toast . info ( notification . title || 'Bạn có thông báo mới' , { theme : 'colored' } )
103+ }
104+ }
105+
106+ // Listen for notification read (from other devices)
107+ const handleNotificationRead = ( { notificationId, userId } ) => {
108+ if ( userId === currentUser . _id ) {
109+ dispatch ( setNotificationRead ( { notificationId } ) )
110+ }
111+ }
112+
113+ // Listen for all notifications read (from other devices)
114+ const handleAllNotificationsRead = ( { userId } ) => {
115+ if ( userId === currentUser . _id ) {
116+ dispatch ( setAllNotificationsRead ( ) )
117+ }
118+ }
119+
120+ // Listen for unread count update
121+ const handleUnreadCountUpdate = ( { userId, count } ) => {
122+ if ( userId === currentUser . _id ) {
123+ dispatch ( setUnreadCount ( count ) )
124+ }
125+ }
126+
127+ socketIoInstance . on ( 'BE_NEW_NOTIFICATION' , handleNewNotification )
128+ socketIoInstance . on ( 'BE_NOTIFICATION_READ' , handleNotificationRead )
129+ socketIoInstance . on ( 'BE_ALL_NOTIFICATIONS_READ' , handleAllNotificationsRead )
130+ socketIoInstance . on ( 'BE_UNREAD_COUNT_UPDATE' , handleUnreadCountUpdate )
131+
132+ // Cleanup on unmount
133+ return ( ) => {
134+ socketIoInstance . emit ( 'FE_LEAVE_NOTIFICATION_ROOM' , currentUser . _id )
135+ socketIoInstance . off ( 'BE_NEW_NOTIFICATION' , handleNewNotification )
136+ socketIoInstance . off ( 'BE_NOTIFICATION_READ' , handleNotificationRead )
137+ socketIoInstance . off ( 'BE_ALL_NOTIFICATIONS_READ' , handleAllNotificationsRead )
138+ socketIoInstance . off ( 'BE_UNREAD_COUNT_UPDATE' , handleUnreadCountUpdate )
139+ }
140+ } , [ currentUser ?. _id , dispatch ] )
141+
74142 // Check for chatbot payload from navigation state or URL params
75143 useEffect ( ( ) => {
76144 // Check navigation state
@@ -215,7 +283,7 @@ const Layout = ({ children }) => {
215283 display : 'flex' ,
216284 alignItems : 'center' ,
217285 justifyContent : 'center' ,
218- gap : ( isMobile || isExpanded ) ? '8px' : 0 ,
286+ gap : isMobile || isExpanded ? '8px' : 0 ,
219287 px : 2.5 ,
220288 minWidth : 0 ,
221289 transition : 'all 0.4s cubic-bezier(0.4, 0, 0.2, 1)' ,
@@ -229,12 +297,12 @@ const Layout = ({ children }) => {
229297 < Box
230298 component = "span"
231299 sx = { {
232- opacity : ( isMobile || isExpanded ) ? 1 : 0 ,
233- width : ( isMobile || isExpanded ) ? 'auto' : 0 ,
300+ opacity : isMobile || isExpanded ? 1 : 0 ,
301+ width : isMobile || isExpanded ? 'auto' : 0 ,
234302 overflow : 'hidden' ,
235303 whiteSpace : 'nowrap' ,
236304 transition : 'opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.4s cubic-bezier(0.4, 0, 0.2, 1)' ,
237- transitionDelay : ( isMobile || isExpanded ) ? '0.1s' : '0s' ,
305+ transitionDelay : isMobile || isExpanded ? '0.1s' : '0s' ,
238306 } }
239307 >
240308 Tạo hóa đơn mới
@@ -254,7 +322,7 @@ const Layout = ({ children }) => {
254322 display : 'flex' ,
255323 alignItems : 'center' ,
256324 justifyContent : 'flex-start' ,
257- gap : ( isMobile || isExpanded ) ? '12px' : 0 ,
325+ gap : isMobile || isExpanded ? '12px' : 0 ,
258326 borderRadius : '16px' ,
259327 height : '48px' ,
260328 px : 2 ,
@@ -294,12 +362,12 @@ const Layout = ({ children }) => {
294362 < Box
295363 component = "span"
296364 sx = { {
297- opacity : ( isMobile || isExpanded ) ? 1 : 0 ,
298- width : ( isMobile || isExpanded ) ? 'auto' : 0 ,
365+ opacity : isMobile || isExpanded ? 1 : 0 ,
366+ width : isMobile || isExpanded ? 'auto' : 0 ,
299367 overflow : 'hidden' ,
300368 whiteSpace : 'nowrap' ,
301369 transition : 'opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), width 0.4s cubic-bezier(0.4, 0, 0.2, 1)' ,
302- transitionDelay : ( isMobile || isExpanded ) ? '0.1s' : '0s' ,
370+ transitionDelay : isMobile || isExpanded ? '0.1s' : '0s' ,
303371 } }
304372 >
305373 { item . label }
@@ -317,8 +385,8 @@ const Layout = ({ children }) => {
317385 p : 2 ,
318386 display : 'flex' ,
319387 alignItems : 'center' ,
320- gap : ( isMobile || isExpanded ) ? 2 : 0 ,
321- justifyContent : ( isMobile || isExpanded ) ? 'flex-start' : 'center' ,
388+ gap : isMobile || isExpanded ? 2 : 0 ,
389+ justifyContent : isMobile || isExpanded ? 'flex-start' : 'center' ,
322390 cursor : 'pointer' ,
323391 borderRadius : '16px' ,
324392 mx : 2 ,
@@ -732,11 +800,12 @@ const Layout = ({ children }) => {
732800 </IconButton> */ }
733801
734802 { /* Notifications Button - Top Right */ }
735- { /* <IconButton
803+ < IconButton
804+ onClick = { ( ) => navigate ( '/activity' ) }
736805 sx = { {
737806 position : 'fixed' ,
738807 top : 16 ,
739- right: chatbotWindowOpen && !isMobile ? `calc(${chatbotWidth} + 72px )` : 72 ,
808+ right : chatbotWindowOpen && ! isMobile ? `calc(${ chatbotWidth } + 16px )` : 20 ,
740809 zIndex : 1200 ,
741810 width : 40 ,
742811 height : 40 ,
@@ -751,8 +820,22 @@ const Layout = ({ children }) => {
751820 } ,
752821 } }
753822 >
754- <NotificationsIcon fontSize="small" sx={{ color: 'text.primary' }} />
755- </IconButton> */ }
823+ < Badge
824+ badgeContent = { unreadNotificationCount }
825+ color = "error"
826+ max = { 99 }
827+ sx = { {
828+ '& .MuiBadge-badge' : {
829+ fontSize : '10px' ,
830+ minWidth : '18px' ,
831+ height : '18px' ,
832+ background : COLORS . gradientPrimary ,
833+ } ,
834+ } }
835+ >
836+ < NotificationsIcon fontSize = "small" sx = { { color : 'text.primary' } } />
837+ </ Badge >
838+ </ IconButton >
756839
757840 { /* Chatbot Button */ }
758841 < ChatbotButton
0 commit comments