Skip to content

Commit 8e60aae

Browse files
committed
feature: implement frontend to display piechart for opt status breakdown of applicants
1 parent 1f8b049 commit 8e60aae

11 files changed

Lines changed: 11051 additions & 24820 deletions

File tree

package-lock.json

Lines changed: 9898 additions & 20148 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/* eslint-disable no-console */
2+
import axios from 'axios';
3+
import { ENDPOINTS } from '~/utils/URL';
4+
import { GET_ERRORS } from '../constants/errors';
5+
import { GET_OPT_STATUS_BREAKDOWN } from '../constants/optStatusBreakdownConstants';
6+
7+
export const setOptStatusBreakdown = payload => {
8+
return {
9+
type: GET_OPT_STATUS_BREAKDOWN,
10+
payload
11+
};
12+
};
13+
export const setErrors = payload => {
14+
return {
15+
type: GET_ERRORS,
16+
payload
17+
};
18+
}
19+
export const fetchOptStatusBreakdown = (startDate = "", endDate = "", role = "") => {
20+
const url = ENDPOINTS.OPT_STATUS_BREAKDOWN(startDate, endDate, role);
21+
return async dispatch => {
22+
try {
23+
const response = await axios.get(url);
24+
dispatch(setOptStatusBreakdown(response.data.breakDown));
25+
} catch (error) {
26+
// eslint-disable-next-line no-console
27+
console.error("Error fetching OPT status breakdown:", error);
28+
}
29+
};
30+
};
31+
32+
export default fetchOptStatusBreakdown;
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
.opt-status-container {
2+
width: 100%;
3+
max-width: 1000px;
4+
margin: 0 auto;
5+
padding: 1.5rem;
6+
background-color: #ffffff;
7+
border-radius: 12px;
8+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
9+
}
10+
11+
.opt-status-title {
12+
font-size: 1.75rem;
13+
font-weight: 600;
14+
margin-bottom: 1.5rem;
15+
text-align: center;
16+
color: #333;
17+
}
18+
19+
.chart-filter-layout {
20+
display: flex;
21+
justify-content: center;
22+
align-items: flex-start;
23+
gap: 2rem;
24+
flex-wrap: wrap;
25+
}
26+
27+
.pie-chart-wrapper {
28+
width: 400px;
29+
height: 400px;
30+
position: relative;
31+
}
32+
33+
.filters {
34+
display: flex;
35+
flex-direction: column;
36+
gap: 1rem;
37+
min-width: 300px; /* increased */
38+
max-width: 400px; /* increased */
39+
}
40+
41+
.filters label {
42+
display: flex;
43+
flex-direction: column;
44+
font-size: 0.9rem;
45+
color: #444;
46+
}
47+
48+
.filters input,
49+
.filters select {
50+
padding: 0.5rem 0.75rem;
51+
border-radius: 6px;
52+
border: 1px solid #ccc;
53+
font-size: 1rem;
54+
margin-top: 0.25rem;
55+
outline: none;
56+
transition: border-color 0.2s;
57+
}
58+
59+
.filters input:focus,
60+
.filters select:focus {
61+
border-color: #4caf50;
62+
}
63+
64+
.date-inputs {
65+
display: flex;
66+
align-items: center;
67+
gap: 0.5rem; /* space between inputs and button */
68+
margin-top: 0.25rem;
69+
flex-wrap: nowrap; /* keep everything on one line */
70+
}
71+
72+
.date-inputs input {
73+
width: 40%; /* adjust so two inputs + button fit */
74+
min-width: 100px; /* prevent them from shrinking too much */
75+
}
76+
77+
.reset-btn {
78+
flex: 0 0 auto; /* don't grow or shrink */
79+
padding: 0.4rem 0.8rem;
80+
border-radius: 6px;
81+
border: 1px solid #f44336;
82+
background-color: #f44336;
83+
color: #fff;
84+
font-size: 0.9rem;
85+
cursor: pointer;
86+
transition: background-color 0.2s, border-color 0.2s;
87+
}
88+
.reset-btn:hover {
89+
background-color: #d32f2f;
90+
border-color: #d32f2f;
91+
}
92+
93+
@media (max-width: 768px) {
94+
.chart-filter-layout {
95+
flex-direction: column;
96+
align-items: center;
97+
}
98+
99+
.pie-chart-wrapper {
100+
width: 100%;
101+
height: auto;
102+
max-width: 90vw;
103+
}
104+
105+
.filters {
106+
width: 100%;
107+
max-width: 400px;
108+
align-items: stretch;
109+
}
110+
.date-inputs {
111+
flex-wrap: wrap; /* allows inputs and button to wrap if necessary */
112+
}
113+
.date-inputs input {
114+
width: 45%; /* slightly smaller to fit button */
115+
}
116+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/* eslint-disable jsx-a11y/label-has-associated-control */
2+
/* eslint-disable object-shorthand */
3+
/* eslint-disable func-names */
4+
/* eslint-disable prettier/prettier */
5+
/* eslint-disable react/function-component-definition */
6+
import { useEffect, useState } from 'react';
7+
import { Pie } from 'react-chartjs-2';
8+
import { useDispatch, useSelector } from 'react-redux';
9+
import ChartDataLabels from 'chartjs-plugin-datalabels';
10+
import { Chart as ChartJS } from 'chart.js';
11+
import { fetchOptStatusBreakdown } from '../../actions/optStatusBreakdownAction';
12+
import { roleOptions } from './filter';
13+
import 'chart.js/auto';
14+
import './OptStatusPieChart.css';
15+
16+
ChartJS.register(ChartDataLabels);
17+
18+
const COLORS = {
19+
'OPT started': '#f44336',
20+
'CPT not eligible': '#f4e34cfc',
21+
'OPT not yet started': '#2196f3',
22+
Citizen: '#4caf50',
23+
'N/A': '#ff9800',
24+
};
25+
26+
const OptStatusPieChart = () => {
27+
const dispatch = useDispatch();
28+
const { optStatusBreakdown } = useSelector(state => state.optStatusBreakdown);
29+
// const { optStatusBreakdown = [] } = useSelector(state => state.optStatus);
30+
const [startDate, setStartDate] = useState('');
31+
const [endDate, setEndDate] = useState('');
32+
const [role, setRole] = useState('');
33+
34+
useEffect(() => {
35+
dispatch(fetchOptStatusBreakdown(startDate, endDate, role));
36+
}, [startDate, endDate, role, dispatch]);
37+
38+
const labels = optStatusBreakdown.map(d => d.optStatus);
39+
const dataCounts = optStatusBreakdown.map(d => d.count);
40+
const total = dataCounts.reduce((sum, value) => sum + value, 0);
41+
const backgroundColors = labels.map(label => COLORS[label] || '#ccc');
42+
43+
const chartData = {
44+
labels,
45+
datasets: [
46+
{
47+
data: dataCounts,
48+
backgroundColor: backgroundColors,
49+
},
50+
],
51+
};
52+
53+
const options = {
54+
responsive: true,
55+
maintainAspectRatio: false,
56+
plugins: {
57+
legend: { display: false },
58+
datalabels: {
59+
color: '#000',
60+
font: { weight: 'bold' },
61+
// eslint-disable-next-line no-unused-vars
62+
formatter: (value, context) => {
63+
const percent = ((value / total) * 100).toFixed(1);
64+
return `${percent}%\n(${value})`;
65+
},
66+
},
67+
tooltip: {
68+
callbacks: {
69+
label: function(context) {
70+
const count = context.raw;
71+
const percent = ((count / total) * 100).toFixed(1);
72+
const { label } = context;
73+
return `${label}: ${percent}% (${count})`;
74+
},
75+
},
76+
},
77+
},
78+
};
79+
80+
return (
81+
<div className="opt-status-container">
82+
<h2 className="opt-status-title">Breakdown by OPT Status</h2>
83+
84+
<div className="chart-filter-layout">
85+
<div className="pie-chart-wrapper">
86+
<Pie data={chartData} options={options} />
87+
</div>
88+
89+
<div className="filters">
90+
<label>
91+
<span>
92+
<strong>Dates</strong>
93+
</span>
94+
<div className="date-inputs">
95+
<input type="date" value={startDate} onChange={e => setStartDate(e.target.value)} />
96+
<input type="date" value={endDate} onChange={e => setEndDate(e.target.value)} />
97+
<button
98+
type="button"
99+
className="reset-btn"
100+
onClick={() => {
101+
setStartDate('');
102+
setEndDate('');
103+
}}
104+
>
105+
Reset
106+
</button>
107+
</div>
108+
</label>
109+
110+
<label>
111+
<span>
112+
<strong>Role</strong>
113+
</span>
114+
<select value={role} onChange={e => setRole(e.target.value)}>
115+
{roleOptions.map(option => (
116+
<option key={option.value} value={option.value}>
117+
{option.label}
118+
</option>
119+
))}
120+
</select>
121+
</label>
122+
</div>
123+
</div>
124+
</div>
125+
);
126+
};
127+
128+
export default OptStatusPieChart;
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
/* eslint-disable prettier/prettier */
2+
/* eslint-disable import/prefer-default-export */
3+
export const roleOptions = [
4+
{ label: 'All', value: '' },
5+
{ label: 'Analyst', value: 'Analyst' },
6+
{ label: 'Developer', value: 'Developer' },
7+
{ label: 'QA', value: 'QA' },
8+
{ label: 'Intern', value: 'Intern' },
9+
];
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/* eslint-disable import/prefer-default-export */
2+
export const GET_OPT_STATUS_BREAKDOWN = 'GET_OPT_STATUS_BREAKDOWN';

src/reducers/index.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,8 @@ import { jobApplicationReducer } from './jobApplication/jobApplicationReducer';
8686
// lbdashboard
8787
import wishListReducer from './listBidDashboard/wishListItemReducer';
8888

89+
import { optStatusBreakdownReducer } from './optStatusBreakdownReducer';
90+
8991
// listing and biddding dashboard
9092

9193
import {
@@ -179,6 +181,8 @@ const localReducers = {
179181
lbmessaging: messageReducer,
180182
lbuserpreferences: userPreferencesReducer,
181183

184+
optStatusBreakdown: optStatusBreakdownReducer,
185+
182186
WishListItem: wishListReducer,
183187

184188
listOverview: listOverviewReducer,
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { GET_OPT_STATUS_BREAKDOWN } from '../constants/optStatusBreakdownConstants';
2+
3+
const initialState = {
4+
optStatusBreakdown: [],
5+
loading: false,
6+
error: null,
7+
};
8+
9+
export const optStatusBreakdownReducer = (state = initialState, action) => {
10+
switch (action.type) {
11+
case GET_OPT_STATUS_BREAKDOWN:
12+
return {
13+
...state,
14+
optStatusBreakdown: action.payload,
15+
loading: false,
16+
error: null,
17+
};
18+
default:
19+
return state;
20+
}
21+
};

src/routes.jsx

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ import SupportDashboard from './components/SupportPortal/SupportDashboard';
214214
import SupportLogViewer from './components/SupportPortal/SupportLogViewer';
215215
import JobApplicationForm from './components/Collaboration/JobApplicationForm/JobApplicationForm';
216216

217+
import OptStatusPieChart from './components/OptStatusPieChart/OptStatusPieChart';
217218
// Social Architecture
218219

219220
const ResourceManagement = lazy(() => import('./components/ResourceManagement/ResourceManagement'));
@@ -943,27 +944,6 @@ export default (
943944
<ProtectedRoute path="/ExperienceDonutChart" component={ExperienceDonutChart} fallback />
944945
<ProtectedRoute path="/prPromotionsPage" component={PRPromotionsPage} fallback />
945946
<ProtectedRoute path="/" exact component={Dashboard} />
946-
{/* ----- PR Dashboard ----- */}
947-
<ProtectedRoute
948-
path="/pr-dashboard/promotion-eligibility"
949-
exact
950-
component={PromotionEligibility}
951-
/>
952-
{/* /* for support team*/}
953-
<Route path="/support/login" component={SupportLogin} />
954-
<Route path="/support/dashboard" component={SupportDashboard} />
955-
<Route path="/support/log/:studentId" component={SupportLogViewer} />
956-
<ProtectedRoute
957-
path="/pr-team-analytics/popular-prs"
958-
exact
959-
component={PRReviewTeamAnalytics}
960-
/>
961-
<ProtectedRoute
962-
path="/pr-grading-dashboard"
963-
exact
964-
component={PRGradingDashboard}
965-
fallback
966-
/>
967947
<Route path="*" component={NotFoundPage} />
968948
</Switch>
969949
</>

src/utils/URL.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,6 +473,19 @@ export const ENDPOINTS = {
473473
LB_LISTING_BOOK: `${APIEndpoint}/lb/listing/availability/booking`,
474474
HELP_CATEGORIES: `${APIEndpoint}/help-categories`,
475475

476+
OPT_STATUS_BREAKDOWN: (startDate, endDate, role) => {
477+
let url = `${APIEndpoint}/analytics/opt-status`;
478+
const params = [];
479+
480+
if (startDate) params.push(`startDate=${startDate}`);
481+
if (endDate) params.push(`endDate=${endDate}`);
482+
if (role) params.push(`role=${role}`);
483+
484+
return params.length > 0 ? `${url}?${params.join("&")}` : url;
485+
},
486+
487+
488+
476489
// job analytics
477490
HOURS_PLEDGED: `${APIEndpoint}/analytics/hours-pledged`,
478491

0 commit comments

Comments
 (0)