@@ -5,32 +5,27 @@ import DatePicker from 'react-datepicker';
55import Select from 'react-select' ;
66import { getAllApplicantVolunteerRatios } from '../../services/applicantVolunteerRatioService' ;
77import 'react-datepicker/dist/react-datepicker.css' ;
8+ import styles from './ApplicantVolunteerRatio.module.css' ;
89
910function ApplicantVolunteerRatio ( ) {
1011 const darkMode = useSelector ( state => state . theme . darkMode ) ;
1112 const [ data , setData ] = useState ( [ ] ) ;
12- const [ allRoles , setAllRoles ] = useState ( [ ] ) ; // Store all available roles
13+ const [ allRoles , setAllRoles ] = useState ( [ ] ) ;
1314 const [ loading , setLoading ] = useState ( true ) ;
1415 const [ error , setError ] = useState ( null ) ;
1516 const [ selectedRoles , setSelectedRoles ] = useState ( [ ] ) ;
1617 const [ startDate , setStartDate ] = useState ( null ) ;
1718 const [ endDate , setEndDate ] = useState ( null ) ;
1819 const [ validationError , setValidationError ] = useState ( '' ) ;
1920
20- // Fetch all available roles (without filtering)
2121 useEffect ( ( ) => {
2222 const fetchAllRoles = async ( ) => {
2323 try {
2424 const response = await getAllApplicantVolunteerRatios ( { } ) ;
25- const apiData = response . data ;
26-
27- // Get all unique roles
25+ const apiData = response . data || [ ] ;
2826 const uniqueRoles = [ ...new Set ( apiData . map ( item => item . role ) ) ] ;
2927 const roleOptions = uniqueRoles . map ( role => ( { label : role , value : role } ) ) ;
30-
3128 setAllRoles ( roleOptions ) ;
32-
33- // Set all roles as selected by default
3429 setSelectedRoles ( roleOptions ) ;
3530 } catch ( err ) {
3631 // eslint-disable-next-line no-console
@@ -42,7 +37,6 @@ function ApplicantVolunteerRatio() {
4237 fetchAllRoles ( ) ;
4338 } , [ ] ) ;
4439
45- // Fetch filtered data based on selected roles and date range
4640 useEffect ( ( ) => {
4741 const fetchFilteredData = async ( ) => {
4842 // Validate date range: start must be before or equal to end
@@ -63,23 +57,13 @@ function ApplicantVolunteerRatio() {
6357
6458 try {
6559 setLoading ( true ) ;
66-
67- // Prepare filters
6860 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- }
75- if ( selectedRoles . length > 0 ) {
76- filters . roles = selectedRoles . map ( role => role . value ) . join ( ',' ) ;
77- }
61+ if ( startDate ) filters . startDate = startDate . toISOString ( ) . split ( 'T' ) [ 0 ] ;
62+ if ( endDate ) filters . endDate = endDate . toISOString ( ) . split ( 'T' ) [ 0 ] ;
63+ if ( selectedRoles . length > 0 ) filters . roles = selectedRoles . map ( r => r . value ) . join ( ',' ) ;
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 ,
@@ -97,14 +81,31 @@ function ApplicantVolunteerRatio() {
9781 } ;
9882
9983 fetchFilteredData ( ) ;
100- } , [ startDate , endDate , selectedRoles ] ) ; // Re-fetch when date range or selected roles change
84+ } , [ startDate , endDate , selectedRoles , validationError ] ) ;
10185
102- // Filter and transform data for chart
10386 const chartData = useMemo (
10487 ( ) => data . filter ( d => selectedRoles . map ( r => r . value ) . includes ( d . role ) ) ,
10588 [ data , selectedRoles ] ,
10689 ) ;
10790
91+ const handleStartDateChange = date => {
92+ setStartDate ( date ) ;
93+ if ( endDate && date && date > endDate ) {
94+ setValidationError ( 'Start date must be earlier than or equal to End date.' ) ;
95+ } else {
96+ setValidationError ( '' ) ;
97+ }
98+ } ;
99+
100+ const handleEndDateChange = date => {
101+ setEndDate ( date ) ;
102+ if ( startDate && date && startDate > date ) {
103+ setValidationError ( 'Start date must be earlier than or equal to End date.' ) ;
104+ } else {
105+ setValidationError ( '' ) ;
106+ }
107+ } ;
108+
108109 // Inline styles for react-select to guarantee contrast in dark mode (overrides other CSS)
109110 const selectStyles = useMemo ( ( ) => {
110111 if ( ! darkMode ) return undefined ;
@@ -189,138 +190,129 @@ function ApplicantVolunteerRatio() {
189190 } ;
190191 } , [ darkMode ] ) ;
191192
193+ const containerClass = `${ styles . container } ${ darkMode ? styles . containerDark : '' } ` ;
194+ const headerClass = `${ styles . header } ${ darkMode ? styles . headerDark : '' } ` ;
195+
192196 if ( loading ) {
193197 return (
194- < div style = { { maxWidth : 900 , margin : '0 auto' , padding : 24 } } >
195- < h2 > Number of People Hired vs. Total Applications</ h2 >
196- < div > Loading...</ div >
198+ < div className = { containerClass } >
199+ < h2 className = { headerClass } > Number of People Hired vs. Total Applications</ h2 >
200+ < div className = { styles . loading } > Loading...</ div >
197201 </ div >
198202 ) ;
199203 }
200204
201205 if ( error ) {
202206 return (
203- < div style = { { maxWidth : 900 , margin : '0 auto' , padding : 24 } } >
204- < h2 > Number of People Hired vs. Total Applications</ h2 >
205- < div style = { { color : 'red' } } > { error } </ div >
207+ < div className = { containerClass } >
208+ < h2 className = { headerClass } > Number of People Hired vs. Total Applications</ h2 >
209+ < div className = { styles . error } > { error } </ div >
206210 </ div >
207211 ) ;
208212 }
209213
210- const darkModeStyles = darkMode
211- ? {
212- backgroundColor : '#1B2A41' ,
213- color : '#e0e0e0' ,
214- }
215- : { } ;
216-
217214 return (
218- < div
219- className = { `applicant-volunteer-page ${ darkMode ? 'dark-mode' : '' } ` }
220- style = { { maxWidth : 900 , margin : '0 auto' , padding : 24 } }
221- >
222- < div className = "applicant-volunteer-content" style = { darkMode ? darkModeStyles : { } } >
223- < h2 className = { darkMode ? 'text-light' : '' } >
224- Number of People Hired vs. Total Applications
225- </ h2 >
226- < div style = { { display : 'flex' , gap : 16 , marginBottom : 24 , flexWrap : 'wrap' } } >
227- < div >
228- < label
229- htmlFor = "start-date"
230- style = { { fontWeight : 500 } }
231- className = { darkMode ? 'text-light' : '' }
232- >
233- Date Range:{ ' ' }
234- </ label >
235- < DatePicker
236- id = "start-date"
237- selected = { startDate }
238- onChange = { date => setStartDate ( date ) }
239- selectsStart
240- startDate = { startDate }
241- endDate = { endDate }
242- placeholderText = "Start Date"
243- dateFormat = "yyyy/MM/dd"
244- style = { { marginRight : 8 } }
245- />
246- < span > to </ span >
247- < DatePicker
248- id = "end-date"
249- selected = { endDate }
250- onChange = { date => setEndDate ( date ) }
251- selectsEnd
252- startDate = { startDate }
253- endDate = { endDate }
254- minDate = { startDate }
255- placeholderText = "End Date"
256- dateFormat = "yyyy/MM/dd"
215+ < div className = { containerClass } >
216+ < h2 className = { headerClass } > Number of People Hired vs. Total Applications</ h2 >
217+
218+ < div className = { styles . controls } >
219+ < div className = { styles . dateGroup } >
220+ < label
221+ htmlFor = "start-date"
222+ className = { `${ styles . label } ${ darkMode ? styles . labelDark : '' } ` }
223+ >
224+ Date Range:
225+ </ label >
226+ < DatePicker
227+ id = "start-date"
228+ selected = { startDate }
229+ onChange = { handleStartDateChange }
230+ selectsStart
231+ startDate = { startDate }
232+ endDate = { endDate }
233+ placeholderText = "Start Date"
234+ dateFormat = "yyyy/MM/dd"
235+ className = { `${ styles . dateInput } ${ darkMode ? styles . dateInputDark : '' } ` }
236+ />
237+ < span className = { darkMode ? styles . labelDark : '' } > to</ span >
238+ < DatePicker
239+ id = "end-date"
240+ selected = { endDate }
241+ onChange = { handleEndDateChange }
242+ selectsEnd
243+ startDate = { startDate }
244+ endDate = { endDate }
245+ minDate = { startDate }
246+ placeholderText = "End Date"
247+ dateFormat = "yyyy/MM/dd"
248+ className = { `${ styles . dateInput } ${ darkMode ? styles . dateInputDark : '' } ` }
249+ />
250+ { validationError && (
251+ < div className = { styles . validationError } role = "alert" >
252+ { validationError }
253+ </ div >
254+ ) }
255+ </ div >
256+
257+ < div className = { styles . selectWrapper } >
258+ < label
259+ htmlFor = "role-select"
260+ className = { `${ styles . label } ${ darkMode ? styles . labelDark : '' } ` }
261+ >
262+ Role:
263+ </ label >
264+ < Select
265+ id = "role-select"
266+ isMulti
267+ options = { allRoles }
268+ value = { selectedRoles }
269+ onChange = { setSelectedRoles }
270+ placeholder = "Select roles..."
271+ className = { styles . select }
272+ classNamePrefix = "custom-select"
273+ styles = { selectStyles }
274+ menuPortalTarget = { typeof document !== 'undefined' ? document . body : undefined }
275+ />
276+ </ div >
277+ </ div >
278+
279+ { chartData . length > 0 ? (
280+ < ResponsiveContainer width = "100%" height = { 400 } >
281+ < BarChart
282+ data = { chartData }
283+ layout = "vertical"
284+ margin = { { top : 20 , right : 40 , left : 80 , bottom : 20 } }
285+ barCategoryGap = { 24 }
286+ >
287+ < XAxis
288+ type = "number"
289+ label = { {
290+ value : 'Percentage of People Hired vs. Total Applications' ,
291+ position : 'insideBottom' ,
292+ offset : - 5 ,
293+ } }
294+ allowDecimals = { false }
257295 />
258- { validationError && (
259- < div style = { { color : '#ffcc00' , marginTop : 8 , fontWeight : 'bold' } } role = "alert" >
260- { validationError }
261- </ div >
262- ) }
263- </ div >
264- < div style = { { minWidth : 220 } } >
265- < label
266- htmlFor = "role-select"
267- style = { { fontWeight : 500 } }
268- className = { darkMode ? 'text-light' : '' }
269- >
270- Role:{ ' ' }
271- </ label >
272- < Select
273- id = "role-select"
274- isMulti
275- options = { allRoles } // Use allRoles for the dropdown
276- value = { selectedRoles }
277- onChange = { setSelectedRoles }
278- placeholder = "Select roles..."
279- className = { darkMode ? 'dark-select' : '' }
280- classNamePrefix = "custom-select"
281- styles = { selectStyles }
282- menuPortalTarget = { typeof document !== 'undefined' ? document . body : undefined }
296+ < YAxis
297+ dataKey = "role"
298+ type = "category"
299+ width = { 180 }
300+ label = { { value : 'Name of Role' , angle : - 90 , position : 'insideLeft' } }
283301 />
284- </ div >
302+ < Tooltip />
303+ < Bar dataKey = "applicants" fill = "#1976d2" name = "Total Applicants" >
304+ < LabelList dataKey = "applicants" position = "right" />
305+ </ Bar >
306+ < Bar dataKey = "hired" fill = "#43a047" name = "Total Hired" >
307+ < LabelList dataKey = "hired" position = "right" />
308+ </ Bar >
309+ </ BarChart >
310+ </ ResponsiveContainer >
311+ ) : (
312+ < div className = { styles . noData } >
313+ No data available. Please add some applicant volunteer ratio data.
285314 </ div >
286- { chartData . length > 0 ? (
287- < ResponsiveContainer width = "100%" height = { 400 } >
288- < BarChart
289- data = { chartData }
290- layout = "vertical"
291- margin = { { top : 20 , right : 40 , left : 80 , bottom : 20 } }
292- barCategoryGap = { 24 }
293- >
294- < XAxis
295- type = "number"
296- label = { {
297- value : 'Percentage of People Hired vs. Total Applications' ,
298- position : 'insideBottom' ,
299- offset : - 5 ,
300- } }
301- allowDecimals = { false }
302- />
303- < YAxis
304- dataKey = "role"
305- type = "category"
306- width = { 180 }
307- label = { { value : 'Name of Role' , angle : - 90 , position : 'insideLeft' } }
308- />
309- < Tooltip />
310- < Bar dataKey = "applicants" fill = "#1976d2" name = "Total Applicants" >
311- < LabelList dataKey = "applicants" position = "right" />
312- </ Bar >
313- < Bar dataKey = "hired" fill = "#43a047" name = "Total Hired" >
314- < LabelList dataKey = "hired" position = "right" />
315- </ Bar >
316- </ BarChart >
317- </ ResponsiveContainer >
318- ) : (
319- < div style = { { textAlign : 'center' , padding : '40px 0' } } >
320- No data available. Please add some applicant volunteer ratio data.
321- </ div >
322- ) }
323- </ div >
315+ ) }
324316 </ div >
325317 ) ;
326318}
0 commit comments