From e30941c86d83aedb0db3df72df199e89e87dba82 Mon Sep 17 00:00:00 2001 From: MUREKEZI Ismael <110295158+Ismaelmurekezi@users.noreply.github.com> Date: Thu, 26 Sep 2024 08:46:47 +0200 Subject: [PATCH] #40 Fix single trainee detail page (#125) * feature: improve trainee details page * handling missing application info, also adding download functionality * fixing error related to download and refactoring * Update TrainneeDetails.tsx * handling issues related to deployment * Fix number can't be shared (#130) Co-authored-by: Mugisha * #102 sidebar links review (#128) * fix: remove placeholder property * fix duplicate links --------- Co-authored-by: ceelogre * Ft minimize dashboard menu #110 (#140) * fix: remove placeholder property * ft minimize dashboard menu * fix minimize dashboard by icon and categorize into section * fix minimize dashboard by icon and categorize into section * fix minimize dashboard by icon and categorize into section * fix minimize dashboard by icon and categorize into section * fix minimize dashboard by icon and categorize into section * fix minimize dashboard by icon and categorize into section * fix minimize dashboard scrollbar * fix minimize dashboard scrollbar * fix minimize dashboard scrollbar * Fix layout spacing between sidebar and main content in AdminLayout * new * Fix layout spacing between sidebar and main content in AdminLayout * fix layout --------- Co-authored-by: ceelogre Co-authored-by: Prince-Kid Co-authored-by: Mucyo Prince <134399659+Prince-Kid@users.noreply.github.com> Co-authored-by: Aime-Patrick * #118 fx: builtinSuperAdminCreateProgram (#126) * fix: remove placeholder property * The built-in superadmin account cannot create a program --------- Co-authored-by: ceelogre * feature: improve trainee details page * handling missing application info, also adding download functionality * Update TrainneeDetails.tsx * adding way to send email and other adjustments * refining and fixing some issues * Update webpack.config.js * customizing way of sending email * refactoring code to fix issue related to code climate * fixing issue for deployment * fixing issues related to refactoring --------- Co-authored-by: MUGISHA Emmanuel Co-authored-by: Mugisha Co-authored-by: ISHIMWE Jean Baptiste Co-authored-by: ceelogre Co-authored-by: ManziPatrick <144239912+ManziPatrick@users.noreply.github.com> Co-authored-by: Prince-Kid Co-authored-by: Mucyo Prince <134399659+Prince-Kid@users.noreply.github.com> Co-authored-by: Aime-Patrick Co-authored-by: Niyonshuti Jean De Dieu <152473876+Jadowacu1@users.noreply.github.com> --- .gitignore | 3 +- package.json | 9 +- src/components/TraineeDetail/DetailItem.tsx | 19 + src/components/TraineeDetail/ProgramBox.tsx | 23 + .../TraineeDetail/decisionSection.tsx | 79 ++++ src/components/sidebar/sidebarItems.tsx | 5 + src/pages/TraineApplicant/Trainee.tsx | 2 +- src/pages/TrainneeDetails.tsx | 434 ++++++------------ src/redux/actions/axiosconfig.ts | 5 +- src/redux/actions/trainnee.ts | 6 + src/utils/DownloadPdf.tsx | 50 ++ webpack.config.js | 134 +++--- 12 files changed, 392 insertions(+), 377 deletions(-) create mode 100644 src/components/TraineeDetail/DetailItem.tsx create mode 100644 src/components/TraineeDetail/ProgramBox.tsx create mode 100644 src/components/TraineeDetail/decisionSection.tsx create mode 100644 src/utils/DownloadPdf.tsx diff --git a/.gitignore b/.gitignore index 8c808a8b..db5330e1 100755 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ coverage dist buildcoverage package-lock.json -.DS_Store \ No newline at end of file +.DS_Store +build/ \ No newline at end of file diff --git a/package.json b/package.json index 5fa77c40..1b7c6691 100755 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "index.js", "scripts": { - "start": "echo \"Error: no test specified\" && exit 1", + "start": "webpack server", "dev": "webpack server", "test": "jest --watchAll=false", "build": "webpack build", @@ -74,6 +74,7 @@ "@emotion/styled": "^11.10.4", "@fortawesome/fontawesome-svg-core": "^6.4.2", "@fortawesome/free-regular-svg-icons": "^6.4.2", + "@fortawesome/free-solid-svg-icons": "^6.2.0", "@fortawesome/react-fontawesome": "^0.2.0", "@heroicons/react": "^1.0.6", "@hookform/resolvers": "^3.3.0", @@ -89,6 +90,7 @@ "bootstrap": "^5.2.2", "browser": "^0.2.6", "date-fns": "^2.29.3", + "dayjs": "^1.11.6", "dotenv": "^16.0.3", "express": "^4.21.0", "flowbite": "^1.5.3", @@ -97,11 +99,13 @@ "googleapis": "^126.0.1", "graphql": "^16.6.0", "graphql-request": "^5.1.0", + "html2canvas": "^1.4.1", "icons": "^1.0.0", "jest": "^29.1.2", "jest-environment-jsdom": "^29.1.2", "joi": "^17.10.2", "jquery": "^3.6.1", + "jspdf": "^2.5.2", "jwt-decode": "^3.1.2", "mini-css-extract-plugin": "^2.6.1", "moment": "^2.29.4", @@ -123,7 +127,8 @@ "react-scripts": "^5.0.1", "react-select": "^5.7.4", "react-table": "^7.8.0", - "react-toastify": "^9.1.3", + "react-toastify": "^9.0.8", + "redux": "^4.2.0", "redux-devtools-extension": "^2.13.9", "redux-state-sync": "^3.1.4", "redux-thunk": "^2.4.1", diff --git a/src/components/TraineeDetail/DetailItem.tsx b/src/components/TraineeDetail/DetailItem.tsx new file mode 100644 index 00000000..41fd07f8 --- /dev/null +++ b/src/components/TraineeDetail/DetailItem.tsx @@ -0,0 +1,19 @@ +import React from "react"; + +interface DetailItemProps { + title: string; + value: string | number | boolean; +} + +const DetailItem: React.FC = ({ title, value }) => { + return ( +
+

