Skip to content

Commit

Permalink
#105 Ft applicant dashboard (#197)
Browse files Browse the repository at this point in the history
* add applicant dashboard page

* add dashboard cards

* integrate with backend

* fetch all data from their respective queries

* remove console logs

* update cohort types and query

* fix routes

* fix code climate

* add graph

* fix chart

* delete yarn

* fix graph and login

* fix

* trigger

* revert

---------

Co-authored-by: uwituzeb <b.uwituze@alustudent.com>
  • Loading branch information
berniceu and uwituzeb authored Oct 14, 2024
1 parent af249ad commit 78b4b3b
Show file tree
Hide file tree
Showing 24 changed files with 678 additions and 28 deletions.
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"@babel/preset-env": "^7.19.3",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.18.6",
"@expo/webpack-config": "^0.17.2",
"@expo/webpack-config": "^19.0.1",
"@iconify/react": "^4.0.0",
"@testing-library/dom": "^8.19.0",
"@testing-library/jest-dom": "^5.16.5",
Expand All @@ -53,7 +53,7 @@
"html-webpack-plugin": "^5.6.0",
"i": "^0.3.7",
"node-polyfill-webpack-plugin": "^2.0.1",
"npm": "^8.19.2",
"npm": "^10.8.3",
"postcss": "^8.4.14",
"postcss-loader": "^7.0.0",
"process": "^0.11.10",
Expand Down Expand Up @@ -83,7 +83,7 @@
"@mui/x-date-pickers": "^5.0.6",
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0",
"@tinymce/tinymce-react": "^4.2.0",
"@tinymce/tinymce-react": "^5.1.1",
"@types/react-i18next": "^8.1.0",
"@types/react-router": "^5.1.19",
"@types/react-router-dom": "^5.3.3",
Expand Down Expand Up @@ -120,14 +120,14 @@
"react-hot-toast": "^2.4.1",
"react-i18next": "^11.18.6",
"react-icons": "^4.6.0",
"react-js-pagination": "^3.0.3",
"react-js-pagination": "^3.0.2",
"react-loader-spinner": "^6.1.6",
"react-modal": "^3.16.1",
"react-paginate": "^8.1.3",
"react-redux": "^8.0.4",
"react-render-html": "^0.6.0",
"react-router-dom": "^6.4.2",
"react-scripts": "^5.0.1",
"react-scripts": "^3.0.1",
"react-select": "^5.7.4",
"react-table": "^7.8.0",
"react-toastify": "^9.0.8",
Expand Down
9 changes: 9 additions & 0 deletions src/assets/assets/calendar.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/assets/collaborative-learning.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/assets/performance.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions src/assets/assets/strategy.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions src/components/form/SignInForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,13 @@ const LoginForm = () => {
const response = await loginAction(validatedData.email, validatedData.password);

const token = response?.data?.data?.login?.token;
const userId = response?.data?.data?.login?.userId;

if (token) {
localStorage.setItem("access_token", token);
if (userId) {
localStorage.setItem("userId", userId);
}
await redirectAfterLogin();
} else {
toast.error(response?.data?.errors[0].message);
Expand Down
5 changes: 5 additions & 0 deletions src/components/sidebar/sidebarItems.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ export const sidebarItems1 = [
];

export const applicantSidebarItems = [
{
path: "/applicant",
icon: <Icon icon="fontisto:pie-chart-1"></Icon>,
title: "Dashboard",
},
{
path: "myApplications",
icon: <Icon icon="material-symbols:wysiwyg-rounded"></Icon>,
Expand Down
295 changes: 295 additions & 0 deletions src/pages/Applicant/ApplicantDashboard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
import React from "react";
import { useState, useEffect } from "react";
import NavBar from "components/sidebar/navHeader";
import { getTraineeApplicant } from "../../redux/actions/TraineeAction";
import { getTraineeAttendance } from "../../redux/actions/attendanceAction";
import { getTraineePerformance } from "../../redux/actions/performanceAction";
import { getTraineeByUserId } from "../../redux/actions/TraineeAction";
import { getCohort } from "../../redux/actions/cohortActions";
import { useDispatch, useSelector } from "react-redux";
import {
Area,
XAxis,
YAxis,
Tooltip,
ResponsiveContainer,
Scatter,
ComposedChart,
Label,
Line,
AreaChart,
CartesianGrid
} from "recharts";

const calendar: string = require("../../assets/assets/calendar.svg").default;
const collaboration: string =
require("../../assets/assets/collaborative-learning.svg").default;
const performanceIcon: string =
require("../../assets/assets/performance.svg").default;
const strategy: string = require("../../assets/assets/strategy.svg").default;

const DashboardCard = ({ title, value, img }) => {
return (
<>
<div className="bg-gray-300 dark:bg-gray-800 rounded-lg p-4 flex items-center justify-between border border-[#DFE3E8] flex-1 min-w-[250px] ">
<div className="flex items-center">
<div className="bg-gray-100 dark:bg-gray-700 p-2 rounded-lg mr-3">
<img src={img} alt={title} />
</div>
<div>
<h2 className="text-gray-600 dark:text-gray-300 text-lg font-medium">{title}</h2>
<p className="text-gray-900 dark:text-white text-2xl font-bold">{value}</p>
</div>
</div>
</div>
</>
);
};

const ApplicantChart = ({ performance }) => {
const transformPerformanceData = (performanceData) => {
if (!performanceData?.performances) return [];

const sortedPerformances = [...performanceData.performances].sort((a, b) => {
return parseInt(a.date) - parseInt(b.date);
});


return sortedPerformances.map((performance, index) => ({
name: index + 1,
score: performance.score || 0,

}));

};

const chartData = transformPerformanceData(performance);

if (chartData.length === 0) {
return <div className="text-gray-900 dark:text-white ">No performance data available</div>;
}



return (
<ResponsiveContainer width="100%" height={300}>
{chartData.length === 1 ? (
<ComposedChart
data={chartData}
margin={{
top: 10,
right: 30,
left: 0,
bottom: 20,
}}
>
<XAxis
dataKey="name"
tick={{ fill: "white" }}
tickLine={false}
axisLine={false}
>
<Label value="Sprints" position="insideBottom" fill="white" />
</XAxis>

<YAxis
domain={[0, 100]}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
>
<Label value="Performance" angle={-90} position="insideLeft" fill="white" />
</YAxis>
<Tooltip />
<Scatter name="Performance" dataKey="score" fill="#56C870" />
</ComposedChart>
) : (
<AreaChart data={chartData} margin={{
top: 10,
right: 30,
left: 0,
bottom: 20,
}}>
<CartesianGrid vertical={false} stroke="#374151" />
<XAxis
dataKey="name"
tick={{ fill: "white" }}
tickLine={false}
axisLine={false}
>
<Label value="Sprints" offset={-10} position="insideBottom" fill="white" />
</XAxis>
<YAxis
domain={[0, 100]}
tick={{ fill: "white" }}
axisLine={false}
tickLine={false}
>
<Label value="Performance" angle={-90} position="insideLeft" fill="white" />
</YAxis>
<Tooltip />
<defs>
<linearGradient id="colorUv" x1="0" y1="0" x2="0" y2="1">
<stop offset="1%" stopColor="#243A3D" stopOpacity={1} />
<stop offset="99%" stopColor="#243A3D" stopOpacity={0} />
</linearGradient>
</defs>
<Area
type="monotone"
dataKey="score"
stroke="#56C870"
fillOpacity={1}
fill="url(#colorUv)"
/>
<Line
type="monotone"
dataKey="score"
stroke="#56C870"
strokeWidth={4}
dot={false}
/>
</AreaChart>
)}
</ResponsiveContainer>
);
}


const useTraineeData = () => {
const dispatch = useDispatch();
const traineeId = useSelector((state: any) => state.traineeApplicant.currentTrainee);
const traineeData = useSelector((state: any) => state.traineeApplicant.data);
const attendance = useSelector((state: any) => state.traineeAttendance);
const performance = useSelector((state: any) => state.traineePerformance);
const cohort = useSelector((state: any) => state.cohorts);

useEffect(() => {
const userId = localStorage.getItem("userId");
if (userId) {
dispatch(getTraineeByUserId(userId));
}
}, [dispatch]);

useEffect(() => {
const fetchTraineeData = async () => {
if (!traineeId) return;
try {
await dispatch(getTraineeApplicant(traineeId));
await dispatch(getTraineeAttendance(traineeId));
await dispatch(getTraineePerformance(traineeId));
} catch (err) {
console.error("Failed to fetch trainee details:", err);
}
};
fetchTraineeData();
}, [dispatch, traineeId]);

useEffect(() => {
const fetchCohortData = async () => {
if (traineeData && traineeData.cohort) {
try {
await dispatch(getCohort(traineeData.cohort));
} catch (err) {
console.error("Failed to fetch cohort data:", err);
}
}
};
fetchCohortData();
}, [dispatch, traineeData]);


return { traineeData, attendance, performance, cohort };

}

const formatters = {
extractCohortNumber: (cohortTitle: string): string => {
if (!cohortTitle) return "N/A";
const match = cohortTitle.match(/\d+/);
return match ? match[0] : "N/A";
},

formatPercentage: (value: number | string | undefined): string => {
if (typeof value === 'number') {
return value % 1 === 0 ? `${value}%` : `${value.toFixed(1)}%`;
}
return 'N/A';
},

calculateAttendancePercentage: (ratio: string | undefined): string => {
if (!ratio) return 'N/A';
const [attended, total] = ratio.split('/').map(Number);
if (isNaN(attended) || isNaN(total) || total === 0) return 'N/A';
return formatters.formatPercentage(attended / total * 100);
}
}

const ApplicantDashboard = (props: any) => {

const { cohort, performance, attendance } = useTraineeData();
const cohortData = cohort?.traineeCohort || {};
const { extractCohortNumber, formatPercentage, calculateAttendancePercentage } = formatters;

const dashboardData = [
{
title: "Cohort",
value: cohortData.title ? extractCohortNumber(cohortData.title) : "N/A",
img: collaboration,
},
{
title: "Current Phase",
value: cohortData.phase || "N/A",
img: strategy,
},
{
title: "Performance",
value: formatPercentage(performance?.averageScore),
img: performanceIcon,
},
{
title: "Attendance",
value: calculateAttendancePercentage(attendance?.attendanceRatio),
img: calendar,
},
];

return (
<>
<div className="bg-white dark:bg-[#374151] w-full min-h-screen p-8">
<div>
<div className="mt-6">
<div>
<div className="flex flex-wrap gap-4">
{dashboardData.map((item, index) => (
<DashboardCard
key={index}
title={item.title}
value={item.value}
img={item.img}
/>
))}
</div>
<div className="w-full border border-slate-200 py-10 px-7 rounded-2xl dark:bg-dark-bg bg-white mt-10">
<div className="w-full flex flex-col mb-8">
<h2 className="text-gray-900 dark:text-white font-medium text-xl">
My overall performance
</h2>
<p className="text-gray-900 dark:text-white text-sm">
As of {new Date().toLocaleDateString('en-US', {
day: 'numeric',
month: 'long',
year: 'numeric'
})}
</p>
</div>
<ApplicantChart performance={performance} />
</div>
</div>
</div>
</div>
</div>
</>
);
};

export default ApplicantDashboard;
Loading

0 comments on commit 78b4b3b

Please sign in to comment.