Skip to content

Commit 893eae5

Browse files
Merge pull request #3840 from OneCommunityGlobal/Pranav-create-donut-chart
Pranav- donut chart for experience break down
2 parents 66a0e1b + f5c56b0 commit 893eae5

3 files changed

Lines changed: 304 additions & 1 deletion

File tree

Lines changed: 296 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,296 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { useSelector } from 'react-redux';
3+
import axios from 'axios';
4+
import { PieChart, Pie, Cell, Tooltip, ResponsiveContainer } from 'recharts';
5+
import DatePicker from 'react-datepicker';
6+
import Select from 'react-select';
7+
import 'react-datepicker/dist/react-datepicker.css';
8+
9+
const COLORS = ['#8884d8', '#82ca9d', '#ffc658', '#ff8042'];
10+
11+
const ExperienceBreakdownChart = () => {
12+
const [data, setData] = useState([]);
13+
const [roles, setRoles] = useState([]);
14+
const [selectedRoles, setSelectedRoles] = useState([]);
15+
const [startDate, setStartDate] = useState(null);
16+
const [endDate, setEndDate] = useState(null);
17+
const [loading, setLoading] = useState(false);
18+
const [noData, setNoData] = useState(false);
19+
const darkMode = useSelector(state => state.theme.darkMode);
20+
21+
const containerRef = useRef(null);
22+
const [containerWidth, setContainerWidth] = useState(0);
23+
24+
useEffect(() => {
25+
if (!containerRef.current) return;
26+
const ro = new ResizeObserver(entries => {
27+
const w = entries[0]?.contentRect?.width || 0;
28+
setContainerWidth(w);
29+
});
30+
ro.observe(containerRef.current);
31+
return () => ro.disconnect();
32+
}, []);
33+
34+
const isNarrow = containerWidth && containerWidth < 520;
35+
36+
const fontSizeFor = percent => {
37+
if (containerWidth < 320) return percent < 0.12 ? 9 : 10;
38+
if (containerWidth < 400) return percent < 0.12 ? 10 : 11;
39+
if (containerWidth < 520) return percent < 0.12 ? 11 : 12;
40+
return 13;
41+
};
42+
43+
const renderInsideLabel = ({ cx, cy, midAngle, innerRadius, outerRadius, percent, payload }) => {
44+
const radius = innerRadius + (outerRadius - innerRadius) / 2;
45+
const rad = (-midAngle * Math.PI) / 180;
46+
const x = cx + radius * Math.cos(rad);
47+
const y = cy + radius * Math.sin(rad);
48+
const txt = `${payload.experience} (${payload.count})`;
49+
return (
50+
<text
51+
x={x}
52+
y={y}
53+
fill={darkMode ? '#fff' : '#000'}
54+
textAnchor="middle"
55+
dominantBaseline="central"
56+
fontSize={fontSizeFor(percent)}
57+
style={{ pointerEvents: 'none' }}
58+
>
59+
{txt}
60+
</text>
61+
);
62+
};
63+
64+
const renderOutsideLabel = ({ experience, count, percentage }) =>
65+
`${experience} - ${count} (${percentage}%)`;
66+
67+
const fetchData = async () => {
68+
setLoading(true);
69+
setNoData(false);
70+
try {
71+
const params = {};
72+
if (startDate) params.startDate = startDate.toISOString().split('T')[0];
73+
if (endDate) params.endDate = endDate.toISOString().split('T')[0];
74+
if (selectedRoles.length > 0) params.roles = selectedRoles.map(r => r.value).join(',');
75+
const res = await axios.get('/api/applicants/experience-breakdown', {
76+
params,
77+
});
78+
setData(res.data);
79+
if (res.data.length === 0) setNoData(true);
80+
} catch (error) {
81+
if (error.response?.status === 404) {
82+
setData([]);
83+
setNoData(true);
84+
} else {
85+
console.error('Error fetching chart data:', error);
86+
}
87+
} finally {
88+
setLoading(false);
89+
}
90+
};
91+
92+
const fetchAllRoles = async () => {
93+
try {
94+
const res = await axios.get('/api/applicants/experience-roles');
95+
const options = res.data.map(role => ({ value: role, label: role }));
96+
setRoles(options);
97+
} catch (error) {
98+
console.error('Error fetching roles:', error);
99+
}
100+
};
101+
102+
useEffect(() => {
103+
fetchData();
104+
fetchAllRoles();
105+
}, []);
106+
107+
return (
108+
<div
109+
ref={containerRef}
110+
className={`${darkMode ? 'bg-oxford-blue text-light' : ''}`}
111+
style={{
112+
padding: '20px',
113+
width: '100%',
114+
minHeight: '100vh',
115+
}}
116+
>
117+
{/* Filters */}
118+
<div
119+
className={`mb-6 rounded-lg shadow ${darkMode ? 'bg-space-cadet text-light' : 'bg-white'}`}
120+
style={{
121+
padding: '15px 20px',
122+
display: 'flex',
123+
flexWrap: 'wrap',
124+
gap: 30,
125+
alignItems: 'flex-end',
126+
}}
127+
>
128+
<div style={{ display: 'flex', flexDirection: 'column' }}>
129+
<label
130+
htmlFor="startDate"
131+
style={{
132+
fontSize: 14,
133+
fontWeight: 600,
134+
marginBottom: 5,
135+
color: darkMode ? '#fff' : '#000',
136+
}}
137+
>
138+
Start Date
139+
</label>
140+
<DatePicker
141+
selected={startDate}
142+
onChange={setStartDate}
143+
placeholderText="Select start date"
144+
className={darkMode ? 'bg-space-cadet text-light dark-mode-placeholder' : ''}
145+
/>
146+
</div>
147+
148+
<div style={{ display: 'flex', flexDirection: 'column' }}>
149+
<label
150+
htmlFor="endDate"
151+
style={{
152+
fontSize: 14,
153+
fontWeight: 600,
154+
marginBottom: 5,
155+
color: darkMode ? '#fff' : '#000',
156+
}}
157+
>
158+
End Date
159+
</label>
160+
<DatePicker
161+
selected={endDate}
162+
onChange={setEndDate}
163+
placeholderText="Select end date"
164+
className={darkMode ? 'bg-space-cadet text-light dark-mode-placeholder' : ''}
165+
/>
166+
</div>
167+
168+
<div style={{ display: 'flex', flexDirection: 'column', minWidth: 250 }}>
169+
<label
170+
htmlFor="roles"
171+
style={{
172+
fontSize: 14,
173+
fontWeight: 600,
174+
marginBottom: 5,
175+
color: darkMode ? '#fff' : '#000',
176+
}}
177+
>
178+
Roles
179+
</label>
180+
<Select
181+
isMulti
182+
options={roles}
183+
value={selectedRoles}
184+
onChange={setSelectedRoles}
185+
placeholder="Select roles"
186+
classNamePrefix={darkMode ? 'react-select-dark' : 'react-select'}
187+
styles={{
188+
control: base => ({
189+
...base,
190+
backgroundColor: darkMode ? '#1b1f3b' : '#fff',
191+
color: darkMode ? '#fff' : '#000',
192+
}),
193+
menu: base => ({
194+
...base,
195+
backgroundColor: darkMode ? '#1b1f3b' : '#fff',
196+
color: darkMode ? '#fff' : '#000',
197+
}),
198+
option: (base, { isFocused, isSelected }) => ({
199+
...base,
200+
backgroundColor: isSelected
201+
? darkMode
202+
? '#4a4f74'
203+
: '#d1d1d1'
204+
: isFocused
205+
? darkMode
206+
? '#2c2f4a'
207+
: '#eee'
208+
: 'transparent',
209+
color: isSelected ? (darkMode ? '#fff' : '#000') : darkMode ? '#fff' : '#000',
210+
}),
211+
singleValue: base => ({
212+
...base,
213+
color: darkMode ? '#fff' : '#000',
214+
}),
215+
multiValueLabel: base => ({
216+
...base,
217+
color: darkMode ? 'red' : '#000',
218+
}),
219+
}}
220+
/>
221+
</div>
222+
223+
<button
224+
onClick={fetchData}
225+
style={{
226+
marginLeft: 'auto',
227+
backgroundColor: '#007bff',
228+
color: '#fff',
229+
border: 'none',
230+
padding: '8px 16px',
231+
borderRadius: 4,
232+
cursor: 'pointer',
233+
fontWeight: 600,
234+
}}
235+
>
236+
Apply Filters
237+
</button>
238+
</div>
239+
240+
{/* Chart or Message */}
241+
{loading && <p style={{ textAlign: 'center' }}>Loading...</p>}
242+
243+
{noData && !loading && (
244+
<div style={{ textAlign: 'center', marginTop: 40, color: darkMode ? '#ccc' : '#777' }}>
245+
<svg
246+
xmlns="http://www.w3.org/2000/svg"
247+
style={{ width: 60, height: 60, marginBottom: 10 }}
248+
fill="none"
249+
viewBox="0 0 24 24"
250+
stroke="currentColor"
251+
>
252+
<path
253+
strokeLinecap="round"
254+
strokeLinejoin="round"
255+
strokeWidth={1.5}
256+
d="M9 13h6m2 0a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v4a2 2 0 002 2m2 0v5a2 2 0 004 0v-5"
257+
/>
258+
</svg>
259+
<p style={{ fontSize: 18 }}>No data available for the selected filters.</p>
260+
</div>
261+
)}
262+
263+
{!loading && !noData && data.length > 0 && (
264+
<ResponsiveContainer width="100%" height={320}>
265+
<PieChart>
266+
<Pie
267+
data={data}
268+
dataKey="count"
269+
nameKey="experience"
270+
cx="50%"
271+
cy="50%"
272+
innerRadius={isNarrow ? 40 : 0}
273+
outerRadius={Math.max(90, Math.min(130, Math.floor(containerWidth / 3)))}
274+
labelLine={!isNarrow}
275+
label={isNarrow ? renderInsideLabel : renderOutsideLabel}
276+
>
277+
{data.map((entry, index) => (
278+
<Cell key={entry.experience} fill={COLORS[index % COLORS.length]} />
279+
))}
280+
</Pie>
281+
<Tooltip
282+
formatter={value => [`${value}`, 'Applicants']}
283+
labelFormatter={() => 'Experience'}
284+
contentStyle={{
285+
backgroundColor: darkMode ? '#1b1f3b' : '#fff',
286+
color: darkMode ? '#fff' : '#000',
287+
}}
288+
/>
289+
</PieChart>
290+
</ResponsiveContainer>
291+
)}
292+
</div>
293+
);
294+
};
295+
296+
export default ExperienceBreakdownChart;