{title}

+

+ {String(value)} +

+
+ ); +}; + +export default DetailItem; diff --git a/src/components/TraineeDetail/ProgramBox.tsx b/src/components/TraineeDetail/ProgramBox.tsx new file mode 100644 index 00000000..5bff7ff8 --- /dev/null +++ b/src/components/TraineeDetail/ProgramBox.tsx @@ -0,0 +1,23 @@ +import React from "react"; + +import { IconType } from "react-icons"; + +interface ProgramItemProps { + title: string; + value: string | number | boolean; + Icon: IconType; +} + +const ProgramItem: React.FC = ({ title, value,Icon }) => { + return ( +
+ +
+

{title}

+

{value}

+
+
+ ); +}; + +export default ProgramItem; diff --git a/src/components/TraineeDetail/decisionSection.tsx b/src/components/TraineeDetail/decisionSection.tsx new file mode 100644 index 00000000..da9cb303 --- /dev/null +++ b/src/components/TraineeDetail/decisionSection.tsx @@ -0,0 +1,79 @@ +import React from "react"; +import { DownloadPdf } from "../../utils/DownloadPdf"; + +interface TraineeDetails { + interview_decision: string; + trainee_id: { + email: string; + lastName: string; + firstName: string; + }; +} + +interface DecisionButtonProps { + decision: string; +} + +const getDecisionDetails = (decision: string) => { + switch (decision) { + case "Passed": + case "Approved": + return { + text: "Passed", + style: "bg-[#56C870] hover:bg-[#67dc82] dark:hover:bg-[#1f544cef] dark:bg-[#56C870]", + }; + case "Failed": + case "Rejected": + return { + text: "Failed", + style: "bg-red-800 hover:bg-red-500", + }; + default: + return { + text: "No Decision", + style: "bg-gray-400", + }; + } +}; + +const DecisionButton: React.FC = ({ decision }) => { + const { text, style } = getDecisionDetails(decision); + + return ( + + {text} + + ); +}; + +const DecisionSection: React.FC<{ traineeDetails: TraineeDetails }> = ({ traineeDetails }) => { + const { interview_decision, trainee_id } = traineeDetails; + + return ( +
+

+ Status +

+
+ + + +
+
+ ); +}; + +export default DecisionSection; \ No newline at end of file diff --git a/src/components/sidebar/sidebarItems.tsx b/src/components/sidebar/sidebarItems.tsx index c3484b9a..7a0b5ff7 100644 --- a/src/components/sidebar/sidebarItems.tsx +++ b/src/components/sidebar/sidebarItems.tsx @@ -34,6 +34,11 @@ export const sidebarItems1 = [ icon: , title: "Applications", }, + { + path: "Trainee-applicants", + icon: , + title: "Trainees-Applicants", + }, { path: "cohort", icon: , diff --git a/src/pages/TraineApplicant/Trainee.tsx b/src/pages/TraineApplicant/Trainee.tsx index 0b2fe239..5ff3638e 100755 --- a/src/pages/TraineApplicant/Trainee.tsx +++ b/src/pages/TraineApplicant/Trainee.tsx @@ -354,7 +354,7 @@ const AddTrainee = (props: any) => {
  • View diff --git a/src/pages/TrainneeDetails.tsx b/src/pages/TrainneeDetails.tsx index a3ddfbb7..e06d42db 100644 --- a/src/pages/TrainneeDetails.tsx +++ b/src/pages/TrainneeDetails.tsx @@ -1,336 +1,164 @@ -import React, { useState, useEffect } from "react"; -import { BsEnvelope } from "react-icons/bs"; -import { TiExportOutline } from "react-icons/ti"; -import { FcApproval } from "react-icons/fc"; -import { AiFillSetting, AiFillCaretDown } from "react-icons/ai"; -import { MdOutlineCancel } from "react-icons/md"; -import { BsFillPersonLinesFill } from "react-icons/bs"; -import Sidebar from "../components/sidebar/sidebar"; +import { useState, useEffect } from "react"; +import { LuCalendarDays } from "react-icons/lu"; +import { FaRecycle } from "react-icons/fa6"; import { getOneTraineeAllDetails } from "../redux/actions/trainnee"; import { connect } from "react-redux"; -import Navbar from "./../components/sidebar/navHeader"; import { useParams } from "react-router"; -import { - getAllScoreValues, - updateManyScoreValues, -} from "../redux/actions/scoreValueActions"; -import { toast } from "react-toastify"; +import DetailItem from "../components/TraineeDetail/DetailItem"; +import ProgramItem from "../components/TraineeDetail/ProgramBox"; +import DecisionSection from "../components/TraineeDetail/decisionSection"; const TrainneeDetails = (props: any) => { const params = useParams(); const [key, setKey] = useState(params.traineeId); - const { oneTraineeDetails, scoreValues } = props; - - const urlId = window.location.href.substring( - window.location.href.lastIndexOf("/") + 1 - ); - - const availableScores = scoreValues.data?.filter((values: any) => { - return values.attr_id?.trainee_id._id == urlId; - }); - const [changed, setChanged] = useState(false); - - const arr = availableScores?.map((values: any) => { - // return values.score_value; - return { - id: values.id, - test: values.score_id.score_type, - score_value: values.score_value, - }; - }); - - const [score_value, setscore_value] = useState(); - - const values = availableScores?.map((values: any) => { - return { test: values.score_id.score_type, value: values.score_value }; - }); - - const attrValues = availableScores?.map((values: any) => { - return { - attr_id: values.attr_id._id, - id: values.id, - score_id: values.score_id.id, - score_value: values.score_value, - }; - }); + const { oneTraineeDetails } = props; const [ID, setId] = useState(key); - const [open, setOpen] = useState(false); - const handleDropDown = (state: boolean) => { - setOpen(!state); - }; - - let input = { - id: ID, - }; - useEffect(() => { - setscore_value(arr); - }, [scoreValues]); - useEffect(() => { - props.getOneTraineeAllDetails(input); - props.getAllScoreValues(); - }, []); + props.getOneTraineeAllDetails({ id: ID }); + }, [ID]); const traineeDetails = oneTraineeDetails.data; - const updateManyScoreValues = () => { - const tsabus = score_value.map((values: any) => { - delete values.test; - return values; - }); - props.updateManyScoreValues(tsabus); - }; - return ( <> - -
    - {/*
    */} -
    - {traineeDetails && ( -
    -
    -

    - - Trainee Applicant Information -

    - -
    - {traineeDetails?.trainee_id && ( - <> - {" "} -

    FirstName

    -

    - {traineeDetails?.trainee_id.firstName} -

    - - )} -

    Gender

    -

    - {traineeDetails.gender} -

    -

    Address

    -

    - {traineeDetails.Address} -

    -

    Phone Number

    -

    - {traineeDetails.phone} -

    -

    Field of Study

    -

    - {traineeDetails.field_of_study} -

    -

    Education Level

    -

    - {traineeDetails.education_level} -

    -

    Is Employed

    -

    - {String(traineeDetails.isEmployed)} -

    - {traineeDetails?.trainee_id && ( - <> -

    Email

    -

    - {traineeDetails?.trainee_id.email} -

    - - )} +
    +
    +
    + {traineeDetails && ( +
    +
    +

    + + Trainee Applicant Information + +

    +
    + + + + + + + + + +
    -
    -
    - {traineeDetails?.trainee_id && ( - <> -

    LastName

    -

    - {traineeDetails?.trainee_id.lastName} -

    - - )} -

    Province

    -

    - {traineeDetails.province} -

    -

    District

    -

    - {traineeDetails.district} -

    -

    Sector

    -

    - {traineeDetails.sector} -

    -

    Is Student

    -

    - {String(traineeDetails.isStudent)} -

    -

    Hackerrank Score

    -

    - {traineeDetails.Hackerrank_score} -

    -

    English Score

    -

    - {traineeDetails.english_score} -

    -

    Date of Bith

    -

    - {traineeDetails.birth_date} -

    -
    -
    - )} -
    -
    -

    - - User ratings -

    -
    - {score_value?.map((values: any, idx: number) => { - return ( -
    -
    - -
    -
    - { - setChanged(true); - setscore_value((values) => { - const newValue = [...values]; - newValue[idx] = { - ...newValue[idx], - score_value: e.target.value, - }; - return newValue; - }); - }} - /> -
    -
    - ); - })} -
    - {changed && ( -
    - - +
    + + + + + + + +
    - )} - -
    -
    -
    -

    - - Application Information -

    -
    -

    Application Phase

    -

    - Initial Phase -

    -

    Program

    -

    - {" "} - Andela Technical Leadership Program -

    -
    -
    -
    -

    Application Date

    -

    - Initial Phase -

    -

    Expected program start date

    -

    - {" "} - 08/01/2022 -

    + )} + + {traineeDetails && ( +
    +
    +

    + + Application Information + +

    +
    + + + +
    +
    +
    +
    + + +
    +
    -
    -
    {" "} -
    -

    - - Actions -

    -
    - - - {/*
    */} - - {/*
    */} - - - -
    + )}
    + +
    - {/*
    */}
    ); }; -// export default TrainneeDetails + const mapState = (state: any) => ({ oneTraineeDetails: state.traineeAllDetails, - scoreValues: state.scoreValues, }); export default connect(mapState, { getOneTraineeAllDetails, - getAllScoreValues, - updateManyScoreValues, })(TrainneeDetails); diff --git a/src/redux/actions/axiosconfig.ts b/src/redux/actions/axiosconfig.ts index 7a1bcc9e..aba17356 100755 --- a/src/redux/actions/axiosconfig.ts +++ b/src/redux/actions/axiosconfig.ts @@ -3,11 +3,13 @@ import axios from "axios"; const config = axios.create({ baseURL: process.env.BACKEND_URL, }); + config.interceptors.request.use( (config) => { const token = localStorage.getItem("access_token"); if (token) { - config.headers["Authorization"] = `${token}`; + config.headers = config.headers || {}; + config.headers["Authorization"] = `Bearer ${token}`; } return config; }, @@ -15,6 +17,7 @@ config.interceptors.request.use( return Promise.reject(error); } ); + export default config; export function logout() { localStorage.removeItem("access_token"); diff --git a/src/redux/actions/trainnee.ts b/src/redux/actions/trainnee.ts index cfbe882a..56a2e834 100644 --- a/src/redux/actions/trainnee.ts +++ b/src/redux/actions/trainnee.ts @@ -36,6 +36,12 @@ export const getOneTraineeAllDetails = firstName _id email + cycle_id { + id + name + startDate + endDate + } } } diff --git a/src/utils/DownloadPdf.tsx b/src/utils/DownloadPdf.tsx new file mode 100644 index 00000000..2f10b3c1 --- /dev/null +++ b/src/utils/DownloadPdf.tsx @@ -0,0 +1,50 @@ +import html2canvas from "html2canvas"; +import jsPDF from "jspdf"; + +export const DownloadPdf = () => { + const element = document.getElementById("trainee-info"); + + if (element) { + + const clonedElement = element.cloneNode(true) as HTMLElement; + + clonedElement.style.position = "absolute"; + clonedElement.style.top = "-9999px"; + document.body.appendChild(clonedElement); + + html2canvas(clonedElement, { + scale: 1.2, + useCORS: true, + logging: true, + backgroundColor: null, + imageTimeout: 0, + }).then((canvas) => { + const imgData = canvas.toDataURL("image/png", 1.0); + + const pdf = new jsPDF({ + orientation: "portrait", + unit: "pt", + format: [canvas.width, canvas.height], + }); + + const pdfWidth = pdf.internal.pageSize.getWidth(); + const pdfHeight = pdf.internal.pageSize.getHeight(); + const padding = 20; + + pdf.addImage( + imgData, + "PNG", + padding, + padding, + pdfWidth - padding * 2, + pdfHeight - padding * 2 + ); + pdf.save("trainee-info.pdf"); + + // Remove the cloned element from the DOM after download + document.body.removeChild(clonedElement); + }); + } else { + console.error("Element with id 'trainee-info' not found."); + } +}; diff --git a/webpack.config.js b/webpack.config.js index de1e90ef..6cfa7f37 100755 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,82 +1,78 @@ const HtmlWebpackPlugin = require("html-webpack-plugin"); const MiniCssExtractPlugin = require("mini-css-extract-plugin"); -const CopyWebpackPlugin = require('copy-webpack-plugin'); const path = require("path"); const dotenv = require("dotenv"); const webpack = require("webpack"); -dotenv.config(); - -const prod = process.env.NODE_ENV === "production"; - -module.exports = { - mode: prod ? "production" : "development", - entry: "./src/index.tsx", - output: { - path: path.resolve(__dirname, "build/"), - filename: "bundle.js", - publicPath: "/", - }, - devServer: { - historyApiFallback: true, - port: 3000, - compress: true, - allowedHosts: "all", - }, - resolve: { - extensions: [".js", ".json", ".tsx", ".ts", ".svg"], - fallback: { - zlib: require.resolve("browserify-zlib"), - https: require.resolve("https-browserify"), - http: require.resolve("stream-http"), +module.exports = () => { + dotenv.config(); + const prod = process.env.NODE_ENV === "production"; + return { + mode: prod ? "production" : "development", + entry: "./src/index.tsx", + output: { + path: path.resolve(__dirname, "build/"), + publicPath: "/", }, - alias: { - process: "process/browser", - stream: "stream-browserify", + devServer: { + historyApiFallback: true, + port: 3000, + compress: true, + allowedHosts: ["all"], }, - }, - module: { - rules: [ - { - test: /\.(tsx|ts)$/, - exclude: /node_modules/, - use: "ts-loader", + resolve: { + fallback: { + zlib: require.resolve("browserify-zlib"), + https: require.resolve("https-browserify"), + http: require.resolve("stream-http"), }, - { - test: /\.css$/, - use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"], + alias: { + process: "process/browser", + stream: "stream-browserify", }, - { - test: /\.(png|jpe?g|svg|gif)$/, - use: [ - { - loader: 'file-loader', - options: { - name: 'assets/[name].[hash].[ext]', - outputPath: 'assets/', - publicPath: 'assets/', - }, - }, - ], + alias: { + process: "process/browser", + stream: "stream-browserify", }, - ], - }, - devtool: prod ? undefined : "source-map", - plugins: [ - new HtmlWebpackPlugin({ - template: path.resolve(__dirname, "index.html"), - }), - new MiniCssExtractPlugin(), - new webpack.DefinePlugin({ - "process.env": JSON.stringify(process.env), - }), - new webpack.ProvidePlugin({ - process: "process/browser", - }), - new CopyWebpackPlugin({ - patterns: [ - { from: 'src/assets/', to: 'assets/' }, + }, + module: { + rules: [ + { + test: /\.(tsx|ts)$/, + exclude: /node_modules/, + resolve: { + extensions: [".js", ".json", ".tsx", ".ts", ".svg"], + }, + use: "ts-loader", + }, + { + test: /\.css$/, + use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"], + }, + { + test: /\.(png|jp(e*)g|svg|gif)$/, + use: ["file-loader"], + }, + { + test: /\.m?js$/, + resolve: { + fullySpecified: false, + }, + }, ], - }), - ], + }, + devtool: prod ? undefined : "source-map", + plugins: [ + new HtmlWebpackPlugin({ + template: "index.html", + }), + new MiniCssExtractPlugin(), + new webpack.DefinePlugin({ + "process.env": JSON.stringify(process.env), + }), + new webpack.ProvidePlugin({ + process: "process/browser", + }), + ], + }; };