1
+ import React , { useCallback , useContext , useEffect , useState } from 'react'
2
+ import { useNavigate } from 'react-router-dom'
3
+ import { CommandDialog , CommandEmpty , CommandGroup , CommandInput , CommandItem , CommandList , } from '@/components/ui/command'
4
+ import { Search , TreePalm } from 'lucide-react'
5
+ import { Button } from './ui/button'
6
+ import { getUsers } from '@/core/services/userService'
7
+ import { getLeavesPolicies } from '@/core/services/leaveService'
8
+ import { UserResponse } from '@/core/types/user'
9
+ import { LeavePolicyResponse } from '@/core/types/leave'
10
+ import UserAvatar from "@/modules/user/components/UserAvatar.tsx" ;
11
+ import { UserContext } from "@/contexts/UserContext.tsx" ;
12
+ import { getAccessibleNavigationItems } from "@/core/utils/navigation.ts" ;
13
+ import { UserRole } from "@/core/types/enum.ts" ;
14
+
15
+ export function GlobalSearch ( ) {
16
+ const [ open , setOpen ] = useState ( false )
17
+ const [ loading , setLoading ] = useState ( false )
18
+ const [ users , setUsers ] = useState < UserResponse [ ] > ( [ ] )
19
+ const [ policies , setPolicies ] = useState < LeavePolicyResponse [ ] > ( [ ] )
20
+ const [ searchTerm , setSearchTerm ] = useState ( '' )
21
+ const navigate = useNavigate ( ) ;
22
+ const { user} = useContext ( UserContext ) ;
23
+ const accessibleItems = user ? getAccessibleNavigationItems ( user . role ) : [ ] ;
24
+
25
+ const fetchData = useCallback ( async ( ) => {
26
+ try {
27
+ setLoading ( true )
28
+ const [ usersData , policiesData ] = await Promise . all ( [
29
+ getUsers ( 0 , 100 ) ,
30
+ getLeavesPolicies ( )
31
+ ] )
32
+ setUsers ( usersData . contents )
33
+ setPolicies ( policiesData )
34
+ } catch ( error ) {
35
+ console . error ( 'Error fetching data:' , error )
36
+ } finally {
37
+ setLoading ( false )
38
+ }
39
+ } , [ ] )
40
+
41
+ useEffect ( ( ) => {
42
+ if ( open ) {
43
+ fetchData ( )
44
+ }
45
+ } , [ open , fetchData ] )
46
+
47
+ const onSelect = ( path : string ) => {
48
+ setOpen ( false )
49
+ navigate ( path )
50
+ }
51
+
52
+ const filteredUsers = users . filter ( user => {
53
+ if ( ! searchTerm ) return true ;
54
+ const searchLower = searchTerm . toLowerCase ( ) ;
55
+ return (
56
+ `${ user . firstName } ${ user . lastName } ` . toLowerCase ( ) . includes ( searchLower ) ||
57
+ user . email . toLowerCase ( ) . includes ( searchLower )
58
+ ) ;
59
+ } ) ;
60
+
61
+ const filteredPolicies = policies . filter ( policy => {
62
+ if ( ! searchTerm ) return true ;
63
+ const searchLower = searchTerm . toLowerCase ( ) ;
64
+ return policy . name . toLowerCase ( ) . includes ( searchLower ) ;
65
+ } ) ;
66
+
67
+ const visibleUsers = searchTerm ? filteredUsers : filteredUsers . slice ( 0 , 10 ) ;
68
+ const visiblePolicies = searchTerm ? filteredPolicies : filteredPolicies . slice ( 0 , 10 ) ;
69
+ const visiblePages = searchTerm ? accessibleItems : accessibleItems . slice ( 0 , 5 ) ;
70
+
71
+ return (
72
+ < >
73
+ < Button variant = "outline" className = 'px-2 h-9' onClick = { ( ) => setOpen ( true ) } >
74
+ < Search className = "h-4 w-4 mr-1" />
75
+ Search
76
+ </ Button >
77
+ < CommandDialog open = { open } onOpenChange = { setOpen } >
78
+ < CommandInput placeholder = "Type to search..." value = { searchTerm } onValueChange = { setSearchTerm } />
79
+ < CommandList className = "max-h-[80vh] overflow-auto" >
80
+ < CommandEmpty > No results found.</ CommandEmpty >
81
+ { loading ? (
82
+ < div className = "space-y-1 overflow-hidden px-1 py-2" >
83
+ < div className = "animate-pulse space-y-2" >
84
+ { [ ...Array ( 5 ) ] . map ( ( _ , i ) => (
85
+ < div key = { i } className = "h-4 w-full rounded bg-muted" />
86
+ ) ) }
87
+ </ div >
88
+ </ div >
89
+ ) : (
90
+ < >
91
+ < CommandGroup heading = "Pages" className = "p-2" >
92
+ { visiblePages . map ( ( page ) => (
93
+ < CommandItem
94
+ key = { page . path }
95
+ onSelect = { ( ) => onSelect ( page . path ) }
96
+ className = "flex items-center gap-2 p-2 hove:bg-indigo-50 rounded-lg"
97
+ >
98
+ < page . icon className = 'h-4 w-4 text-gray-500' />
99
+ < div className = "flex flex-col" >
100
+ < span className = "font-medium" > { page . title } </ span >
101
+ < span className = "text-gray-500 text-sm" > { page . description } </ span >
102
+ </ div >
103
+ </ CommandItem >
104
+ ) ) }
105
+ </ CommandGroup >
106
+
107
+ { ( user . role === UserRole . ORGANIZATION_ADMIN || user . role === UserRole . TEAM_ADMIN ) && (
108
+ < >
109
+ < CommandGroup heading = "Users" className = "p-2" >
110
+ < div className = "grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[300px] overflow-auto" >
111
+ { visibleUsers . map ( ( user ) => (
112
+ < CommandItem
113
+ key = { user . id }
114
+ onSelect = { ( ) => onSelect ( `/users/${ user . id } ` ) }
115
+ className = "p-3 cursor-pointer hover:bg-indigo-50 rounded-lg"
116
+ >
117
+ < div className = "flex items-center gap-2" >
118
+ < UserAvatar size = { 32 } user = { user } />
119
+ < div className = "flex flex-col" >
120
+ < span className = "font-medium" > { user . firstName } { user . lastName } </ span >
121
+ < span className = "text-xs text-muted-foreground" > { user . email } </ span >
122
+ </ div >
123
+ </ div >
124
+ </ CommandItem >
125
+ ) ) }
126
+ </ div >
127
+ </ CommandGroup >
128
+
129
+ < CommandGroup heading = "Leave Policies" className = "p-2" >
130
+ < div className = "grid grid-cols-1 sm:grid-cols-2 gap-2 max-h-[300px] overflow-auto" >
131
+ { visiblePolicies . map ( ( policy ) => (
132
+ < CommandItem
133
+ key = { policy . id }
134
+ onSelect = { ( ) => onSelect ( `/leaves/policies/${ policy . id } ` ) }
135
+ className = "flex items-center gap-2 p-2 hover:bg-indigo-50 rounded-lg"
136
+ >
137
+ < TreePalm className = 'h-4 w-4 text-gray-500' />
138
+ < span className = "font-medium" > { policy . name } </ span >
139
+ </ CommandItem >
140
+ ) ) }
141
+ </ div >
142
+ </ CommandGroup >
143
+ </ >
144
+ ) }
145
+ </ >
146
+ ) }
147
+ </ CommandList >
148
+ </ CommandDialog >
149
+ </ >
150
+ )
151
+ }
0 commit comments