diff --git a/src/Mutations/User.tsx b/src/Mutations/User.tsx index d8276d8d..f275fcbb 100644 --- a/src/Mutations/User.tsx +++ b/src/Mutations/User.tsx @@ -6,6 +6,17 @@ export const DROP_TTL_USER = gql` } `; +export const DROP_COORDINATOR = gql` + mutation DropCordinator($id: String!, $reason: String!) { + dropCordinator(id: $id, reason: $reason) + } +`; +export const UNDROP_COORDINATOR = gql` + mutation UndropCordinator($id: String!) { + undropCordinator(id: $id) + } +`; + export const UNDROP_TTL_USER = gql` mutation UnDropTTLUser($email: String!) { undropTTLUser(email: $email) diff --git a/src/components/DataTable.tsx b/src/components/DataTable.tsx index e19c900c..f015f35e 100644 --- a/src/components/DataTable.tsx +++ b/src/components/DataTable.tsx @@ -68,36 +68,33 @@ function DataTable({ data, columns, title, loading, className }: TableData) { return (

{t(title)}

- {/* Uncomment if you want a filter input */} - {/* */} + />
-
+
{headerGroups.map((headerGroup) => ( - + {headerGroup.headers.map((column) => ( {row.cells.map((cell) => (
@@ -116,15 +113,15 @@ function DataTable({ data, columns, title, loading, className }: TableData) {
diff --git a/src/components/DropOrUndropUser.tsx b/src/components/DropOrUndropUser.tsx new file mode 100644 index 00000000..19eb1aa4 --- /dev/null +++ b/src/components/DropOrUndropUser.tsx @@ -0,0 +1,118 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import Button from './Buttons'; + +interface DropOrUndropUserProps { + subject: string; + title: string; + drop?: boolean; + loading: boolean; + removalReason?: string; + setRemovalReason?: React.Dispatch>; + onSubmit: (data: React.MouseEvent) => void; + onClose: () => void; +} + +function DropOrUndropUser({ + subject, + title, + drop, + loading, + removalReason, + setRemovalReason, + onSubmit, + onClose, +}: DropOrUndropUserProps) { + const { t } = useTranslation(); + + const handleConfirm = (e: React.MouseEvent) => { + e.stopPropagation(); + if (onSubmit) onSubmit(e); + }; + + return ( +
+
+
+

+ {t(subject)} +

+
+
+
+
+
+

+ {t(title)} +

+
+ {/* Reason input field */} + {drop && setRemovalReason && ( +
+ ) => + setRemovalReason(e.target.value) + } + id="removalReason" + /> +

+ Reason is required! +

+
+ )} + +
+ + +
+
+
+
+
+ ); +} + +export default DropOrUndropUser; diff --git a/src/components/ViewComment.tsx b/src/components/ViewComment.tsx index 008164ee..b7598d3b 100644 --- a/src/components/ViewComment.tsx +++ b/src/components/ViewComment.tsx @@ -39,7 +39,7 @@ function Comment({ remark }: CommentProps) { {isModalOpen &&
} ([]); + const [cohorts, setCohorts] = useState([]); + const [viewCoordinator, setViewCoordinator] = useState( + null, + ); + const cohortOptions: any = []; + const [coordinatorModle, setCoordinatorModle] = useState(false); + const [unAssignedCohorts, setUnAssignedCohorts] = useState([]); + const [editModel, setEditModel] = useState(false); + const [removalReason, setRemovalReason] = useState(''); + const [dropModle, setDropModel] = useState(false); + const [undropModle, setUndropModel] = useState(false); + const [coordinatorId, setCoordinatorId] = useState(null); + const [cohortId, setCohortId] = useState(''); + + const { data: cohortsData, refetch: refetchCohorts } = useQuery( + GET_COHORTS_QUERY, + { + variables: { + orgToken, + }, + }, + ); + + const [giveCoordinatorCohort, { loading: givingLoading }] = useMutation( + GIVE_COORDINATOR_COHORT, + ); + + const [dropCordinator, { loading: dropLoading }] = + useMutation(DROP_COORDINATOR); + + const [undropCordinator, { loading: undropLoading }] = + useMutation(UNDROP_COORDINATOR); + + useEffect(() => { + if (cohortsData) { + setCohorts(cohortsData.getCohorts); + } + }, [cohortsData]); useEffect(() => { - if (data) { + if (data && cohorts.length > 0) { const extractedCoordinators = data.getAllCoordinators.map( - (coordinator: any) => ({ - email: coordinator.email, - profile: coordinator.profile || { name: null }, - organizations: coordinator.organizations || [], - role: coordinator.role, - }), + (coordinator: any) => { + const coordinatorCohorts = cohorts.filter( + (cohort) => + cohort.coordinator && cohort.coordinator.id === coordinator.id, + ); + return { + email: coordinator.email, + id: coordinator.id, + status: coordinator.status, + profile: coordinator.profile || { name: null }, + organizations: coordinator.organizations || [], + role: coordinator.role, + cohorts: coordinatorCohorts, + }; + }, ); - setCoordinators(extractedCoordinators); } - }, [data]); + }, [data, cohorts]); + + function PaperComponent(props: PaperProps) { + return ( + + + + ); + } + + const handleEdit = (id: string) => { + const unAssgnedCohorts = cohorts.filter((cohort, index) => { + if (cohort.coordinator == null) { + return true; + } + return cohort.coordinator.id !== id; + }); + + setUnAssignedCohorts(unAssgnedCohorts); + setEditModel(true); + }; + + useEffect(() => { + if (unAssignedCohorts.length > 0) { + unAssignedCohorts.forEach((cohort: any, index: any) => { + cohortOptions[index] = {}; + cohortOptions[index].value = cohort.id; + cohortOptions[index].label = cohort.name; + }); + } + }, [unAssignedCohorts]); + + const handleCloseDropModle = () => { + setDropModel(false); + }; + const handleCloseUnropModle = () => { + setUndropModel(false); + }; + + const handleViewCoordinator = (id: string) => { + const coordinatorToView = coordinators.find( + (coordinator) => coordinator.id === id, + ); + if (coordinatorToView) { + setViewCoordinator(coordinatorToView); + setCoordinatorModle(true); + } + }; + + const handleGiveCoordinatorCohort = () => { + if (!coordinatorId) return; + if (!cohortId) { + toast.error('First select cohort'); + return; + } + const coordinatorToGive = coordinators.find( + (coordinator) => coordinator.id === coordinatorId, + ); + + if ( + coordinatorToGive?.cohorts && + coordinatorToGive?.cohorts.length && + coordinatorToGive?.cohorts[0].id === cohortId + ) { + return; + } + giveCoordinatorCohort({ + variables: { + coordinatorId, + cohortId, + }, + }) + .then((response) => { + toast.success(response.data.giveCoordinatorCohort); + refetchCohorts(); + refetchCoordinators(); + setEditModel(false); + setCoordinatorId(null); + }) + .catch((error) => { + toast.error(error.message || 'An error occurred'); + }); + }; + + const handleDropCoordinator = (reason: string) => { + if (!coordinatorId) return; + dropCordinator({ + variables: { + id: coordinatorId, + reason, + }, + }) + .then((response) => { + toast.success('Coordinator Dropped Successfully'); + refetchCohorts(); + refetchCoordinators(); + handleCloseDropModle(); + setCoordinatorId(null); + }) + .catch((error) => { + toast.error(error.message || 'An error occurred'); + }); + }; + + const handleUndropCoordinator = () => { + if (!coordinatorId) return; + undropCordinator({ + variables: { + id: coordinatorId, + }, + }) + .then((response) => { + toast.success('Coordinator Undropped Successfully'); + refetchCohorts(); + refetchCoordinators(); + handleCloseUnropModle(); + setCoordinatorId(null); + }) + .catch((error) => { + toast.error(error.message || 'An error occurred'); + }); + }; + + const handleClose = () => { + setCoordinatorModle(false); + }; const columns = [ { @@ -60,8 +274,90 @@ export default function CoordinatorsPage() { Cell: ({ value }: any) => value || '-', }, { Header: t('Email'), accessor: 'email' }, - { Header: t('Organizations'), accessor: 'organizations' }, - { Header: t('Role'), accessor: 'role' }, + { + Header: t('Cohorts'), + accessor: 'cohorts', + Cell: ({ value }: any) => ( +
+ {value && value.length > 0 ? ( + value.map((cohort: Cohort) => ( +
{cohort.name}
+ )) + ) : ( +
{t('Not assigned')}
+ )} +
+ ), + }, + { + Header: t('actions'), + accessor: '', + /* istanbul ignore next */ + Cell: useMemo( + () => + function ({ row }: any) { + return ( +
+ { + if (row.original.status?.status === 'active') { + setCoordinatorId(row.original.id); + handleEdit(row.original.id); + } else { + toast.error('This Coordinator is Dropped out'); + } + }} + /> + + {row.original.status?.status === 'active' ? ( + { + setCoordinatorId(row.original.id); + setDropModel(true); + }} + /> + ) : ( + { + setCoordinatorId(row.original.id); + setUndropModel(true); + }} + /> + )} + { + handleViewCoordinator(row.original.id); + }} + /> +
+ ); + }, + [coordinators], + ), + }, ]; return ( @@ -71,7 +367,7 @@ export default function CoordinatorsPage() {
{loading ? ( - + ) : ( )}
+
+ + + +
+
+ Logo +
+ +

+ {viewCoordinator && viewCoordinator.profile + ? viewCoordinator.profile.name + : 'Unavailable'} +

+ +
+ {' '} +

+ EMAIL{' '} +

+

+ + {' '} + {viewCoordinator ? viewCoordinator.email : 'Unavailable'} + +

+
+ +
+ {' '} +

+ COHORT{' '} +

+

+ + {' '} + {viewCoordinator?.cohorts && + viewCoordinator.cohorts.length > 0 + ? viewCoordinator?.cohorts.map((cohort) => ( +

+ {cohort.name} +
+ )) + : 'Not assigned'} + +

+
+ +
+ {' '} +

+ STATUS{' '} +

+

+ {viewCoordinator?.status?.status} +

+
+ + +
+
+
+
+
+ {editModel && ( +
+
+
+

+ {t('Edit Coordinator')} +

+
+
+
+
+
+

+ {t( + 'Choose a cohort to assign Coordinator from the dropdown below.', + )} +

+
+ +
+
+ { + setCohortId(e.value); + }, + }} + options={cohortOptions} + /> +
+
+ +
+ + +
+
+
+
+
+ )} + + {dropModle && ( + { + handleDropCoordinator(removalReason); + }} + /> + )} + {undropModle && ( + + )} ); } diff --git a/src/containers/admin-dashBoard/TtlsModal.tsx b/src/containers/admin-dashBoard/TtlsModal.tsx index 2f86d74c..556c43d8 100644 --- a/src/containers/admin-dashBoard/TtlsModal.tsx +++ b/src/containers/admin-dashBoard/TtlsModal.tsx @@ -26,7 +26,7 @@ import { import ControlledSelect from '../../components/ControlledSelect'; import GitHubActivityChart from '../../components/chartGitHub'; import { toast } from 'react-toastify'; -import TtlSkeleton from '../../Skeletons/ttl.skeleton' +import TtlSkeleton from '../../Skeletons/ttl.skeleton'; /* istanbul ignore next */ export default function TtlsPage() { const { t } = useTranslation(); @@ -79,7 +79,8 @@ export default function TtlsPage() { const [isLoaded, setIsLoaded] = useState(false); const [gitHubStatistics, setGitHubStatistics] = useState({}); const [dropTTLUser, { loading: dropLoading }] = useMutation(DROP_TTL_USER); - const [undropTTLUser, { loading: undropLoading }] = useMutation(UNDROP_TTL_USER); + const [undropTTLUser, { loading: undropLoading }] = + useMutation(UNDROP_TTL_USER); function PaperComponent(props: PaperProps) { return ( { + const undropTTLMod = () => { let newState = !undropTTLModel; setUndropTTLModel(newState); }; @@ -223,57 +224,53 @@ export default function TtlsPage() { cursor="pointer" color="#9e85f5" /* istanbul ignore next */ - onClick={() => { - if (row.original.status?.status === "active") { - setSelectedOptionUpdate({ - value: row.original.team?.cohort?.name, - label: row.original.team?.cohort?.name, - }); - setSelectedTeamOptionUpdate({ - value: row.original?.team?.name, - label: row.original?.team?.name, - }); - removeEditModel(); - setEditEmail(row.original?.email); - setEditCohort(row.original.team?.cohort?.name); - setEditTeam(row.original.team?.name); - } - else { - toast.error("This TTL is Dropped out") - } - }} - /> - - {row.original.status?.status === "active" ? ( - { - - removeTraineeMod(); - setDeleteEmail(row.original?.email); - - }} - /> - ) : ( - { - console.log(row.original.status?.status); - - undropTTLMod(); - setDeleteEmail(row.original?.email); - - }} - /> - )} + onClick={() => { + if (row.original.status?.status === 'active') { + setSelectedOptionUpdate({ + value: row.original.team?.cohort?.name, + label: row.original.team?.cohort?.name, + }); + setSelectedTeamOptionUpdate({ + value: row.original?.team?.name, + label: row.original?.team?.name, + }); + removeEditModel(); + setEditEmail(row.original?.email); + setEditCohort(row.original.team?.cohort?.name); + setEditTeam(row.original.team?.name); + } else { + toast.error('This TTL is Dropped out'); + } + }} + /> + + {row.original.status?.status === 'active' ? ( + { + removeTraineeMod(); + setDeleteEmail(row.original?.email); + }} + /> + ) : ( + { + console.log(row.original.status?.status); + + undropTTLMod(); + setDeleteEmail(row.original?.email); + }} + /> + )} { setButtonLoading(true); setButtonLoading(true); - - if (editEmail) { - editMemberMutation(); - } else { - toast.error('Please select the trainee again '); - } - + + if (editEmail) { + editMemberMutation(); + } else { + toast.error('Please select the trainee again '); + } }} loading={buttonLoading} > @@ -506,7 +502,7 @@ export default function TtlsPage() {

{' '} - {traineeDetails.team!==undefined + {traineeDetails.team !== undefined ? traineeDetails.team.name : 'Not assigned'} @@ -554,10 +550,7 @@ export default function TtlsPage() { STATUS{' '}

- - {' '} - {traineeDetails.status?.status} - + {traineeDetails.status?.status}

@@ -592,10 +585,8 @@ export default function TtlsPage() { 'Unavailable' )}

- - + )} - - + className={`h-screen w-screen bg-black bg-opacity-30 backdrop-blur-sm fixed top-0 left-0 z-20 flex items-center justify-center px-4 ${ + removeTraineeModel === true ? 'block' : 'hidden' + }`} + > +
+
+

+ {t('Remove TTL')} +

+
+
+
+ +
+

+ {t('Are you sure you want to remove TTL from this cohort?')} +

+
+ {/* Reason input field */} +
+ ) => + setRemovalReason(e.target.value) + } + id="removalReason" + /> +

+ Reason is required! +

+
+
+ + +
+ +
- - - - + {/* =========================== End:: RemoveTraineeModel =============================== */} - {/* =========================== Start:: UndropTTLModel =============================== */}
-
-
-

- {t('Undrop TTL')} -

-
-
-
-
-
-

- {t('Are you sure you want to Undrop this TTL ?')} -

-
- {/* Reason input field */} -
- - - + className={`h-screen w-screen bg-black bg-opacity-30 backdrop-blur-sm fixed top-0 left-0 z-20 flex items-center justify-center px-4 ${ + undropTTLModel === true ? 'block' : 'hidden' + }`} + > +
+
+

+ {t('Undrop TTL')} +

+
+
+
+ +
+

+ {t('Are you sure you want to Undrop this TTL ?')} +

+
+ {/* Reason input field */} +
+ + +
+ +
- -
-
-
+
{/* =========================== End:: UndropTTLModel =============================== */}
- {loading ? ( + {loading ? ( ) : ( { - const [input, setInput] = useState(Array(6).fill('')); - const [error, setError] = useState(''); - const [loading, setLoading] = useState(false); - const [isDark, setIsDark] = useState(false); - const { login } = useContext(UserContext); - const client = useApolloClient(); - const { t } = useTranslation(); - - const location = useLocation(); - const navigate = useNavigate(); - const { email, TwoWayVerificationToken } = location.state || {}; - useEffect(() => { - // Update document class and localStorage when theme changes - if (isDark) { - document.documentElement.classList.add('dark'); - localStorage.setItem('theme', 'dark'); - } else { - document.documentElement.classList.remove('dark'); - localStorage.setItem('theme', 'light'); - } - }, [isDark]); - - useEffect(() => { - if (!email || !TwoWayVerificationToken) { - navigate('/login'); - } - }, [email, TwoWayVerificationToken, navigate]); - - const [loginWithTwoFactorAuthentication] = useMutation( - LOGIN_WITH_2FA, - { - onCompleted: async (data) => { - const response = data.loginWithTwoFactorAuthentication; - try { - localStorage.setItem('authToken', response.token); - localStorage.setItem('user', JSON.stringify(response.user)); - await login(response); - await client.resetStore(); - toast.success(response.message); - - const rolePaths: Record = { - superAdmin: '/organizations', - admin: '/trainees', - coordinator: '/trainees', - manager: '/dashboard', - ttl: '/ttl-trainees', - trainee: '/performance', - }; - - const redirectPath = rolePaths[response.user.role] || '/dashboard'; - navigate(redirectPath, { replace: true }); - } catch (error) { - toast.error('Login Error'); - } - }, - onError: (error) => { - const errorMessage = error.message || 'Verification Failed'; - setError(errorMessage); - toast.error(errorMessage); - setInput(Array(6).fill('')); - }, - }, - ); - - const verifyOtp = async (currentInput = input) => { - if (currentInput.some((val) => !val)) { - setError('Please Enter All Digits'); - return; - } - - setLoading(true); - setError(''); - - try { - await loginWithTwoFactorAuthentication({ - variables: { - email, - otp: currentInput.join(''), - TwoWayVerificationToken, - }, - }); - } finally { - setLoading(false); - } - }; - - const handleInput = useCallback( - (index: number, value: string) => { - if (!/^\d*$/.test(value)) return; - - const newInput = [...input]; - newInput[index] = value; - setInput(newInput); - - if (value && index < input.length - 1) { - const nextInput = document.getElementById( - `otp-input-${index + 1}`, - ) as HTMLInputElement; - nextInput?.focus(); - } - - if (value && index === input.length - 1) { - const allFilled = newInput.every((val) => val !== ''); - if (allFilled) { - verifyOtp(newInput); - } - } - }, - [input], - ); - - const handleKeyDown = ( - index: number, - e: React.KeyboardEvent, - ) => { - if (e.key === 'Backspace' && !input[index] && index > 0) { - const prevInput = document.getElementById( - `otp-input-${index - 1}`, - ) as HTMLInputElement; - prevInput?.focus(); - } - }; - - const handlePaste = (e: React.ClipboardEvent) => { - e.preventDefault(); - const pastedData = e.clipboardData.getData('text').trim(); - - if (!/^\d+$/.test(pastedData)) { - setError('Only Numbers Can Be Pasted'); - return; - } - - const digits = pastedData.slice(0, 6).split(''); - const newInput = [...digits, ...Array(6 - digits.length).fill('')]; - - setInput(newInput); - - if (digits.length < 6) { - const nextEmptyIndex = digits.length; - const nextInput = document.getElementById( - `otp-input-${nextEmptyIndex}`, - ) as HTMLInputElement; - nextInput?.focus(); - } else { - verifyOtp(newInput); - } - }; - - const toggleTheme = () => { - setIsDark(!isDark); - }; - - return ( -
-
-

- {'Verification Required'} -

- -

- {'Enter Verification Code'} -
- {email} -

- - {error && ( -
- {error} -
- )} - -
{ - e.preventDefault(); - verifyOtp(); - }} - > -
- {input.map((value, index) => ( - handleInput(index, e.target.value)} - onKeyDown={(e) => handleKeyDown(index, e)} - onPaste={index === 0 ? handlePaste : undefined} - className="w-12 h-12 text-lg font-semibold text-center text-gray-800 transition-colors bg-white border rounded dark:text-gray-100 dark:border-gray-600 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent" - disabled={loading} - autoComplete="one-time-code" - required - /> - ))} -
- - -
-
-
- ); -}; - -export default TwoFactorPage; +/* eslint-disable */ +import React, { useState, useEffect, useCallback, useContext } from 'react'; +import { useMutation, gql, useApolloClient } from '@apollo/client'; +import { useLocation, useNavigate } from 'react-router-dom'; +import { UserContext } from '../hook/useAuth'; +import { toast } from 'react-toastify'; +import { useTranslation } from 'react-i18next'; + +interface Profile { + id: string; + firstName: string; + lastName: string; + name: string | null; + address: string | null; + city: string | null; + country: string | null; + phoneNumber: string | null; + biography: string | null; + avatar: string | null; + cover: string | null; + __typename: 'Profile'; +} + +interface User { + id: string; + role: string; + email: string; + profile: Profile; + __typename: 'User'; +} + +interface LoginResponse { + loginWithTwoFactorAuthentication: { + token: string; + user: User; + message: string; + __typename: 'LoginResponse'; + }; +} + +export const LOGIN_WITH_2FA = gql` + mutation LoginWithTwoFactorAuthentication( + $email: String! + $otp: String! + $TwoWayVerificationToken: String! + ) { + loginWithTwoFactorAuthentication( + email: $email + otp: $otp + TwoWayVerificationToken: $TwoWayVerificationToken + ) { + token + user { + id + role + email + profile { + id + firstName + lastName + name + address + city + country + phoneNumber + biography + avatar + cover + } + } + message + } + } +`; + +const TwoFactorPage: React.FC = () => { + const [input, setInput] = useState(Array(6).fill('')); + const [error, setError] = useState(''); + const [loading, setLoading] = useState(false); + const [isDark, setIsDark] = useState(false); + const { login } = useContext(UserContext); + const client = useApolloClient(); + const { t } = useTranslation(); + + const location = useLocation(); + const navigate = useNavigate(); + const { email, TwoWayVerificationToken } = location.state || {}; + useEffect(() => { + // Update document class and localStorage when theme changes + if (isDark) { + document.documentElement.classList.add('dark'); + localStorage.setItem('theme', 'dark'); + } else { + document.documentElement.classList.remove('dark'); + localStorage.setItem('theme', 'light'); + } + }, [isDark]); + + useEffect(() => { + if (!email || !TwoWayVerificationToken) { + navigate('/login'); + } + }, [email, TwoWayVerificationToken, navigate]); + + const [loginWithTwoFactorAuthentication] = useMutation( + LOGIN_WITH_2FA, + { + onCompleted: async (data) => { + const response = data.loginWithTwoFactorAuthentication; + try { + localStorage.setItem('authToken', response.token); + localStorage.setItem('user', JSON.stringify(response.user)); + await login(response); + await client.resetStore(); + toast.success(response.message); + + const rolePaths: Record = { + superAdmin: '/dashboard', + admin: '/trainees', + coordinator: '/trainees', + manager: '/dashboard', + ttl: '/ttl-trainees', + trainee: '/dashboard', + }; + + const redirectPath = rolePaths[response.user.role] || '/dashboard'; + navigate(redirectPath, { replace: true }); + } catch (error) { + toast.error('Login Error'); + } + }, + onError: (error) => { + const errorMessage = error.message || 'Verification Failed'; + setError(errorMessage); + toast.error(errorMessage); + setInput(Array(6).fill('')); + }, + }, + ); + + const verifyOtp = async (currentInput = input) => { + if (currentInput.some((val) => !val)) { + setError('Please Enter All Digits'); + return; + } + + setLoading(true); + setError(''); + + try { + await loginWithTwoFactorAuthentication({ + variables: { + email, + otp: currentInput.join(''), + TwoWayVerificationToken, + }, + }); + } finally { + setLoading(false); + } + }; + + const handleInput = useCallback( + (index: number, value: string) => { + if (!/^\d*$/.test(value)) return; + + const newInput = [...input]; + newInput[index] = value; + setInput(newInput); + + if (value && index < input.length - 1) { + const nextInput = document.getElementById( + `otp-input-${index + 1}`, + ) as HTMLInputElement; + nextInput?.focus(); + } + + if (value && index === input.length - 1) { + const allFilled = newInput.every((val) => val !== ''); + if (allFilled) { + verifyOtp(newInput); + } + } + }, + [input], + ); + + const handleKeyDown = ( + index: number, + e: React.KeyboardEvent, + ) => { + if (e.key === 'Backspace' && !input[index] && index > 0) { + const prevInput = document.getElementById( + `otp-input-${index - 1}`, + ) as HTMLInputElement; + prevInput?.focus(); + } + }; + + const handlePaste = (e: React.ClipboardEvent) => { + e.preventDefault(); + const pastedData = e.clipboardData.getData('text').trim(); + + if (!/^\d+$/.test(pastedData)) { + setError('Only Numbers Can Be Pasted'); + return; + } + + const digits = pastedData.slice(0, 6).split(''); + const newInput = [...digits, ...Array(6 - digits.length).fill('')]; + + setInput(newInput); + + if (digits.length < 6) { + const nextEmptyIndex = digits.length; + const nextInput = document.getElementById( + `otp-input-${nextEmptyIndex}`, + ) as HTMLInputElement; + nextInput?.focus(); + } else { + verifyOtp(newInput); + } + }; + + const toggleTheme = () => { + setIsDark(!isDark); + }; + + return ( +
+
+

+ {'Verification Required'} +

+ +

+ {'Enter Verification Code'} +
+ {email} +

+ + {error && ( +
+ {error} +
+ )} + +
{ + e.preventDefault(); + verifyOtp(); + }} + > +
+ {input.map((value, index) => ( + handleInput(index, e.target.value)} + onKeyDown={(e) => handleKeyDown(index, e)} + onPaste={index === 0 ? handlePaste : undefined} + className="w-12 h-12 text-lg font-semibold text-center text-gray-800 transition-colors bg-white border rounded dark:text-gray-100 dark:border-gray-600 dark:bg-gray-700 focus:ring-2 focus:ring-blue-500 focus:border-transparent" + disabled={loading} + autoComplete="one-time-code" + required + /> + ))} +
+ + +
+
+
+ ); +}; + +export default TwoFactorPage; diff --git a/src/pages/Organization/AdminLogin.tsx b/src/pages/Organization/AdminLogin.tsx index a3a37788..285d67ee 100644 --- a/src/pages/Organization/AdminLogin.tsx +++ b/src/pages/Organization/AdminLogin.tsx @@ -1,348 +1,348 @@ -/* eslint-disable */ -'use client'; -import { useApolloClient, useMutation } from '@apollo/client'; -import React, { useContext, useEffect, useState } from 'react'; -import { useForm } from 'react-hook-form'; -import { useTranslation } from 'react-i18next'; -import { FaRegEnvelope, FaRegEye } from 'react-icons/fa'; -import { FiEyeOff } from 'react-icons/fi'; -import { MdLockOutline } from 'react-icons/md'; -import { - Link, - useLocation, - useNavigate, - useSearchParams, -} from 'react-router-dom'; -import { toast, ToastContent } from 'react-toastify'; -import ButtonLoading from '../../components/ButtonLoading'; -import Button from '../../components/Buttons'; -import { UserContext } from '../../hook/useAuth'; -import useDocumentTitle from '../../hook/useDocumentTitle'; -import LOGIN_MUTATION from './LoginMutation'; -import functionTree from '../../assets/Functionality_Tree.svg'; -import pulseStars from '../../assets/Property 1=Logo_flie (1).svg'; - -function AdminLogin() { - const orgToken: any = localStorage.getItem('orgToken'); - const orgName: any = localStorage.getItem('orgName'); - const [loading, setLoading] = useState(false); - const [otpRequired, setOtpRequired] = useState(false); - const [TwoWayVerificationToken, setTwoWayVerificationToken] = useState(''); - const [otp, setOtp] = useState(''); - - useDocumentTitle('Login'); - const { t } = useTranslation(); - const [passwordShown, setPasswordShown] = useState(false); - /* istanbul ignore next */ - const tooglePassword = () => { - setPasswordShown(!passwordShown); - }; - const { - register, - handleSubmit, - formState: { errors }, - setError, - }: any = useForm(); - const { login } = useContext(UserContext); - const navigate = useNavigate(); - const [LoginUser] = useMutation(LOGIN_MUTATION); - const client = useApolloClient(); - const [searchParams] = useSearchParams(); - // Function to get the redirect_message from the URL and toast it - const showRedirectMessage = () => { - const redirectMessage = searchParams.get('redirect_message'); - if (redirectMessage) { - toast.error(redirectMessage); - } - }; - - // Call showRedirectMessage when the component mounts - useEffect(() => { - showRedirectMessage(); - }, [searchParams]); - - const onSubmit = async (userInput: any) => { - userInput.orgToken = orgToken; - try { - setLoading(true); - const redirect = searchParams.get('redirect'); - // const activity = await getLocation(); - await LoginUser({ - variables: { - loginInput: { - ...userInput, - // activity, //disable geolocation - }, - }, - - - /* istanbul ignore next */ - onCompleted: async (data) => { - if (data.loginUser.otpRequired) { - setOtpRequired(true); - setTwoWayVerificationToken(data.loginUser.TwoWayVerificationToken); - navigate('/users/LoginWith2fa', { - state: { - email: userInput.email, - TwoWayVerificationToken: data.loginUser.TwoWayVerificationToken, - },}) - - }else{ - /* istanbul ignore next */ - toast.success(data.addMemberToCohort); - /* istanbul ignore next */ - login(data.loginUser); - /* istanbul ignore next */ - await client.resetStore(); - /* istanbul ignore next */ - toast.success(t(`Welcome`) as ToastContent); - /* istanbul ignore next */ - if (data.loginUser) { - redirect - ? navigate(`${redirect}`) - : data.loginUser.user.role === 'superAdmin' - ? navigate(`/dashboard`) - : data.loginUser.user.role === 'admin' - ? navigate(`/trainees`) - : data.loginUser.user.role === 'coordinator' - ? navigate(`/trainees`) - : data.loginUser.user.role === 'manager' - ? navigate(`/dashboard`) - : data.loginUser.user.role === 'ttl' - ? navigate('/ttl-trainees') - : navigate('/performance'); - } else { - navigate('/dashboard'); - } - }}, - onError: (err) => { - /* istanbul ignore next */ - console.log(err.message); - - if (err.networkError) - toast.error('There was a problem contacting the server'); - else if (err.message.toLowerCase() !== 'invalid credential') { - const translateError = t( - 'Please wait to be added to a program or cohort', - ); - toast.error(translateError); - } else { - /* istanbul ignore next */ - setError('password', { - type: 'custom', - message: t('Invalid credentials'), - }); - } - }, - }); - } catch (error: any) { - /* istanbul ignore next */ - setError('password', { - type: 'custom', - message: t('Invalid credentials'), - }); - } finally { - setLoading(false); - } - }; - - const getLocation = async () => { - const location = await fetch('https://geolocation-db.com/json/') - .then(async (res) => { - const activityResponse = await res.json(); - - const activityResponseActual = { - IPv4: activityResponse.IPv4, - city: activityResponse.city, - country_code: activityResponse.country_code, - country_name: activityResponse.country_name, - latitude: - activityResponse.latitude === 'Not found' - ? null - : activityResponse.latitude, - longitude: - activityResponse.longitude === 'Not found' - ? null - : activityResponse.longitude, - postal: activityResponse.postal, - state: activityResponse.state, - }; - return activityResponseActual; - }) - .then((data) => data); - const date = new Date().toString(); - return { date, ...location } || null; - }; - - return ( -
-
-
-
- pulses -

- {t('Boost your organization')} -

-
- -
- functions -
-
-
- -
-
-
-
-

- {t('Welcome to')}{' '} - {orgName - ? orgName.charAt(0).toUpperCase() + - orgName.slice(1).toLowerCase() - : ''} -

-
-
- -
- - {t('Switch')} {t('your organization')} - -
- -
-
- {errors.password && - errors.password.message === t('Invalid credentials') ? ( -
- - {errors.password.message} - -
- ) : ( - '' - )} -
- - -
-
- {errors.email && ( - - {errors.email.message} - - )} -
- -
- - -
- {passwordShown ? ( - - ) : ( - - )} -
-
-
- {errors.password && - errors.password.message !== t('Invalid credentials') ? ( - - {errors.password.message} - - ) : ( - '' - )} -
-
-
- -
-
- - {t('Forgot Password?')} - -
-
-
- {loading ? ( - - ) : ( - - )} -
-
-
- -
- {t('First time here?')} - - {t('Register')} - - {t('your organization')} -
-
-
-
-
- ); -} - -export default AdminLogin; +/* eslint-disable */ +'use client'; +import { useApolloClient, useMutation } from '@apollo/client'; +import React, { useContext, useEffect, useState } from 'react'; +import { useForm } from 'react-hook-form'; +import { useTranslation } from 'react-i18next'; +import { FaRegEnvelope, FaRegEye } from 'react-icons/fa'; +import { FiEyeOff } from 'react-icons/fi'; +import { MdLockOutline } from 'react-icons/md'; +import { + Link, + useLocation, + useNavigate, + useSearchParams, +} from 'react-router-dom'; +import { toast, ToastContent } from 'react-toastify'; +import ButtonLoading from '../../components/ButtonLoading'; +import Button from '../../components/Buttons'; +import { UserContext } from '../../hook/useAuth'; +import useDocumentTitle from '../../hook/useDocumentTitle'; +import LOGIN_MUTATION from './LoginMutation'; +import functionTree from '../../assets/Functionality_Tree.svg'; +import pulseStars from '../../assets/Property 1=Logo_flie (1).svg'; + +function AdminLogin() { + const orgToken: any = localStorage.getItem('orgToken'); + const orgName: any = localStorage.getItem('orgName'); + const [loading, setLoading] = useState(false); + const [otpRequired, setOtpRequired] = useState(false); + const [TwoWayVerificationToken, setTwoWayVerificationToken] = useState(''); + const [otp, setOtp] = useState(''); + + useDocumentTitle('Login'); + const { t } = useTranslation(); + const [passwordShown, setPasswordShown] = useState(false); + /* istanbul ignore next */ + const tooglePassword = () => { + setPasswordShown(!passwordShown); + }; + const { + register, + handleSubmit, + formState: { errors }, + setError, + }: any = useForm(); + const { login } = useContext(UserContext); + const navigate = useNavigate(); + const [LoginUser] = useMutation(LOGIN_MUTATION); + const client = useApolloClient(); + const [searchParams] = useSearchParams(); + // Function to get the redirect_message from the URL and toast it + const showRedirectMessage = () => { + const redirectMessage = searchParams.get('redirect_message'); + if (redirectMessage) { + toast.error(redirectMessage); + } + }; + + // Call showRedirectMessage when the component mounts + useEffect(() => { + showRedirectMessage(); + }, [searchParams]); + + const onSubmit = async (userInput: any) => { + userInput.orgToken = orgToken; + try { + setLoading(true); + const redirect = searchParams.get('redirect'); + // const activity = await getLocation(); + await LoginUser({ + variables: { + loginInput: { + ...userInput, + // activity, //disable geolocation + }, + }, + + /* istanbul ignore next */ + onCompleted: async (data) => { + if (data.loginUser.otpRequired) { + setOtpRequired(true); + setTwoWayVerificationToken(data.loginUser.TwoWayVerificationToken); + navigate('/users/LoginWith2fa', { + state: { + email: userInput.email, + TwoWayVerificationToken: data.loginUser.TwoWayVerificationToken, + }, + }); + } else { + /* istanbul ignore next */ + toast.success(data.addMemberToCohort); + /* istanbul ignore next */ + login(data.loginUser); + /* istanbul ignore next */ + await client.resetStore(); + /* istanbul ignore next */ + toast.success(t(`Welcome`) as ToastContent); + /* istanbul ignore next */ + if (data.loginUser) { + redirect + ? navigate(`${redirect}`) + : data.loginUser.user.role === 'superAdmin' + ? navigate(`/dashboard`) + : data.loginUser.user.role === 'admin' + ? navigate(`/trainees`) + : data.loginUser.user.role === 'coordinator' + ? navigate(`/trainees`) + : data.loginUser.user.role === 'manager' + ? navigate(`/dashboard`) + : data.loginUser.user.role === 'ttl' + ? navigate('/ttl-trainees') + : navigate('/dashboard'); + } else { + navigate('/dashboard'); + } + } + }, + onError: (err) => { + /* istanbul ignore next */ + console.log(err.message); + + if (err.networkError) + toast.error('There was a problem contacting the server'); + else if (err.message.toLowerCase() !== 'invalid credential') { + const translateError = t( + 'Please wait to be added to a program or cohort', + ); + toast.error(translateError); + } else { + /* istanbul ignore next */ + setError('password', { + type: 'custom', + message: t('Invalid credentials'), + }); + } + }, + }); + } catch (error: any) { + /* istanbul ignore next */ + setError('password', { + type: 'custom', + message: t('Invalid credentials'), + }); + } finally { + setLoading(false); + } + }; + + const getLocation = async () => { + const location = await fetch('https://geolocation-db.com/json/') + .then(async (res) => { + const activityResponse = await res.json(); + + const activityResponseActual = { + IPv4: activityResponse.IPv4, + city: activityResponse.city, + country_code: activityResponse.country_code, + country_name: activityResponse.country_name, + latitude: + activityResponse.latitude === 'Not found' + ? null + : activityResponse.latitude, + longitude: + activityResponse.longitude === 'Not found' + ? null + : activityResponse.longitude, + postal: activityResponse.postal, + state: activityResponse.state, + }; + return activityResponseActual; + }) + .then((data) => data); + const date = new Date().toString(); + return { date, ...location } || null; + }; + + return ( +
+
+
+
+ pulses +

+ {t('Boost your organization')} +

+
+ +
+ functions +
+
+
+ +
+
+
+
+

+ {t('Welcome to')}{' '} + {orgName + ? orgName.charAt(0).toUpperCase() + + orgName.slice(1).toLowerCase() + : ''} +

+
+
+ +
+ + {t('Switch')} {t('your organization')} + +
+ +
+
+ {errors.password && + errors.password.message === t('Invalid credentials') ? ( +
+ + {errors.password.message} + +
+ ) : ( + '' + )} +
+ + +
+
+ {errors.email && ( + + {errors.email.message} + + )} +
+ +
+ + +
+ {passwordShown ? ( + + ) : ( + + )} +
+
+
+ {errors.password && + errors.password.message !== t('Invalid credentials') ? ( + + {errors.password.message} + + ) : ( + '' + )} +
+
+
+ +
+
+ + {t('Forgot Password?')} + +
+
+
+ {loading ? ( + + ) : ( + + )} +
+
+
+ +
+ {t('First time here?')} + + {t('Register')} + + {t('your organization')} +
+
+
+
+
+ ); +} + +export default AdminLogin; diff --git a/src/pages/TraineeDashboard.tsx b/src/pages/TraineeDashboard.tsx index c9361f3a..a5e7cb2c 100644 --- a/src/pages/TraineeDashboard.tsx +++ b/src/pages/TraineeDashboard.tsx @@ -133,11 +133,41 @@ function traineeDashboard() { }, [selectedPhase, selectedTimeFrame]); const columns = [ - { Header: `${t('Sprint')}`, accessor: 'sprint' }, - { Header: `${t('Quantity')}`, accessor: 'quantity' }, - { Header: `${t('Quality')}`, accessor: 'quality' }, - { Header: `${t('Professionalism')}`, accessor: 'professionalism' }, - { Header: `${t('Attendance')}`, accessor: 'attendance' }, + { + Header: `${t('Sprint')}`, + accessor: 'sprint', + Cell: ({ value }: any) => ( +
{value}
+ ), + }, + { + Header: `${t('Quantity')}`, + accessor: 'quantity', + Cell: ({ value }: any) => ( +
{value}
+ ), + }, + { + Header: `${t('Quality')}`, + accessor: 'quality', + Cell: ({ value }: any) => ( +
{value}
+ ), + }, + { + Header: `${t('Professionalism')}`, + accessor: 'professionalism', + Cell: ({ value }: any) => ( +
{value}
+ ), + }, + { + Header: `${t('Attendance')}`, + accessor: 'attendance', + Cell: ({ value }: any) => ( +
{value}
+ ), + }, { Header: `${t('Comment')}`, accessor: '', @@ -155,7 +185,7 @@ function traineeDashboard() { setCohort(data?.getProfile?.user?.team?.cohort?.name); if (selectedPhase === undefined) { setSelectedPhase(data?.getProfile?.user?.team?.cohort?.phase?.name); - setAllPhase([data?.getProfile?.user?.team?.cohort?.phase?.name]); + // setAllPhase([data?.getProfile?.user?.team?.cohort?.phase?.name]); } } catch (error: any) {} }; diff --git a/src/queries/manageStudent.queries.tsx b/src/queries/manageStudent.queries.tsx index 19cbe021..371ccf33 100644 --- a/src/queries/manageStudent.queries.tsx +++ b/src/queries/manageStudent.queries.tsx @@ -111,6 +111,9 @@ export const GET_COHORTS_QUERY = gql` getCohorts(orgToken: $orgToken) { name id + coordinator { + id + } } } `; diff --git a/tests/pages/LoginWith2fa.test.tsx b/tests/pages/LoginWith2fa.test.tsx index 515c4b4d..aca637c2 100644 --- a/tests/pages/LoginWith2fa.test.tsx +++ b/tests/pages/LoginWith2fa.test.tsx @@ -15,56 +15,58 @@ jest.mock('react-router-dom', () => ({ useNavigate: () => mockNavigate, useLocation: () => ({ state: { - email: "user@example.com", - TwoWayVerificationToken: "test-token" - } - }) + email: 'user@example.com', + TwoWayVerificationToken: 'test-token', + }, + }), })); // Mock UserContext const mockLogin = jest.fn(); interface UserContextWrapperProps { - children: React.ReactNode; - } - - const UserContextWrapper: React.FC = ({ children }) => ( - - {children} - - ); + children: React.ReactNode; +} + +const UserContextWrapper: React.FC = ({ + children, +}) => ( + + {children} + +); const mocks = [ { request: { query: LOGIN_WITH_2FA, variables: { - email: "user@example.com", - otp: "123456", - TwoWayVerificationToken: "test-token", + email: 'user@example.com', + otp: '123456', + TwoWayVerificationToken: 'test-token', }, }, result: { data: { loginWithTwoFactorAuthentication: { - token: "jwt-token", + token: 'jwt-token', user: { - id: "1", - role: "trainee", - email: "user@example.com", + id: '1', + role: 'trainee', + email: 'user@example.com', profile: { - id: "1", - firstName: "John", - lastName: "Doe", - name: "John Doe", - address: "", - city: "", - country: "", - phoneNumber: "", - biography: "", - avatar: "", - cover: "", + id: '1', + firstName: 'John', + lastName: 'Doe', + name: 'John Doe', + address: '', + city: '', + country: '', + phoneNumber: '', + biography: '', + avatar: '', + cover: '', }, }, - message: "Login successful", + message: 'Login successful', }, }, }, @@ -73,13 +75,13 @@ const mocks = [ request: { query: LOGIN_WITH_2FA, variables: { - email: "user@example.com", - otp: "654321", - TwoWayVerificationToken: "test-token", + email: 'user@example.com', + otp: '654321', + TwoWayVerificationToken: 'test-token', }, }, result: { - errors: [{ message: "Invalid OTP" }], + errors: [{ message: 'Invalid OTP' }], }, }, ]; @@ -95,7 +97,7 @@ describe('TwoFactorPage', () => { - + , ); // Type OTP digits @@ -106,7 +108,7 @@ describe('TwoFactorPage', () => { // Get and click submit button const submitButton = screen.getByRole('button', { name: /verify(ing)?/i }); - + // Wait for the button to be enabled after all inputs are filled // await waitFor(() => { // expect(submitButton).not.toBeDisabled(); @@ -117,7 +119,9 @@ describe('TwoFactorPage', () => { // Wait for success message and navigation await waitFor(() => { expect(mockLogin).toHaveBeenCalled(); - expect(mockNavigate).toHaveBeenCalledWith('/performance', { replace: true }); + expect(mockNavigate).toHaveBeenCalledWith('/dashboard', { + replace: true, + }); }); }); @@ -127,7 +131,7 @@ describe('TwoFactorPage', () => { - + , ); // Type incorrect OTP @@ -138,7 +142,7 @@ describe('TwoFactorPage', () => { // Get and click submit button const submitButton = screen.getByRole('button', { name: /verify(ing)?/i }); - + // await waitFor(() => { // expect(submitButton).not.toBeDisabled(); // }); @@ -151,4 +155,4 @@ describe('TwoFactorPage', () => { }); expect(mockNavigate).not.toHaveBeenCalled(); }); -}); \ No newline at end of file +});