@@ -9,77 +9,61 @@ import 'react-datepicker/dist/react-datepicker.css';
99
1010function ApplicantVolunteerRatio ( ) {
1111 const darkMode = useSelector ( state => state . theme . darkMode ) ;
12+
1213 const [ data , setData ] = useState ( [ ] ) ;
13- const [ allRoles , setAllRoles ] = useState ( [ ] ) ; // Store all available roles
14+ const [ allRoles , setAllRoles ] = useState ( [ ] ) ;
1415 const [ loading , setLoading ] = useState ( true ) ;
1516 const [ error , setError ] = useState ( null ) ;
1617 const [ selectedRoles , setSelectedRoles ] = useState ( [ ] ) ;
1718 const [ startDate , setStartDate ] = useState ( null ) ;
1819 const [ endDate , setEndDate ] = useState ( null ) ;
1920 const [ validationError , setValidationError ] = useState ( '' ) ;
21+ const [ viewMode , setViewMode ] = useState ( 'count' ) ;
2022
21- // Fetch all available roles (without filtering)
23+ // Fetch all available roles
2224 useEffect ( ( ) => {
2325 const fetchAllRoles = async ( ) => {
2426 try {
2527 const response = await getAllApplicantVolunteerRatios ( { } ) ;
26- const apiData = response . data ;
27-
28- // Get all unique roles
28+ const apiData = response ?. data ?? [ ] ;
2929 const uniqueRoles = [ ...new Set ( apiData . map ( item => item . role ) ) ] ;
3030 const roleOptions = uniqueRoles . map ( role => ( { label : role , value : role } ) ) ;
31-
3231 setAllRoles ( roleOptions ) ;
33-
34- // Set all roles as selected by default
3532 setSelectedRoles ( roleOptions ) ;
3633 } catch ( err ) {
37- // Error fetching all roles
3834 setError ( 'Failed to load roles. Please try again.' ) ;
3935 }
4036 } ;
41-
4237 fetchAllRoles ( ) ;
4338 } , [ ] ) ;
4439
45- // Fetch filtered data based on selected roles and date range
40+ // Fetch filtered data
4641 useEffect ( ( ) => {
4742 const fetchFilteredData = async ( ) => {
48- // Validate date range: start must be before or equal to end
4943 if ( startDate && endDate && startDate > endDate ) {
5044 setValidationError ( 'Start date must be earlier than or equal to End date.' ) ;
5145 setData ( [ ] ) ;
5246 setLoading ( false ) ;
5347 return ;
5448 }
5549
56- // clear previous validation error when dates are valid
5750 if ( validationError ) setValidationError ( '' ) ;
5851
5952 if ( selectedRoles . length === 0 ) {
6053 setData ( [ ] ) ;
6154 return ;
6255 }
63-
6456 try {
6557 setLoading ( true ) ;
66-
67- // Prepare filters
6858 const filters = { } ;
69- if ( startDate ) {
70- filters . startDate = startDate . toISOString ( ) . split ( 'T' ) [ 0 ] ; // Format as YYYY-MM-DD
71- }
72- if ( endDate ) {
73- filters . endDate = endDate . toISOString ( ) . split ( 'T' ) [ 0 ] ; // Format as YYYY-MM-DD
74- }
59+ if ( startDate ) filters . startDate = startDate . toISOString ( ) . split ( 'T' ) [ 0 ] ;
60+ if ( endDate ) filters . endDate = endDate . toISOString ( ) . split ( 'T' ) [ 0 ] ;
7561 if ( selectedRoles . length > 0 ) {
7662 filters . roles = selectedRoles . map ( role => role . value ) . join ( ',' ) ;
7763 }
7864
7965 const response = await getAllApplicantVolunteerRatios ( filters ) ;
80- const apiData = response . data ;
81-
82- // Transform API data to match chart format
66+ const apiData = response ?. data ?? [ ] ;
8367 const transformedData = apiData . map ( item => ( {
8468 role : item . role ,
8569 applicants : item . totalApplicants ,
@@ -88,23 +72,34 @@ function ApplicantVolunteerRatio() {
8872
8973 setData ( transformedData ) ;
9074 } catch ( err ) {
91- // Error fetching applicant volunteer ratio data
9275 setError ( 'Failed to load data. Please try again.' ) ;
9376 } finally {
9477 setLoading ( false ) ;
9578 }
9679 } ;
97-
9880 fetchFilteredData ( ) ;
99- } , [ startDate , endDate , selectedRoles ] ) ; // Re-fetch when date range or selected roles change
81+ } , [ startDate , endDate , selectedRoles ] ) ;
10082
101- // Filter and transform data for chart
102- const chartData = useMemo (
103- ( ) => data . filter ( d => selectedRoles . map ( r => r . value ) . includes ( d . role ) ) ,
104- [ data , selectedRoles ] ,
105- ) ;
83+ // Prepare chart data
84+ const chartData = useMemo ( ( ) => {
85+ const filtered = data . filter ( d => selectedRoles . map ( r => r . value ) . includes ( d . role ) ) ;
86+
87+ if ( viewMode === 'percentage' ) {
88+ return filtered . map ( item => {
89+ const percentage =
90+ item . applicants > 0 ? Number ( ( ( item . hired / item . applicants ) * 100 ) . toFixed ( 1 ) ) : 0 ;
91+
92+ return {
93+ ...item ,
94+ hiredPercentage : percentage ,
95+ } ;
96+ } ) ;
97+ }
10698
107- // Apply dark mode to document body
99+ return filtered ;
100+ } , [ data , selectedRoles , viewMode ] ) ;
101+
102+ // Apply dark mode
108103 useEffect ( ( ) => {
109104 if ( darkMode ) {
110105 document . body . classList . add ( 'dark-mode-body' ) ;
@@ -138,10 +133,12 @@ function ApplicantVolunteerRatio() {
138133 return (
139134 < div className = { `${ styles . page } ${ darkMode ? styles . dark : '' } ` } >
140135 < h2 className = { styles . heading } > Number of People Hired vs. Total Applications</ h2 >
136+
137+ { /* Filters */ }
141138 < div className = { styles . filters } >
142139 < div className = { styles . filterGroup } >
143140 < label htmlFor = "start-date" className = { styles . label } >
144- Date Range:{ ' ' }
141+ Date Range:
145142 </ label >
146143 < div className = { styles . dateInputWrapper } >
147144 < DatePicker
@@ -173,9 +170,10 @@ function ApplicantVolunteerRatio() {
173170 </ div >
174171 ) }
175172 </ div >
173+
176174 < div className = { styles . filterGroupInline } >
177175 < label htmlFor = "role-select" className = { styles . label } >
178- Role:{ ' ' }
176+ Role:
179177 </ label >
180178 < Select
181179 id = "role-select"
@@ -189,39 +187,119 @@ function ApplicantVolunteerRatio() {
189187 />
190188 </ div >
191189 </ div >
190+
191+ { /* Toggle Buttons */ }
192+ < div style = { { marginBottom : '12px' , display : 'flex' , gap : '8px' } } >
193+ < button
194+ type = "button"
195+ onClick = { ( ) => setViewMode ( 'count' ) }
196+ style = { {
197+ padding : '6px 12px' ,
198+ cursor : 'pointer' ,
199+ backgroundColor : viewMode === 'count' ? '#1976d2' : '#e0e0e0' ,
200+ color : viewMode === 'count' ? '#fff' : '#000' ,
201+ border : 'none' ,
202+ borderRadius : '4px' ,
203+ } }
204+ >
205+ Count View
206+ </ button >
207+
208+ < button
209+ type = "button"
210+ onClick = { ( ) => setViewMode ( 'percentage' ) }
211+ style = { {
212+ padding : '6px 12px' ,
213+ cursor : 'pointer' ,
214+ backgroundColor : viewMode === 'percentage' ? '#1976d2' : '#e0e0e0' ,
215+ color : viewMode === 'percentage' ? '#fff' : '#000' ,
216+ border : 'none' ,
217+ borderRadius : '4px' ,
218+ } }
219+ >
220+ Percentage View
221+ </ button >
222+ </ div >
223+
192224 { chartData . length > 0 ? (
193225 < div className = { styles . chartContainer } >
194- < ResponsiveContainer width = "100%" height = { 400 } >
226+ < ResponsiveContainer width = "100%" height = { 350 } >
195227 < BarChart
196228 data = { chartData }
197229 layout = "vertical"
198230 margin = { { top : 20 , right : 40 , left : 80 , bottom : 20 } }
199231 barCategoryGap = { 24 }
232+ barSize = { 16 }
200233 >
201234 < XAxis
202235 type = "number"
203- label = { {
204- value : 'Percentage of People Hired vs. Total Applications' ,
205- position : 'insideBottom' ,
206- offset : - 5 ,
207- } }
208- allowDecimals = { false }
236+ domain = { viewMode === 'percentage' ? [ 0 , 100 ] : [ 'auto' , 'auto' ] }
237+ allowDecimals = { viewMode === 'percentage' }
209238 />
239+
210240 < YAxis
211241 dataKey = "role"
212242 type = "category"
213243 width = { 180 }
214- label = { { value : 'Name of Role' , angle : - 90 , position : 'insideLeft' } }
244+ label = { { value : 'Role' , angle : - 90 , position : 'insideLeft' } }
215245 />
246+
216247 < Tooltip />
217- < Bar dataKey = "applicants" fill = "#1976d2" name = "Total Applicants" >
218- < LabelList dataKey = "applicants" position = "right" />
219- </ Bar >
220- < Bar dataKey = "hired" fill = "#43a047" name = "Total Hired" >
221- < LabelList dataKey = "hired" position = "right" />
222- </ Bar >
248+
249+ { viewMode === 'count' ? (
250+ < >
251+ < Bar dataKey = "applicants" fill = "#1976d2" >
252+ < LabelList dataKey = "applicants" position = "right" />
253+ </ Bar >
254+
255+ < Bar dataKey = "hired" fill = "#43a047" >
256+ < LabelList dataKey = "hired" position = "right" />
257+ </ Bar >
258+ </ >
259+ ) : (
260+ < Bar dataKey = "hiredPercentage" fill = "#43a047" >
261+ < LabelList
262+ dataKey = "hiredPercentage"
263+ position = "right"
264+ formatter = { value => `${ value } %` }
265+ />
266+ </ Bar >
267+ ) }
223268 </ BarChart >
224269 </ ResponsiveContainer >
270+
271+ { /* Manual Legend */ }
272+ < div
273+ style = { {
274+ display : 'flex' ,
275+ justifyContent : 'center' ,
276+ gap : '16px' ,
277+ marginTop : '12px' ,
278+ fontWeight : 500 ,
279+ } }
280+ >
281+ { viewMode === 'count' ? (
282+ < >
283+ < span style = { { color : '#1976d2' } } > ■ Total Applications</ span >
284+ < span style = { { color : '#43a047' } } > ■ People Hired</ span >
285+ </ >
286+ ) : (
287+ < span style = { { color : '#43a047' } } > ■ People Hired (%)</ span >
288+ ) }
289+ </ div >
290+
291+ { /* Axis Title */ }
292+ < div
293+ style = { {
294+ textAlign : 'center' ,
295+ marginTop : '10px' ,
296+ fontWeight : 500 ,
297+ } }
298+ >
299+ { viewMode === 'percentage'
300+ ? 'Percentage of People Hired (%)'
301+ : 'Number of Applications / Hires' }
302+ </ div >
225303 </ div >
226304 ) : (
227305 < div className = { styles . noData } >
0 commit comments