1- import { useState , useMemo } from 'react' ;
1+ import { useState , useMemo , useEffect } from 'react' ;
2+ import { useSelector } from 'react-redux' ;
23import { v4 as uuidv4 } from 'uuid' ;
3- import getApplicationData from './api' ;
4+ import { ENDPOINTS } from '../../utils/URL' ;
5+ import httpService from '../../services/httpService' ;
6+ import { getAggregatedMockForChart } from './api' ;
47import styles from './ApplicationTimeChart.module.css' ;
58
9+ function uniqueRolesFromRows ( rows ) {
10+ return [ ...new Set ( ( rows || [ ] ) . map ( r => r ?. role ) . filter ( Boolean ) ) ] . sort ( ( a , b ) =>
11+ a . localeCompare ( b ) ,
12+ ) ;
13+ }
14+
15+ function mergeRoleOptions ( prev , rows ) {
16+ const fromData = uniqueRolesFromRows ( rows ) ;
17+ const fromPrev = prev . filter ( r => r !== 'all' ) ;
18+ const combined = new Set ( [ ...fromPrev , ...fromData ] ) ;
19+ return [ 'all' , ...Array . from ( combined ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ] ;
20+ }
21+
622function ApplicationTimeChart ( ) {
723 const [ dateFilter , setDateFilter ] = useState ( 'all' ) ;
824 const [ selectedRole , setSelectedRole ] = useState ( 'all' ) ;
25+ const [ data , setData ] = useState ( [ ] ) ;
26+ const [ availableRoles , setAvailableRoles ] = useState ( [ 'all' ] ) ;
27+ const [ loading , setLoading ] = useState ( true ) ;
28+ const [ error , setError ] = useState ( null ) ;
929
10- const rawData = getApplicationData ( ) ;
30+ // Get dark mode state from Redux
31+ const darkMode = useSelector ( state => state . theme ?. darkMode || false ) ;
1132
12- const processedData = useMemo ( ( ) => {
13- let filtered = [ ...rawData ] ;
14-
15- filtered = filtered . filter ( item => item . timeToApply <= 30 ) ;
16-
17- if ( dateFilter !== 'all' ) {
18- const now = new Date ( ) ;
19- filtered = filtered . filter ( item => {
20- const itemDate = new Date ( item . timestamp ) ;
21- const daysAgo = Math . floor ( ( now . getTime ( ) - itemDate . getTime ( ) ) / ( 1000 * 60 * 60 * 24 ) ) ;
22-
23- switch ( dateFilter ) {
24- case 'weekly' :
25- return daysAgo <= 7 ;
26- case 'monthly' :
27- return daysAgo <= 30 ;
28- case 'yearly' :
29- return daysAgo <= 365 ;
30- default :
31- return true ;
33+ // Fetch available roles from backend
34+ useEffect ( ( ) => {
35+ const fetchRoles = async ( ) => {
36+ try {
37+ // Construct roles endpoint URL: /api/analytics/application-time/roles
38+ const baseUrl = ENDPOINTS . APPLICATION_TIME_DATA ( '' , '' , [ ] ) ;
39+ const rolesUrl = baseUrl . split ( '?' ) [ 0 ] + '/roles' ;
40+ const response = await httpService . get ( rolesUrl ) ;
41+ if ( response . data && response . data . data && Array . isArray ( response . data . data ) ) {
42+ const apiRoles = response . data . data . filter ( Boolean ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
43+ setAvailableRoles ( [ 'all' , ...apiRoles ] ) ;
44+ } else if ( response . data && response . data . success && Array . isArray ( response . data . data ) ) {
45+ const apiRoles = response . data . data . filter ( Boolean ) . sort ( ( a , b ) => a . localeCompare ( b ) ) ;
46+ setAvailableRoles ( [ 'all' , ...apiRoles ] ) ;
47+ } else {
48+ setAvailableRoles ( mergeRoleOptions ( [ 'all' ] , getAggregatedMockForChart ( ) ) ) ;
3249 }
33- } ) ;
50+ } catch ( err ) {
51+ console . error ( 'Error fetching available roles:' , err ) ;
52+ setAvailableRoles ( mergeRoleOptions ( [ 'all' ] , getAggregatedMockForChart ( ) ) ) ;
53+ }
54+ } ;
55+
56+ fetchRoles ( ) ;
57+ } , [ ] ) ;
58+
59+ // Fetch data from backend
60+ useEffect ( ( ) => {
61+ const fetchData = async ( ) => {
62+ try {
63+ setLoading ( true ) ;
64+ setError ( null ) ;
65+
66+ // Prepare query parameters
67+ let startDate = null ;
68+ let endDate = null ;
69+
70+ if ( dateFilter !== 'all' ) {
71+ const now = new Date ( ) ;
72+
73+ switch ( dateFilter ) {
74+ case 'weekly' :
75+ startDate = new Date ( now . getTime ( ) - 7 * 24 * 60 * 60 * 1000 ) ;
76+ break ;
77+ case 'monthly' :
78+ startDate = new Date ( now . getTime ( ) - 30 * 24 * 60 * 60 * 1000 ) ;
79+ break ;
80+ case 'yearly' :
81+ startDate = new Date ( now . getTime ( ) - 365 * 24 * 60 * 60 * 1000 ) ;
82+ break ;
83+ default :
84+ break ;
85+ }
86+
87+ if ( startDate ) {
88+ endDate = now ;
89+ }
90+ }
91+
92+ const url = ENDPOINTS . APPLICATION_TIME_DATA (
93+ startDate ? startDate . toISOString ( ) : null ,
94+ endDate ? endDate . toISOString ( ) : null ,
95+ selectedRole !== 'all' ? [ selectedRole ] : [ ] ,
96+ ) ;
97+
98+ const response = await httpService . get ( url ) ;
99+
100+ // Backend returns { data: [], message: "", summary: {} }
101+ if ( response . data && response . data . data && Array . isArray ( response . data . data ) ) {
102+ const rows = response . data . data ;
103+ setData ( rows ) ;
104+ setAvailableRoles ( prev => mergeRoleOptions ( prev , rows ) ) ;
105+ } else if ( response . data && Array . isArray ( response . data ) ) {
106+ const rows = response . data ;
107+ setData ( rows ) ;
108+ setAvailableRoles ( prev => mergeRoleOptions ( prev , rows ) ) ;
109+ } else {
110+ console . error ( 'Backend returned unexpected data format:' , response . data ) ;
111+ setError ( 'Unexpected data format from server' ) ;
112+ setData ( [ ] ) ;
113+ }
114+ } catch ( err ) {
115+ console . error ( 'Error fetching application time data:' , err ) ;
116+ const status = err ?. response ?. status ;
117+ if ( status === 404 ) {
118+ const rows = getAggregatedMockForChart ( ) ;
119+ setData ( rows ) ;
120+ setAvailableRoles ( prev => mergeRoleOptions ( prev , rows ) ) ;
121+ setError ( null ) ;
122+ } else {
123+ setError ( err . message || 'Failed to fetch data from server' ) ;
124+ setData ( [ ] ) ;
125+ }
126+ } finally {
127+ setLoading ( false ) ;
128+ }
129+ } ;
130+
131+ fetchData ( ) ;
132+ } , [ dateFilter , selectedRole ] ) ;
133+
134+ const processedData = useMemo ( ( ) => {
135+ // Backend may return all roles even when `roles` query is set; mock data is always full set.
136+ // Apply Role filter here so the chart always matches the dropdown.
137+ if ( ! Array . isArray ( data ) || data . length === 0 ) {
138+ return [ ] ;
34139 }
35140
141+ let rows = data ;
36142 if ( selectedRole !== 'all' ) {
37- filtered = filtered . filter ( item => item . role === selectedRole ) ;
143+ rows = data . filter ( item => item && item . role === selectedRole ) ;
38144 }
39145
40- const roleGroups = { } ;
41- filtered . forEach ( item => {
42- if ( ! roleGroups [ item . role ] ) roleGroups [ item . role ] = [ ] ;
43- roleGroups [ item . role ] . push ( item . timeToApply ) ;
44- } ) ;
45-
46- const chartData = Object . entries ( roleGroups )
47- . map ( ( [ role , times ] ) => {
48- const avg = times . reduce ( ( a , b ) => a + b , 0 ) / times . length ;
49- return {
50- role,
51- avgTime : Math . round ( avg * 10 ) / 10 ,
52- count : times . length ,
53- } ;
54- } )
55- . sort ( ( a , b ) => b . avgTime - a . avgTime ) ;
146+ // Map backend response to chart data format
147+ // Backend returns timeToApplyMinutes (average time in minutes)
148+ const chartData = rows
149+ . map ( item => ( {
150+ role : item . role ,
151+ avgTime : item . timeToApplyMinutes || ( item . timeToApply ? item . timeToApply / 60 : 0 ) ,
152+ count : item . totalApplications || 1 ,
153+ formattedTime :
154+ item . timeToApplyFormatted ||
155+ `${ Math . round ( ( item . timeToApplyMinutes || 0 ) * 10 ) / 10 } min` ,
156+ } ) )
157+ . sort ( ( a , b ) => b . avgTime - a . avgTime ) ; // Sort highest to lowest (most time-consuming first)
56158
57159 return chartData ;
58- } , [ rawData , dateFilter , selectedRole ] ) ;
59-
60- const roles = useMemo ( ( ) => {
61- const uniqueRoles = [ ...new Set ( rawData . map ( item => item . role ) ) ] ;
62- return [ 'all' , ...uniqueRoles ] ;
63- } , [ rawData ] ) ;
160+ } , [ data , selectedRole ] ) ;
64161
65162 const maxTime = Math . max ( ...processedData . map ( item => item . avgTime ) , 10 ) ;
66163
164+ // Show loading state
165+ if ( loading ) {
166+ return (
167+ < div className = { `${ styles . container } ${ darkMode ? styles . darkMode : '' } ` } >
168+ < div className = { `${ styles . chartCard } ${ darkMode ? styles . darkMode : '' } ` } >
169+ < h2 className = { `${ styles . title } ${ darkMode ? styles . darkMode : '' } ` } >
170+ Comparing the Average Time Taken to Fill an Application by Role
171+ </ h2 >
172+ < div className = { `${ styles . noData } ${ darkMode ? styles . darkMode : '' } ` } >
173+ Loading application time data...
174+ </ div >
175+ </ div >
176+ </ div >
177+ ) ;
178+ }
179+
180+ // Show error state
181+ if ( error ) {
182+ return (
183+ < div className = { `${ styles . container } ${ darkMode ? styles . darkMode : '' } ` } >
184+ < div className = { `${ styles . chartCard } ${ darkMode ? styles . darkMode : '' } ` } >
185+ < h2 className = { `${ styles . title } ${ darkMode ? styles . darkMode : '' } ` } >
186+ Comparing the Average Time Taken to Fill an Application by Role
187+ </ h2 >
188+ < div className = { `${ styles . noData } ${ darkMode ? styles . darkMode : '' } ` } >
189+ Error loading data: { error } . Please try again later.
190+ </ div >
191+ </ div >
192+ </ div >
193+ ) ;
194+ }
195+
67196 return (
68- < div className = { styles . container } >
197+ < div className = { ` ${ styles . container } ${ darkMode ? styles . darkMode : '' } ` } >
69198 { /* Chart Container */ }
70- < div className = { styles . chartCard } >
71- < h2 className = { styles . title } >
199+ < div className = { ` ${ styles . chartCard } ${ darkMode ? styles . darkMode : '' } ` } >
200+ < h2 className = { ` ${ styles . title } ${ darkMode ? styles . darkMode : '' } ` } >
72201 Comparing the Average Time Taken to Fill an Application by Role
73202 </ h2 >
74203
@@ -78,7 +207,7 @@ function ApplicationTimeChart() {
78207 < >
79208 { /* Grid Lines */ }
80209 < div
81- className = { styles . grid }
210+ className = { ` ${ styles . grid } ${ darkMode ? styles . darkMode : '' } ` }
82211 style = { {
83212 backgroundSize : `${ 100 / 6 } % ${ 100 / processedData . length } %` ,
84213 } }
@@ -89,7 +218,7 @@ function ApplicationTimeChart() {
89218 { processedData . map ( item => (
90219 < div
91220 key = { uuidv4 ( ) }
92- className = { styles . yAxisItem }
221+ className = { ` ${ styles . yAxisItem } ${ darkMode ? styles . darkMode : '' } ` }
93222 style = { { height : `${ 100 / processedData . length } %` } }
94223 >
95224 { item . role }
@@ -98,21 +227,31 @@ function ApplicationTimeChart() {
98227 </ div >
99228
100229 { /* X-axis */ }
101- < div className = { styles . xAxis } >
102- { [ 0 , 5 , 10 , 15 , 20 , 25 , 30 ] . map ( tick => (
103- < div
104- key = { tick }
105- style = { {
106- position : 'absolute' ,
107- left : `${ ( tick / maxTime ) * 100 } %` ,
108- fontSize : '12px' ,
109- color : '#5f6368' ,
110- transform : 'translateX(-50%)' ,
111- } }
112- >
113- { tick <= maxTime ? tick : '' }
114- </ div >
115- ) ) }
230+ < div className = { `${ styles . xAxis } ${ darkMode ? styles . darkMode : '' } ` } >
231+ { ( ( ) => {
232+ // Generate dynamic ticks based on maxTime
233+ const tickCount = 6 ;
234+ const ticks = [ ] ;
235+ for ( let i = 0 ; i <= tickCount ; i ++ ) {
236+ const tickValue = Math . round ( ( ( maxTime * i ) / tickCount ) * 10 ) / 10 ;
237+ ticks . push ( tickValue ) ;
238+ }
239+ return ticks . map ( tick => (
240+ < div
241+ key = { tick }
242+ className = { darkMode ? styles . darkMode : '' }
243+ style = { {
244+ position : 'absolute' ,
245+ left : `${ ( tick / maxTime ) * 100 } %` ,
246+ fontSize : '12px' ,
247+ color : darkMode ? '#e0e0e0' : '#5f6368' ,
248+ transform : 'translateX(-50%)' ,
249+ } }
250+ >
251+ { tick }
252+ </ div >
253+ ) ) ;
254+ } ) ( ) }
116255 </ div >
117256
118257 { /* Bars */ }
@@ -124,28 +263,32 @@ function ApplicationTimeChart() {
124263 style = { { height : `${ 100 / processedData . length } %` } }
125264 >
126265 < div
127- className = { styles . bar }
266+ className = { ` ${ styles . bar } ${ darkMode ? styles . darkMode : '' } ` }
128267 style = { { width : `${ ( item . avgTime / maxTime ) * 100 } %` } }
129268 >
130- < div className = { styles . dataLabel } > { item . avgTime } min</ div >
269+ < div className = { `${ styles . dataLabel } ${ darkMode ? styles . darkMode : '' } ` } >
270+ { item . formattedTime || `${ Math . round ( item . avgTime * 10 ) / 10 } min` }
271+ </ div >
131272 </ div >
132273 </ div >
133274 ) ) }
134275 </ div >
135276
136277 { /* X-axis Label */ }
137- < div className = { styles . xAxisLabel } >
278+ < div className = { ` ${ styles . xAxisLabel } ${ darkMode ? styles . darkMode : '' } ` } >
138279 Average Time taken to fill application (in minutes)
139280 </ div >
140281 </ >
141282 ) : (
142- < div className = { styles . noData } > No data available for the selected filters</ div >
283+ < div className = { `${ styles . noData } ${ darkMode ? styles . darkMode : '' } ` } >
284+ No data available for the selected filters
285+ </ div >
143286 ) }
144287 </ div >
145288
146289 { /* Summary Info */ }
147290 { processedData . length > 0 && (
148- < div className = { styles . summary } >
291+ < div className = { ` ${ styles . summary } ${ darkMode ? styles . darkMode : '' } ` } >
149292 < div >
150293 < strong > Showing:</ strong > { processedData . length } role(s)
151294 </ div >
@@ -163,12 +306,12 @@ function ApplicationTimeChart() {
163306 { /* Filters Panel */ }
164307 < div className = { styles . filters } >
165308 { /* Dates Filter */ }
166- < div className = { styles . filterCard } >
167- < div className = { styles . filterTitle } > Dates</ div >
309+ < div className = { ` ${ styles . filterCard } ${ darkMode ? styles . darkMode : '' } ` } >
310+ < div className = { ` ${ styles . filterTitle } ${ darkMode ? styles . darkMode : '' } ` } > Dates</ div >
168311 < select
169312 value = { dateFilter }
170313 onChange = { e => setDateFilter ( e . target . value ) }
171- className = { styles . select }
314+ className = { ` ${ styles . select } ${ darkMode ? styles . darkMode : '' } ` }
172315 >
173316 < option value = "all" > ALL</ option >
174317 < option value = "weekly" > Last 7 Days</ option >
@@ -178,14 +321,14 @@ function ApplicationTimeChart() {
178321 </ div >
179322
180323 { /* Role Filter */ }
181- < div className = { styles . filterCard } >
182- < div className = { styles . filterTitle } > Role</ div >
324+ < div className = { ` ${ styles . filterCard } ${ darkMode ? styles . darkMode : '' } ` } >
325+ < div className = { ` ${ styles . filterTitle } ${ darkMode ? styles . darkMode : '' } ` } > Role</ div >
183326 < select
184327 value = { selectedRole }
185328 onChange = { e => setSelectedRole ( e . target . value ) }
186- className = { styles . select }
329+ className = { ` ${ styles . select } ${ darkMode ? styles . darkMode : '' } ` }
187330 >
188- { roles . map ( role => (
331+ { availableRoles . map ( role => (
189332 < option key = { role } value = { role } >
190333 { role === 'all' ? 'ALL' : role }
191334 </ option >
0 commit comments