11import { Link , useLocation } from 'react-router-dom'
2- import { Home , LogOut , Moon , Sun , Heart , Shield , BookOpen , Calculator , Users , Menu , X , Calendar , ChevronLeft , ChevronRight , HelpCircle , UserCircle , Zap , TrendingUp , Trash2 , Scale , ChevronDown , Euro } from 'lucide-react'
2+ import { Home , LogOut , Moon , Sun , Heart , Shield , BookOpen , Calculator , Users , Menu , X , Calendar , ChevronLeft , ChevronRight , HelpCircle , UserCircle , Zap , TrendingUp , Trash2 , Scale , ChevronDown , Euro , MessageCircle } from 'lucide-react'
33import { useAuth } from '@/hooks/useAuth'
44import { usePermissions } from '@/hooks/usePermissions'
55import { useThemeStore } from '@/stores/themeStore'
@@ -12,8 +12,9 @@ import PageHeader from './PageHeader'
1212import { PageTransition } from './PageTransition'
1313import { SEO } from './SEO'
1414import { useSEO } from '@/hooks/useSEO'
15- import { useQueryClient } from '@tanstack/react-query'
15+ import { useQuery , useQueryClient } from '@tanstack/react-query'
1616import { adminApi } from '@/api/admin'
17+ import { energyApi } from '@/api/energy'
1718
1819export default function Layout ( { children } : { children : React . ReactNode } ) {
1920 const { user, logout } = useAuth ( )
@@ -32,6 +33,33 @@ export default function Layout({ children }: { children: React.ReactNode }) {
3233 const [ adminMenuOpen , setAdminMenuOpen ] = useState ( false )
3334 const queryClient = useQueryClient ( )
3435
36+ // Fetch unread contributions count for regular users
37+ const { data : unreadContributionsData } = useQuery ( {
38+ queryKey : [ 'unread-contributions-count' ] ,
39+ queryFn : async ( ) => {
40+ const response = await energyApi . getUnreadContributionsCount ( )
41+ return response . data as { unread_count : number }
42+ } ,
43+ enabled : ! ! user ,
44+ refetchInterval : 30000 , // Refresh every 30 seconds
45+ staleTime : 0 ,
46+ } )
47+
48+ // Fetch pending contributions count for admins
49+ const { data : adminStatsData } = useQuery ( {
50+ queryKey : [ 'admin-contribution-stats' ] ,
51+ queryFn : async ( ) => {
52+ const response = await energyApi . getContributionStats ( )
53+ return response . data as { pending_count : number , approved_this_month : number , rejected_count : number , approved_count : number }
54+ } ,
55+ enabled : ! ! user && canAccessAdmin ( ) ,
56+ refetchInterval : 30000 ,
57+ staleTime : 0 ,
58+ } )
59+
60+ const unreadContributionsCount = unreadContributionsData ?. unread_count || 0
61+ const pendingContributionsCount = adminStatsData ?. pending_count || 0
62+
3563 // Persist sidebar state to localStorage
3664 useEffect ( ( ) => {
3765 localStorage . setItem ( 'sidebarCollapsed' , JSON . stringify ( sidebarCollapsed ) )
@@ -53,7 +81,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
5381 { to : '/production' , icon : Sun , label : 'Production' } ,
5482 { to : '/balance' , icon : Scale , label : 'Bilan' } ,
5583 { to : '/simulator' , icon : Calculator , label : 'Simulateur' } ,
56- { to : '/contribute' , icon : Users , label : 'Contribuer' } ,
84+ { to : '/contribute' , icon : Users , label : 'Contribuer' , badgeCount : unreadContributionsCount } ,
5785 { to : '/tempo' , icon : Calendar , label : 'Tempo' } ,
5886 { to : '/ecowatt' , icon : Zap , label : 'EcoWatt' } ,
5987 ]
@@ -70,7 +98,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
7098 { to : '/admin/users' , label : 'Utilisateurs' } ,
7199 { to : '/admin/tempo' , label : 'Tempo' } ,
72100 { to : '/admin/ecowatt' , label : 'EcoWatt' } ,
73- { to : '/admin/contributions' , label : 'Contributions' } ,
101+ { to : '/admin/contributions' , label : 'Contributions' , badgeCount : pendingContributionsCount } ,
74102 { to : '/admin/offers' , label : 'Offres' } ,
75103 { to : '/admin/roles' , label : 'Rôles' } ,
76104 { to : '/admin/logs' , label : 'Logs' } ,
@@ -263,11 +291,12 @@ export default function Layout({ children }: { children: React.ReactNode }) {
263291 { /* Other menu items */ }
264292 { menuItems . filter ( item => item . to !== '/dashboard' ) . map ( ( item ) => {
265293 const isActive = location . pathname === item . to
294+ const hasBadge = item . badgeCount !== undefined && item . badgeCount > 0
266295 return (
267296 < Link
268297 key = { item . to }
269298 to = { item . to }
270- className = { `flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors ${
299+ className = { `flex items-center gap-3 px-3 py-2.5 rounded-md transition-colors relative ${
271300 isActive
272301 ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400'
273302 : 'hover:bg-gray-100 dark:hover:bg-gray-700'
@@ -276,9 +305,23 @@ export default function Layout({ children }: { children: React.ReactNode }) {
276305 data-tour = { item . to === '/simulator' ? 'nav-simulator' :
277306 item . to === '/contribute' ? 'nav-contribute' : undefined }
278307 >
279- < item . icon size = { 20 } className = "flex-shrink-0" />
308+ < div className = "relative" >
309+ < item . icon size = { 20 } className = "flex-shrink-0" />
310+ { hasBadge && sidebarCollapsed && (
311+ < span className = "absolute -top-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-orange-500 text-[10px] font-bold text-white" >
312+ { item . badgeCount > 9 ? '9+' : item . badgeCount }
313+ </ span >
314+ ) }
315+ </ div >
280316 { ! sidebarCollapsed && (
281- < span className = "font-medium" > { item . label } </ span >
317+ < >
318+ < span className = "font-medium" > { item . label } </ span >
319+ { hasBadge && (
320+ < span className = "ml-auto flex h-5 min-w-[20px] items-center justify-center rounded-full bg-orange-500 px-1.5 text-xs font-bold text-white" >
321+ { item . badgeCount > 99 ? '99+' : item . badgeCount }
322+ </ span >
323+ ) }
324+ </ >
282325 ) }
283326 </ Link >
284327 )
@@ -324,13 +367,18 @@ export default function Layout({ children }: { children: React.ReactNode }) {
324367 < Link
325368 key = { subItem . to }
326369 to = { subItem . to }
327- className = { `flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${
370+ className = { `flex items-center justify-between px-3 py-2 rounded-md transition-colors ${
328371 location . pathname === subItem . to
329372 ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400'
330373 : 'hover:bg-gray-100 dark:hover:bg-gray-700'
331374 } `}
332375 >
333376 < span className = "font-medium text-sm" > { subItem . label } </ span >
377+ { subItem . badgeCount && subItem . badgeCount > 0 && (
378+ < span className = "flex h-5 min-w-[20px] items-center justify-center rounded-full bg-orange-500 px-1.5 text-xs font-bold text-white" >
379+ { subItem . badgeCount > 99 ? '99+' : subItem . badgeCount }
380+ </ span >
381+ ) }
334382 </ Link >
335383 ) ) }
336384 </ div >
@@ -538,6 +586,7 @@ export default function Layout({ children }: { children: React.ReactNode }) {
538586 { /* Other menu items */ }
539587 { menuItems . filter ( item => item . to !== '/dashboard' ) . map ( ( item ) => {
540588 const isActive = location . pathname === item . to
589+ const hasBadge = item . badgeCount !== undefined && item . badgeCount > 0
541590 return (
542591 < Link
543592 key = { item . to }
@@ -549,8 +598,17 @@ export default function Layout({ children }: { children: React.ReactNode }) {
549598 : 'hover:bg-gray-100 dark:hover:bg-gray-700'
550599 } `}
551600 >
552- < item . icon size = { 20 } />
601+ { hasBadge ? (
602+ < MessageCircle size = { 20 } className = "text-orange-500" />
603+ ) : (
604+ < item . icon size = { 20 } />
605+ ) }
553606 < span className = "font-medium" > { item . label } </ span >
607+ { hasBadge && (
608+ < span className = "ml-auto flex h-5 min-w-[20px] items-center justify-center rounded-full bg-orange-500 px-1.5 text-xs font-bold text-white" >
609+ { item . badgeCount > 99 ? '99+' : item . badgeCount }
610+ </ span >
611+ ) }
554612 </ Link >
555613 )
556614 } ) }
@@ -593,13 +651,18 @@ export default function Layout({ children }: { children: React.ReactNode }) {
593651 key = { subItem . to }
594652 to = { subItem . to }
595653 onClick = { ( ) => setMobileMenuOpen ( false ) }
596- className = { `flex items-center gap-3 px-3 py-2 rounded-md transition-colors ${
654+ className = { `flex items-center justify-between px-3 py-2 rounded-md transition-colors ${
597655 location . pathname === subItem . to
598656 ? 'bg-primary-100 dark:bg-primary-900/30 text-primary-600 dark:text-primary-400'
599657 : 'hover:bg-gray-100 dark:hover:bg-gray-700'
600658 } `}
601659 >
602660 < span className = "font-medium text-sm" > { subItem . label } </ span >
661+ { subItem . badgeCount && subItem . badgeCount > 0 && (
662+ < span className = "flex h-5 min-w-[20px] items-center justify-center rounded-full bg-orange-500 px-1.5 text-xs font-bold text-white" >
663+ { subItem . badgeCount > 99 ? '99+' : subItem . badgeCount }
664+ </ span >
665+ ) }
603666 </ Link >
604667 ) ) }
605668 </ div >
0 commit comments