src/routes.jsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import FormEditor from '~/components/Forms/FormEditor';
22
import FormViewer from '~/components/Forms/FormViewer';
3-
3+
import ExperienceBreakdownChart from '~/components/ExperienceBreakdownChart';
44
import { lazy } from 'react';
55
import { Route, Switch } from 'react-router-dom';
66
import SetupProfile from '~/components/SetupProfile/SetupProfile';
@@ -681,6 +681,8 @@ export default (
681681
<ProtectedRoute path="/userprofileedit/:userId" component={UserProfileEdit} />
682682
<ProtectedRoute path="/updatepassword/:userId" component={UpdatePassword} />
683683
<Route path="/Logout" component={Logout} />
684+
<Route path="/experience-breakdown" exact component={ExperienceBreakdownChart} />
685+
684686
<Route path="/forcePasswordUpdate/:userId" component={ForcePasswordUpdate} />
685687
{/* ----- HGN Help Community Skills Dashboard Routes ----- */}
686688
<ProtectedRoute path="/hgnhelp" exact component={LandingPage} />

vite.config.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ export default defineConfig(({ mode }) => {
2121
return prev;
2222
}, {}),
2323
},
24+
server: {
25+
proxy: {
26+
'/api': 'http://localhost:4500',
27+
},
28+
},
2429
build: {
2530
outDir: 'build',
2631
},

0 commit comments

Comments
 (0)