Skip to content

Commit 6ffad10

Browse files
Merge pull request #4325 from OneCommunityGlobal/harsha_job_hits_and_applications
Harshavarma: Created chart for job roles hit and applications
2 parents 4732d63 + 8cd16db commit 6ffad10

9 files changed

Lines changed: 1375 additions & 774 deletions

File tree

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { JOBS_APPS_HITS_REQUEST, JOBS_APPS_HITS_REQUEST_SUCCESS, JOBS_APPS_HITS_REQUEST_FAILURE } from '../../constants/jobAnalytics/JobsApplicationsHitsConstants';
2+
import { ENDPOINTS } from '../../utils/URL';
3+
4+
export const fetchJobsHitsApplications = (queryParams, token) => async (dispatch) => {
5+
dispatch({ type: JOBS_APPS_HITS_REQUEST });
6+
7+
try {
8+
const response = await fetch(`${ENDPOINTS.JOB_HITS_AND_APPLICATIONS}?${queryParams}`, {
9+
method: 'GET',
10+
headers: {
11+
'Content-Type': 'application/json',
12+
'Authorization': token
13+
}
14+
});
15+
16+
const data = await response.json();
17+
18+
if(!response.ok) {
19+
throw new Error(data.error || 'Failed to fetch data');
20+
}
21+
22+
dispatch({ type: JOBS_APPS_HITS_REQUEST_SUCCESS, payload: data });
23+
} catch (error) {
24+
dispatch({ type: JOBS_APPS_HITS_REQUEST_FAILURE, payload: error.message });
25+
}
26+
}
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import { Fragment, useEffect, useState } from 'react';
2+
import { fetchJobsHitsApplications } from '../../../actions/jobAnalytics/JobsHitsApplicationsActions';
3+
import { useDispatch, useSelector } from 'react-redux';
4+
import { BarChart, XAxis, YAxis, Tooltip, Legend, Bar, ResponsiveContainer, Brush } from 'recharts';
5+
import styles from './JobsHitsApplicationsChart.module.css';
6+
import Select from 'react-select';
7+
import DatePicker from 'react-datepicker';
8+
9+
export const JobsHitsApplicationsChart = () => {
10+
const [startDate, setStartDate] = useState(null);
11+
const [endDate, setEndDate] = useState(null);
12+
const [selectedRoles, setSelectedRoles] = useState([]);
13+
const [roleOptions, setRoleOptions] = useState([]);
14+
const [roleAssigned, setRoleAssigned] = useState(false);
15+
16+
const { loading, data, error } = useSelector(state => state.jobsHitsApplications);
17+
const darkMode = useSelector(state => state.theme.darkMode);
18+
const dispatch = useDispatch();
19+
const token = localStorage.getItem('token');
20+
21+
useEffect(() => {
22+
const queryParams = new URLSearchParams();
23+
if (startDate) queryParams.append('startDate', startDate.toISOString());
24+
if (endDate) queryParams.append('endDate', endDate.toISOString());
25+
if (selectedRoles.length > 0) {
26+
const roles = selectedRoles.map(role => role.value).join(',');
27+
queryParams.append('roles', roles);
28+
}
29+
dispatch(fetchJobsHitsApplications(queryParams.toString(), token));
30+
}, [startDate, endDate, selectedRoles, dispatch, token]);
31+
32+
useEffect(() => {
33+
if (data && data.length > 0 && !roleAssigned) {
34+
setRoleOptions(
35+
data.map(item => {
36+
return {
37+
value: item.role,
38+
label: item.role,
39+
};
40+
}, setRoleAssigned(true)),
41+
);
42+
}
43+
}, [loading, error, data]);
44+
45+
const CustomYAxisNames = ({ x, y, payload, darkMode }) => {
46+
const text = payload.value;
47+
const extractedRole = text
48+
.split('-')
49+
.slice(0, 1)[0]
50+
.trim();
51+
return (
52+
<g transform={`translate(${x},${y})`}>
53+
<text x={0} y={0} textAnchor="end" fill={darkMode ? '#ffffff' : '#000000'} fontSize={12}>
54+
<title>{text}</title>
55+
{extractedRole.length > 35 ? `${extractedRole.slice(0, 32)}...` : extractedRole}
56+
</text>
57+
</g>
58+
);
59+
};
60+
61+
const handleStartDateChange = date => {
62+
if (endDate && date > endDate) {
63+
setEndDate(date);
64+
}
65+
setStartDate(date);
66+
};
67+
68+
const handleEndDateChange = date => {
69+
if (startDate && date < startDate) {
70+
setStartDate(date);
71+
}
72+
setEndDate(date);
73+
};
74+
75+
const handleResetDates = () => {
76+
setStartDate(null);
77+
setEndDate(null);
78+
};
79+
80+
return (
81+
<Fragment>
82+
<div className={`${styles.mainContainer} ${darkMode ? styles.bgOxfordBlue : ''}`}>
83+
<h4 className={darkMode ? styles.colorWhite : ''}>Role-wise Hits and Applications</h4>
84+
<div className={styles.filterContainer}>
85+
<div className={styles.dateFilter}>
86+
<div className={styles.dateReset}>
87+
{startDate || endDate ? (
88+
<button
89+
onClick={handleResetDates}
90+
className={`${styles.resetBtn} ${darkMode ? styles.resetBtnDark : ''}`}
91+
>
92+
Reset Dates
93+
</button>
94+
) : (
95+
<button
96+
className={`${styles.resetBtn} ${darkMode ? styles.resetBtnDark : ''}`}
97+
disabled
98+
>
99+
Reset Dates
100+
</button>
101+
)}
102+
</div>
103+
<div className={styles.startDate}>
104+
<label
105+
htmlFor="start-date"
106+
className={`${styles.dateName} ${darkMode ? styles.colorWhite : ''}`}
107+
>
108+
Start Date:
109+
</label>
110+
<DatePicker
111+
id="start-date"
112+
selected={startDate}
113+
onChange={handleStartDateChange}
114+
selectsStart
115+
startDate={startDate}
116+
endDate={endDate}
117+
placeholderText="Start Date"
118+
className={`${styles.datePicker} ${darkMode ? styles.bgSpaceCadet : ''}`}
119+
/>
120+
</div>
121+
<div className={styles.endDate}>
122+
<label
123+
htmlFor="end-date"
124+
className={`${styles.dateName} ${darkMode ? styles.colorWhite : ''}`}
125+
>
126+
End Date:
127+
</label>
128+
<DatePicker
129+
id="end-date"
130+
selected={endDate}
131+
onChange={handleEndDateChange}
132+
selectsEnd
133+
startDate={startDate}
134+
endDate={endDate}
135+
placeholderText="End Date"
136+
className={`${styles.datePicker} ${darkMode ? styles.bgSpaceCadet : ''}`}
137+
/>
138+
</div>
139+
</div>
140+
141+
<div className={`${styles.roleFilter}`}>
142+
<div className={styles.roleFilterContainer}>
143+
<label htmlFor="role-select" className={darkMode ? styles.colorWhite : ''}>
144+
Roles:
145+
</label>
146+
<Select
147+
id="role-select"
148+
isMulti
149+
options={roleOptions}
150+
onChange={setSelectedRoles}
151+
className={styles.roleSelector}
152+
styles={{
153+
control: base => ({
154+
...base,
155+
backgroundColor: darkMode ? '#1C2541' : 'white',
156+
color: darkMode ? 'white' : 'black',
157+
borderColor: darkMode ? '#3A506B' : base.borderColor,
158+
}),
159+
160+
menu: base => ({
161+
...base,
162+
backgroundColor: darkMode ? '#1C2541' : 'white',
163+
}),
164+
165+
option: (base, state) => ({
166+
...base,
167+
backgroundColor: darkMode
168+
? state.isFocused
169+
? '#3A506B' // hover color (FIX)
170+
: '#1C2541'
171+
: state.isFocused
172+
? '#eee'
173+
: 'white',
174+
color: darkMode ? 'white' : 'black', // FIX: always readable
175+
}),
176+
177+
multiValue: base => ({
178+
...base,
179+
backgroundColor: darkMode ? '#3A506B' : base.backgroundColor,
180+
}),
181+
182+
multiValueLabel: base => ({
183+
...base,
184+
color: 'white', // FIX: text inside selected tags
185+
}),
186+
187+
multiValueRemove: base => ({
188+
...base,
189+
color: 'white',
190+
':hover': {
191+
backgroundColor: '#5BC0BE',
192+
color: 'black',
193+
},
194+
}),
195+
196+
singleValue: base => ({
197+
...base,
198+
color: darkMode ? 'white' : 'black',
199+
}),
200+
201+
input: base => ({
202+
...base,
203+
color: darkMode ? 'white' : 'black',
204+
}),
205+
206+
placeholder: base => ({
207+
...base,
208+
color: darkMode ? '#ccc' : '#666',
209+
}),
210+
}}
211+
/>
212+
</div>
213+
</div>
214+
</div>
215+
<div className={styles.chartContainer}>
216+
{loading && <div className={`${styles.spinner}`}>Loading...</div>}
217+
{error && <div className={`${styles.errorMessage}`}>Issue getting the data</div>}
218+
{!loading && !error && data.length === 0 && (
219+
<div className={`${styles.emptyMessage}`}>
220+
No data available for the selected filters.
221+
</div>
222+
)}
223+
{!loading && !error && data.length > 0 && (
224+
<ResponsiveContainer className={styles.chart} width="100%" height="100%">
225+
<BarChart
226+
layout="vertical"
227+
data={data}
228+
barCategoryGap="40%"
229+
barGap={4}
230+
margin={{ top: 20, right: 30, left: 65, bottom: 20 }}
231+
>
232+
<Bar
233+
dataKey="hits"
234+
fill="#8884d8"
235+
activeBar={false}
236+
isAnimationActive={false}
237+
barSize={14}
238+
/>
239+
<Bar
240+
dataKey="applications"
241+
fill="#82ca9d"
242+
activeBar={false}
243+
isAnimationActive={false}
244+
barSize={14}
245+
/>
246+
<XAxis
247+
type="number"
248+
tick={{ fill: darkMode ? '#ffffff' : '#000000' }}
249+
label={{
250+
value: 'Number of Hits/Applications',
251+
position: 'bottom',
252+
style: {
253+
fill: darkMode ? '#ffffff' : '#000000',
254+
},
255+
}}
256+
/>
257+
<YAxis
258+
type="category"
259+
dataKey="role"
260+
width={200}
261+
label={{
262+
value: 'Roles',
263+
position: 'bottom',
264+
style: {
265+
fill: darkMode ? '#ffffff' : '#000000',
266+
},
267+
}}
268+
tick={<CustomYAxisNames darkMode={darkMode} />}
269+
/>
270+
<Tooltip
271+
cursor={{ fill: 'transparent' }}
272+
contentStyle={{
273+
backgroundColor: darkMode ? '#1C2541' : '#fff',
274+
border: darkMode ? '1px solid #3A506B' : '1px solid #ccc',
275+
color: darkMode ? '#fff' : '#000',
276+
}}
277+
itemStyle={{
278+
color: darkMode ? '#fff' : '#000',
279+
}}
280+
labelStyle={{
281+
color: darkMode ? '#fff' : '#000',
282+
}}
283+
/>
284+
<Legend verticalAlign="top" align="center" />
285+
286+
{data.length > 7 && (
287+
<Brush
288+
dataKey="role"
289+
height={20}
290+
// stroke={darkMode ? '#5BC0BE' : '#8884d8'} // border/line
291+
startIndex={0}
292+
endIndex={7}
293+
y={480}
294+
travellerWidth={10}
295+
tickFormatter={value => value}
296+
fill={darkMode ? '#1C2541' : '#fff'}
297+
traveller={{
298+
stroke: darkMode ? '#5BC0BE' : '#8884d8',
299+
fill: darkMode ? '#5BC0BE' : '#8884d8',
300+
}}
301+
/>
302+
)}
303+
</BarChart>
304+
</ResponsiveContainer>
305+
)}
306+
</div>
307+
</div>
308+
</Fragment>
309+
);
310+
};

0 commit comments

Comments
 (0)