From 05e6f79d071ba079583c9f8daefd3dc581e20163 Mon Sep 17 00:00:00 2001 From: Ciprian Platica Date: Tue, 24 Oct 2023 18:10:03 +0300 Subject: [PATCH] Show confirmation before sending mentorship request (#84) * Show confirmation before sending mentorship request * Fix tests * Show toast after sending email * OOps * Add report page to mentor * Get the last reportId when sending request to mentor --- client/src/components/Button/index.tsx | 21 +++- client/src/components/Confirm/index.tsx | 65 +++++----- client/src/components/Navbar/index.tsx | 5 + .../components/ResultsByDimension/index.tsx | 2 +- client/src/lib/score.ts | 2 +- .../authenticated/MentorsList/MentorCard.tsx | 118 +++++++++++++----- .../authenticated/Report/ReportInProgress.tsx | 10 +- .../src/pages/mentor/Report/ReportResults.tsx | 52 ++++++++ client/src/pages/mentor/Report/index.tsx | 64 ++++++++++ client/src/redux/api/userApi.ts | 9 +- client/src/redux/features/userSlice.ts | 4 + client/src/router/index.tsx | 9 +- .../createReport/createReport.spec.ts | 8 +- .../controllers/mentorship-request.js | 8 +- 14 files changed, 297 insertions(+), 80 deletions(-) create mode 100644 client/src/pages/mentor/Report/ReportResults.tsx create mode 100644 client/src/pages/mentor/Report/index.tsx diff --git a/client/src/components/Button/index.tsx b/client/src/components/Button/index.tsx index 7654a2f..cf24b02 100644 --- a/client/src/components/Button/index.tsx +++ b/client/src/components/Button/index.tsx @@ -7,15 +7,23 @@ interface IButton { onClick?: () => void; type?: "submit" | "button"; color?: "teal" | "white"; + disabled?: boolean; } const variation = { - teal: "rounded bg-teal-600 py-2 px-4 font-medium text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600", + teal: "whitespace-nowrap rounded bg-teal-600 py-2 px-4 font-medium text-white shadow-sm hover:bg-teal-500 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600", white: - "rounded bg-white py-2 px-4 font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50", + "whitespace-nowrap rounded bg-white py-2 px-4 font-medium text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50", }; -const Button = ({ children, to, onClick, type, color = "teal" }: IButton) => { +const Button = ({ + children, + to, + onClick, + type, + color = "teal", + disabled, +}: IButton) => { const className = variation[color]; return to ? ( @@ -23,7 +31,12 @@ const Button = ({ children, to, onClick, type, color = "teal" }: IButton) => { {children} ) : ( - ); diff --git a/client/src/components/Confirm/index.tsx b/client/src/components/Confirm/index.tsx index d2deb6c..de6a470 100644 --- a/client/src/components/Confirm/index.tsx +++ b/client/src/components/Confirm/index.tsx @@ -1,7 +1,15 @@ import { Fragment, useRef } from "react"; import { Dialog, Transition } from "@headlessui/react"; -const Confirm = ({ open, setOpen, handleComplete }) => { +const Confirm = ({ + header, + body, + footer, + buttonText, + open, + setOpen, + handleComplete, +}) => { const cancelButtonRef = useRef(null); return ( @@ -25,7 +33,7 @@ const Confirm = ({ open, setOpen, handleComplete }) => {
-
+
{ >
-
+
- Ești sigur că vrei să finalizezi evaluarea? + {header}
-

- Dacă finalizezi acum, persoanele care nu au răspuns la - chestionarul de evaluare nu vor putea să mai completeze. - Asigură-te că ai toate răspunsurile înainte de a face - această acțiune -

+

{body}

- - + {footer ? ( + footer + ) : ( + <> + + + + )}
diff --git a/client/src/components/Navbar/index.tsx b/client/src/components/Navbar/index.tsx index 4c50830..e9ea45d 100644 --- a/client/src/components/Navbar/index.tsx +++ b/client/src/components/Navbar/index.tsx @@ -134,6 +134,11 @@ const Menu = () => { {menu.map((menuItem) => (
  • `flex flex-wrap border-b-2 border-transparent px-3 py-2 font-medium items-center text-gray-900 border-teal-600 ${ diff --git a/client/src/components/ResultsByDimension/index.tsx b/client/src/components/ResultsByDimension/index.tsx index 627e0c0..db80f65 100644 --- a/client/src/components/ResultsByDimension/index.tsx +++ b/client/src/components/ResultsByDimension/index.tsx @@ -7,7 +7,7 @@ const ResultsByDimension = ({ scoreByEvaluation, }: { scoreByEvaluation: { - id: number; + id: string; name: string; score: number; tags: string[]; diff --git a/client/src/lib/score.ts b/client/src/lib/score.ts index 0a1d6e3..a2a5cc5 100644 --- a/client/src/lib/score.ts +++ b/client/src/lib/score.ts @@ -55,7 +55,7 @@ export const calcScoreByDimension = ({ matrix: Matrix; }) => { if (!matrix) { - return null; + return undefined; } const object = evaluationsCompleted.reduce( (acc, evaluation) => diff --git a/client/src/pages/authenticated/MentorsList/MentorCard.tsx b/client/src/pages/authenticated/MentorsList/MentorCard.tsx index 8df43df..92d46fd 100644 --- a/client/src/pages/authenticated/MentorsList/MentorCard.tsx +++ b/client/src/pages/authenticated/MentorsList/MentorCard.tsx @@ -1,11 +1,16 @@ -import React from "react"; +import React, { useEffect, useState } from "react"; import { EnvelopeIcon, SignalSlashIcon, UserIcon, } from "@heroicons/react/20/solid"; -import { Link } from "react-router-dom"; import { useCreateMentorshipRequestMutation } from "@/redux/api/userApi"; +import Confirm from "@/components/Confirm"; +import { useAppSelector } from "@/redux/store"; +import { selectHasFinishedReports } from "@/redux/features/userSlice"; +import Button from "@/components/Button"; +import { toast } from "react-toastify"; +import { Dimension } from "@/redux/api/types"; const MentorCard = ({ id, @@ -14,52 +19,99 @@ const MentorCard = ({ lastName, dimensions, available, +}: { + id: string; + userId: string; + firstName: string; + lastName: string; + dimensions: Dimension[]; + available: boolean; }) => { - const [createMentorshipRequest] = useCreateMentorshipRequestMutation(); - + const [openConfirm, setOpenConfirm] = useState(false); + const [createMentorshipRequest, { isSuccess, isError }] = + useCreateMentorshipRequestMutation(); + const hasFinishedReports = useAppSelector(selectHasFinishedReports); const handleClickEmail = () => { - createMentorshipRequest({ mentor: +id, user: +userId }); + setOpenConfirm(true); }; + useEffect(() => { + if (isSuccess) { + toast.success("Trimis cu succes"); + } + }, [isSuccess]); + + useEffect(() => { + if (isError) { + toast.error("Această acțiune nu a putut fi realizată"); + } + }, [isError]); + return (
  • + + + + + } + />

    {firstName} {lastName}

    - {dimensions?.map((dimension) => dimension.name).join("; ")} + {dimensions?.map((dimension) => dimension?.name).join("; ")}

    -
      -
    • - + + {available ? ( +
    • -
    • - {available ? ( -
      - LinkedIn - - Trimite email -
      - ) : ( -
      - - Indisponibil -
      - )} -
    • + LinkedIn + + Trimite email + + ) : ( + + )}
  • ); diff --git a/client/src/pages/authenticated/Report/ReportInProgress.tsx b/client/src/pages/authenticated/Report/ReportInProgress.tsx index 55f2d9f..e4893bc 100644 --- a/client/src/pages/authenticated/Report/ReportInProgress.tsx +++ b/client/src/pages/authenticated/Report/ReportInProgress.tsx @@ -16,7 +16,15 @@ const CallToAction = ({ reportId }: { reportId: number }) => { return ( <> - + ); diff --git a/client/src/pages/mentor/Report/ReportResults.tsx b/client/src/pages/mentor/Report/ReportResults.tsx new file mode 100644 index 0000000..e85d8b6 --- /dev/null +++ b/client/src/pages/mentor/Report/ReportResults.tsx @@ -0,0 +1,52 @@ +import React from "react"; +import Stats from "@/components/Stats"; +import { calcScore } from "@/lib/score"; +import ResultsByDimension from "@/components/ResultsByDimension"; +import { Evaluation, Report } from "@/redux/api/types"; + +const ReportResults = ({ + report, + evaluationsCompleted, + scoreByEvaluation, +}: { + report: Report; + evaluationsCompleted: Evaluation[]; + scoreByEvaluation?: { + id: string; + name: string; + score: number; + tags: string[]; + }[]; +}) => { + const startDate = new Date(report.createdAt).getTime(); + const endDate = new Date(report.deadline).getTime(); + + const period = Math.ceil( + Math.abs(endDate - startDate) / (1000 * 60 * 60 * 24) + ); + return ( +
    + + {scoreByEvaluation && ( + + )} +
    + ); +}; + +export default ReportResults; diff --git a/client/src/pages/mentor/Report/index.tsx b/client/src/pages/mentor/Report/index.tsx new file mode 100644 index 0000000..a3078a7 --- /dev/null +++ b/client/src/pages/mentor/Report/index.tsx @@ -0,0 +1,64 @@ +import React, { useMemo } from "react"; +import { useParams } from "react-router-dom"; +import { useFindReportQuery, userApi } from "@/redux/api/userApi"; +import Section from "@/components/Section"; +import Heading from "@/components/Heading"; +import ReportResults from "./ReportResults"; +import { useAppSelector } from "@/redux/store"; +import { evaluationsCompletedFilter } from "@/lib/filters"; +import { calcScoreByDimension } from "@/lib/score"; +import FullScreenLoader from "@/components/FullScreenLoader"; + +const Report = () => { + const { reportId } = useParams(); + const { data: report } = useFindReportQuery(reportId || ""); + const matrix = useAppSelector((state) => state.userState.matrix); + const { isLoading } = userApi.endpoints.getMatrix.useQuery(null, { + skip: !!matrix, + refetchOnMountOrArgChange: true, + }); + + const evaluationsCompleted = useMemo( + () => + report?.evaluations + ? evaluationsCompletedFilter(report?.evaluations) + : [], + [report?.evaluations] + ); + + const scoreByEvaluation = useMemo(() => { + return matrix && evaluationsCompleted.length > 0 + ? calcScoreByDimension({ evaluationsCompleted, matrix }) + : []; + }, [evaluationsCompleted, matrix]); + + if (!report || isLoading) { + return ; + } + + return ( + <> +
    +
    + + Evaluare + {new Date(report.createdAt).toLocaleDateString("ro-RO", { + year: "numeric", + month: "long", + day: "numeric", + })} + +
    +
    +
    + +
    + + ); +}; + +export default Report; diff --git a/client/src/redux/api/userApi.ts b/client/src/redux/api/userApi.ts index 449dcb8..c0485cc 100644 --- a/client/src/redux/api/userApi.ts +++ b/client/src/redux/api/userApi.ts @@ -78,11 +78,11 @@ export const userApi = createApi({ url: `users/${userId}?populate[0]=reports.evaluations.dimensions.quiz&populate[1]=domains`, }; }, - transformResponse: (result: User[]) => ({ + transformResponse: (result: User) => ({ ...result, - reports: result.reports.sort( - (a, b) => new Date(b.createdAt) - new Date(a.createdAt) - ), + reports: result.reports?.sort((a: Report, b: Report) => { + return new Date(b?.createdAt) - new Date(a?.createdAt); + }), }), providesTags: ["Report"], }), @@ -247,6 +247,7 @@ export const userApi = createApi({ }, }, }), + invalidatesTags: ["Report"], }), getReports: builder.query({ query: () => ({ diff --git a/client/src/redux/features/userSlice.ts b/client/src/redux/features/userSlice.ts index ac1a68f..bd56caf 100644 --- a/client/src/redux/features/userSlice.ts +++ b/client/src/redux/features/userSlice.ts @@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from "@reduxjs/toolkit"; import { Matrix, User } from "../api/types"; import { userApi } from "../api/userApi"; import Cookies from "js-cookie"; +import { RootState } from "@/redux/store"; interface UserState { user?: User; @@ -42,6 +43,9 @@ export const userSlice = createSlice({ }, }); +export const selectHasFinishedReports = (state: RootState) => + state.userState?.user?.reports?.some((report) => report.finished) ?? false; + export default userSlice.reducer; export const { logout, setUser, setToken } = userSlice.actions; diff --git a/client/src/router/index.tsx b/client/src/router/index.tsx index 62f7cb6..33a2b79 100644 --- a/client/src/router/index.tsx +++ b/client/src/router/index.tsx @@ -32,6 +32,8 @@ import MentorEditProfile from "@/pages/mentor/EditProfile"; import AuthenticatedMentorsList from "@/pages/authenticated/MentorsList"; import AuthenticatedMentor from "@/pages/authenticated/Mentor"; import CreateUser from "@/pages/admin/CreateUser"; +import MentorReport from "@/pages/mentor/Report"; + const Router = () => { const user = useAppSelector((state) => state.userState.user); const userType = getUserType(user); @@ -68,10 +70,11 @@ const Router = () => { ) : userType === "mentor" ? ( }> } /> - } /> - } /> - } /> + } /> + } /> + } /> } /> + } /> ) : ( }> diff --git a/client/tests/features/createReport/createReport.spec.ts b/client/tests/features/createReport/createReport.spec.ts index 4287c45..e4de82f 100644 --- a/client/tests/features/createReport/createReport.spec.ts +++ b/client/tests/features/createReport/createReport.spec.ts @@ -29,14 +29,14 @@ test("creates evaluation and completes it", async ({ page }) => { ); const data = await response.json(); await page.goto( - `https://app.crestem.ong/evaluation/${ - data.evaluations[0].id - }?email=${encodeURIComponent(data.evaluations[0].email)}` + `/evaluation/${data.evaluations[0].id}?email=${encodeURIComponent( + data.evaluations[0].email + )}` ); await fillEvaluation(page); await page.waitForTimeout(5000); - await await page.goto(`https://app.crestem.ong/reports/${data.id}`); + await await page.goto(`/reports/${data.id}`); await page.waitForResponse( (response) => diff --git a/server/src/api/mentorship-request/controllers/mentorship-request.js b/server/src/api/mentorship-request/controllers/mentorship-request.js index 922c841..40bc728 100644 --- a/server/src/api/mentorship-request/controllers/mentorship-request.js +++ b/server/src/api/mentorship-request/controllers/mentorship-request.js @@ -36,8 +36,13 @@ module.exports = createCoreController( const userData = await strapi.entityService.findOne( "plugin::users-permissions.user", user, - { populate: "role" } + { populate: ["role", "reports"] } ); + const reportId = userData.reports + .filter(({ finished }) => finished) + .sort(({ createdAt }) => createdAt) + .reverse() + .at(0); if (userData?.role?.type !== "authenticated") { throw new PolicyError(`Organizatia nu este valida`); @@ -49,6 +54,7 @@ module.exports = createCoreController( sendEmailToMentorFromUser(mentorData.email, { USER_NAME: userData.ongName, USER_EMAIL: userData.email, + REPORT_ID: reportId, }).catch((e) => { throw new PolicyError(`A aparut o eroare la trimiterea email-ului`); });