11'use client' ;
22
33import { Card , CardContent , CardHeader , CardTitle } from '@comp/ui/card' ;
4+ import { Input } from '@comp/ui/input' ;
5+ import { Search } from 'lucide-react' ;
46import type { CSSProperties } from 'react' ;
57import * as React from 'react' ;
68
@@ -16,6 +18,7 @@ interface EmployeeCompletionChartProps {
1618 trainingVideos : ( EmployeeTrainingVideoCompletion & {
1719 metadata : TrainingVideo ;
1820 } ) [ ] ;
21+ showAll ?: boolean ;
1922}
2023
2124// Define colors for the chart
@@ -42,7 +45,11 @@ export function EmployeeCompletionChart({
4245 employees,
4346 policies,
4447 trainingVideos,
48+ showAll = false ,
4549} : EmployeeCompletionChartProps ) {
50+ const [ searchTerm , setSearchTerm ] = React . useState ( '' ) ;
51+ const [ displayedItems , setDisplayedItems ] = React . useState ( showAll ? 20 : 5 ) ;
52+ const [ isLoading , setIsLoading ] = React . useState ( false ) ;
4653 // Calculate completion data for each employee
4754 const employeeStats : EmployeeTaskStats [ ] = React . useMemo ( ( ) => {
4855 return employees . map ( ( employee ) => {
@@ -97,6 +104,51 @@ export function EmployeeCompletionChart({
97104 } ) ;
98105 } , [ employees , policies , trainingVideos ] ) ;
99106
107+ // Filter employees based on search term
108+ const filteredStats = React . useMemo ( ( ) => {
109+ if ( ! searchTerm ) return employeeStats ;
110+
111+ return employeeStats . filter (
112+ ( stat ) =>
113+ stat . name . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ||
114+ stat . email . toLowerCase ( ) . includes ( searchTerm . toLowerCase ( ) ) ,
115+ ) ;
116+ } , [ employeeStats , searchTerm ] ) ;
117+
118+ // Sort and limit employees
119+ const sortedStats = React . useMemo ( ( ) => {
120+ const sorted = [ ...filteredStats ] . sort ( ( a , b ) => b . overallPercentage - a . overallPercentage ) ;
121+ return showAll ? sorted . slice ( 0 , displayedItems ) : sorted . slice ( 0 , 5 ) ;
122+ } , [ filteredStats , displayedItems , showAll ] ) ;
123+
124+ // Load more function for infinite scroll
125+ const loadMore = React . useCallback ( async ( ) => {
126+ if ( isLoading || ! showAll ) return ;
127+
128+ setIsLoading ( true ) ;
129+ // Simulate loading delay
130+ await new Promise ( ( resolve ) => setTimeout ( resolve , 300 ) ) ;
131+ setDisplayedItems ( ( prev ) => prev + 20 ) ;
132+ setIsLoading ( false ) ;
133+ } , [ isLoading , showAll ] ) ;
134+
135+ // Infinite scroll effect
136+ React . useEffect ( ( ) => {
137+ if ( ! showAll ) return ;
138+
139+ const handleScroll = ( ) => {
140+ if (
141+ window . innerHeight + document . documentElement . scrollTop >=
142+ document . documentElement . offsetHeight - 1000
143+ ) {
144+ loadMore ( ) ;
145+ }
146+ } ;
147+
148+ window . addEventListener ( 'scroll' , handleScroll ) ;
149+ return ( ) => window . removeEventListener ( 'scroll' , handleScroll ) ;
150+ } , [ loadMore , showAll ] ) ;
151+
100152 // Check for empty data scenarios
101153 if ( ! employees . length ) {
102154 return (
@@ -129,42 +181,82 @@ export function EmployeeCompletionChart({
129181 ) ;
130182 }
131183
132- // Sort by completion percentage and limit to top 5
133- const sortedStats = [ ...employeeStats ]
134- . sort ( ( a , b ) => b . overallPercentage - a . overallPercentage )
135- . slice ( 0 , 5 ) ;
136-
137184 return (
138185 < Card >
139186 < CardHeader >
140187 < CardTitle > { 'Employee Task Completion' } </ CardTitle >
188+ { showAll && (
189+ < div className = "mt-4" >
190+ < Input
191+ placeholder = "Search employees..."
192+ value = { searchTerm }
193+ onChange = { ( e ) => setSearchTerm ( e . target . value ) }
194+ leftIcon = { < Search className = "h-4 w-4" /> }
195+ />
196+ </ div >
197+ ) }
141198 </ CardHeader >
142199 < CardContent >
143- < div className = "space-y-8" >
144- { sortedStats . map ( ( stat ) => (
145- < div key = { stat . id } className = "space-y-2" >
146- < div className = "flex items-center justify-between text-sm" >
147- < p > { stat . name } </ p >
148- < span className = "text-muted-foreground" >
149- { stat . policiesCompleted + stat . trainingsCompleted } / { stat . totalTasks } { 'tasks' }
150- </ span >
151- </ div >
200+ { filteredStats . length === 0 ? (
201+ < div className = "flex h-[200px] items-center justify-center" >
202+ < p className = "text-muted-foreground text-center text-sm" >
203+ { searchTerm ? 'No employees found matching your search' : 'No employees available' }
204+ </ p >
205+ </ div >
206+ ) : (
207+ < >
208+ < div className = "space-y-8" >
209+ { sortedStats . map ( ( stat ) => (
210+ < div key = { stat . id } className = "space-y-2" >
211+ < div className = "flex items-center justify-between text-sm" >
212+ < div >
213+ < p className = "font-medium" > { stat . name } </ p >
214+ < p className = "text-muted-foreground text-xs" > { stat . email } </ p >
215+ </ div >
216+ < span className = "text-muted-foreground" >
217+ { stat . policiesCompleted + stat . trainingsCompleted } / { stat . totalTasks } { ' ' }
218+ { 'tasks' }
219+ </ span >
220+ </ div >
152221
153- < TaskBarChart stat = { stat } />
222+ < TaskBarChart stat = { stat } />
154223
155- < div className = "text-muted-foreground flex flex-wrap gap-3 text-xs" >
156- < div className = "flex items-center gap-1" >
157- < div className = "bg-primary size-2" />
158- < span > { 'Completed' } </ span >
159- </ div >
160- < div className = "flex items-center gap-1" >
161- < div className = "size-2 bg-[var(--chart-open)]" />
162- < span > { 'Not Completed' } </ span >
224+ < div className = "text-muted-foreground flex flex-wrap gap-3 text-xs" >
225+ < div className = "flex items-center gap-1" >
226+ < div className = "bg-primary size-2 rounded-xs" />
227+ < span > { 'Completed' } </ span >
228+ </ div >
229+ < div className = "flex items-center gap-1" >
230+ < div className = "size-2 rounded-xs bg-[var(--chart-open)]" />
231+ < span > { 'Not Completed' } </ span >
232+ </ div >
233+ </ div >
163234 </ div >
164- </ div >
235+ ) ) }
165236 </ div >
166- ) ) }
167- </ div >
237+
238+ { showAll && sortedStats . length < filteredStats . length && (
239+ < div className = "mt-8 flex justify-center" >
240+ { isLoading ? (
241+ < div className = "text-muted-foreground text-sm" > Loading more employees...</ div >
242+ ) : (
243+ < button
244+ onClick = { loadMore }
245+ className = "text-primary hover:text-primary/80 text-sm font-medium"
246+ >
247+ Load more employees
248+ </ button >
249+ ) }
250+ </ div >
251+ ) }
252+
253+ { showAll && (
254+ < div className = "mt-4 text-center text-muted-foreground text-xs" >
255+ Showing { sortedStats . length } of { filteredStats . length } employees
256+ </ div >
257+ ) }
258+ </ >
259+ ) }
168260 </ CardContent >
169261 </ Card >
170262 ) ;
0 commit comments