From c0c09e33e1036d940da893d3d39ea05be85c33a7 Mon Sep 17 00:00:00 2001 From: aimedivin Date: Wed, 20 Nov 2024 20:42:10 +0200 Subject: [PATCH] fix: fixes and improved trainee dashboard --- src/resolvers/2fa.resolvers.ts | 2 +- src/resolvers/attendance.resolvers.ts | 240 +++++++++++++------------- src/resolvers/ratingsResolvers.ts | 87 ++++++++-- src/resolvers/userResolver.ts | 30 ++-- src/schema/index.ts | 4 +- 5 files changed, 213 insertions(+), 150 deletions(-) diff --git a/src/resolvers/2fa.resolvers.ts b/src/resolvers/2fa.resolvers.ts index 9e5458bc..8a850981 100644 --- a/src/resolvers/2fa.resolvers.ts +++ b/src/resolvers/2fa.resolvers.ts @@ -110,7 +110,7 @@ const resolvers = { const geoData = await logGeoActivity(user, clientIpAdress) const organizationName = user.organizations[0]; if (organizationName) { - const location = geoData.city && geoData.country_name ? `${geoData.city}-${geoData.country_name}` : null; + const location = geoData && geoData.city && geoData.country_name ? `${geoData.city}-${geoData.country_name}` : null; await loginsCount(organizationName, location); } diff --git a/src/resolvers/attendance.resolvers.ts b/src/resolvers/attendance.resolvers.ts index 0c592123..6be74bdf 100644 --- a/src/resolvers/attendance.resolvers.ts +++ b/src/resolvers/attendance.resolvers.ts @@ -349,142 +349,146 @@ const returnAttendanceData = async (teamData: any) => { return formatAttendanceData(sanitizedAttendance, teamData) } -const attendanceResolver = { - Query: { - - async getTraineeAttendance(_: any, { traineeId }: { traineeId: string }, context: Context) { - const { userId: id, role } = (await checkUserLoggedIn(context))([ - RoleOfUser.TRAINEE, RoleOfUser.COORDINATOR, RoleOfUser.ADMIN, RoleOfUser.TTL - ]) - const userId = role === RoleOfUser.TRAINEE ? id : traineeId; - - const user = await User.findById(userId); - if (!user) { - throw new GraphQLError('User with provided id do not exist') - } - const userTeamId = user.team; - const teamData = await Team.findById(userTeamId); - const attendance = await Attendance.find(); - const phases: TraineeAttendancePhase[] = []; +export const fetchTraineeAttendance = async (userId: string | undefined) => { + const user = await User.findById(userId); + if (!user) { + throw new GraphQLError('User with provided id do not exist') + } + const userTeamId = user.team; + const teamData = await Team.findById(userTeamId); + const attendance = await Attendance.find(); + const phases: TraineeAttendancePhase[] = []; - if (!teamData) { - throw new Error('Team provided doesn\'t exist') - } + if (!teamData) { + throw new Error('Team provided doesn\'t exist') + } - let totalAllPhasesScore = 0; - let totalAllPhasesDays = 0; + let totalAllPhasesScore = 0; + let totalAllPhasesDays = 0; - const phasesAverage: PhasesAverage[] = [] + const phasesAverage: PhasesAverage[] = [] - if (attendance.length) { + if (attendance.length) { - for (const attendanceRecord of attendance) { - const phaseData = await Phase.findById(attendanceRecord.phase) + for (const attendanceRecord of attendance) { + const phaseData = await Phase.findById(attendanceRecord.phase) - if (!phaseData) { - throw new Error('Phase provided doesn\'t exist') - } - const phaseObj = phaseData.toObject() as PhaseInterface - - let existingPhaseAverageIndex = phasesAverage.findIndex(phase => phase.id === phaseData.id) - if (existingPhaseAverageIndex === -1) { - phasesAverage.push({ - id: phaseData.id, - totalPhaseScore: 0, - totalPhaseDays: 0 - }); - existingPhaseAverageIndex = phasesAverage.length - 1; - } + if (!phaseData) { + throw new Error('Phase provided doesn\'t exist') + } + const phaseObj = phaseData.toObject() as PhaseInterface + + let existingPhaseAverageIndex = phasesAverage.findIndex(phase => phase.id === phaseData.id) + if (existingPhaseAverageIndex === -1) { + phasesAverage.push({ + id: phaseData.id, + totalPhaseScore: 0, + totalPhaseDays: 0 + }); + existingPhaseAverageIndex = phasesAverage.length - 1; + } - attendanceRecord.teams.forEach((traineeAttendanceData) => { + attendanceRecord.teams.forEach((traineeAttendanceData) => { + if ( + traineeAttendanceData.team.equals( + (userTeamId as string).toString() + ) + ) { + traineeAttendanceData.trainees.forEach((traineeData) => { if ( - traineeAttendanceData.team.equals( - (userTeamId as string).toString() - ) + (traineeData.trainee as string).toString() === userId && + traineeAttendanceData.date ) { - traineeAttendanceData.trainees.forEach((traineeData) => { - if ( - (traineeData.trainee as string).toString() === userId && - traineeAttendanceData.date - ) { - let totalWeekScore = 0; - let totalWeekDays = 0; - const weekDays: WeekdaysInterface = getDateForDays( - new Date(traineeAttendanceData.date).getTime().toString() - ) - - traineeData.status.forEach((status) => { - if (weekDays[status.day]) { - if (!('score' in weekDays[status.day])) { - weekDays[status.day].score = status.score.toString(); - if (status.score && Number(status.score) >= 0) { - // sum for a week - totalWeekScore += Number(status.score); - totalWeekDays++; - - // sum for a phase - phasesAverage[existingPhaseAverageIndex].totalPhaseScore += Number(status.score) - phasesAverage[existingPhaseAverageIndex].totalPhaseDays++ - - // sum for all phases - totalAllPhasesScore += Number(status.score); - totalAllPhasesDays++; - } - } - } - }) + let totalWeekScore = 0; + let totalWeekDays = 0; + const weekDays: WeekdaysInterface = getDateForDays( + new Date(traineeAttendanceData.date).getTime().toString() + ) - Object.keys(weekDays).forEach((day) => { - const dayKey = day as keyof WeekdaysInterface - if (!('score' in weekDays[dayKey])) { - weekDays[dayKey].score = null - } - }) - - let phaseExists = false - - phases.forEach((phase) => { - if ( - phase.phase._id.equals( - (attendanceRecord.phase as string).toString() - ) - ) { - phase.phaseAverage = (phasesAverage[existingPhaseAverageIndex].totalPhaseScore / phasesAverage[existingPhaseAverageIndex].totalPhaseDays).toPrecision(2); - phase.weeks.push({ - week: attendanceRecord.week, - weekAverage: (totalWeekScore / totalWeekDays).toPrecision(2), - daysStatus: weekDays, - }); - phaseExists = true; + traineeData.status.forEach((status) => { + if (weekDays[status.day]) { + if (!('score' in weekDays[status.day])) { + weekDays[status.day].score = status.score.toString(); + if (status.score && Number(status.score) >= 0) { + // sum for a week + totalWeekScore += Number(status.score); + totalWeekDays++; + + // sum for a phase + phasesAverage[existingPhaseAverageIndex].totalPhaseScore += Number(status.score) + phasesAverage[existingPhaseAverageIndex].totalPhaseDays++ + + // sum for all phases + totalAllPhasesScore += Number(status.score); + totalAllPhasesDays++; } - }) - - if (!phaseExists) { - phases.push({ - phase: phaseObj, - phaseAverage: (phasesAverage[existingPhaseAverageIndex].totalPhaseScore / phasesAverage[existingPhaseAverageIndex].totalPhaseDays).toPrecision(2), - weeks: [ - { - week: attendanceRecord.week, - weekAverage: (totalWeekScore / totalWeekDays).toPrecision(2), - daysStatus: weekDays, - }, - ], - }) } } }) + + Object.keys(weekDays).forEach((day) => { + const dayKey = day as keyof WeekdaysInterface + if (!('score' in weekDays[dayKey])) { + weekDays[dayKey].score = null + } + }) + + let phaseExists = false + + phases.forEach((phase) => { + if ( + phase.phase._id.equals( + (attendanceRecord.phase as string).toString() + ) + ) { + phase.phaseAverage = (phasesAverage[existingPhaseAverageIndex].totalPhaseScore / phasesAverage[existingPhaseAverageIndex].totalPhaseDays).toPrecision(2); + phase.weeks.push({ + week: attendanceRecord.week, + weekAverage: (totalWeekScore / totalWeekDays).toPrecision(2), + daysStatus: weekDays, + }); + phaseExists = true; + } + }) + + if (!phaseExists) { + phases.push({ + phase: phaseObj, + phaseAverage: (phasesAverage[existingPhaseAverageIndex].totalPhaseScore / phasesAverage[existingPhaseAverageIndex].totalPhaseDays).toPrecision(2), + weeks: [ + { + week: attendanceRecord.week, + weekAverage: (totalWeekScore / totalWeekDays).toPrecision(2), + daysStatus: weekDays, + }, + ], + }) + } } }) } - } + }) + } + } - return { - traineeId: userId, - allPhasesAverage: (totalAllPhasesScore / totalAllPhasesDays).toPrecision(2), - teamName: teamData.name, - phases, - } + return { + traineeId: userId, + allPhasesAverage: (totalAllPhasesScore / totalAllPhasesDays).toPrecision(2), + teamName: teamData.name, + phases, + } +} + +const attendanceResolver = { + Query: { + + async getTraineeAttendance(_: any, { traineeId }: { traineeId: string }, context: Context) { + const { userId: id, role } = (await checkUserLoggedIn(context))([ + RoleOfUser.TRAINEE, RoleOfUser.COORDINATOR, RoleOfUser.ADMIN, RoleOfUser.TTL + ]) + const userId = role === RoleOfUser.TRAINEE ? id : traineeId; + + return await fetchTraineeAttendance(userId); }, async getTeamAttendance( diff --git a/src/resolvers/ratingsResolvers.ts b/src/resolvers/ratingsResolvers.ts index c8dbc1ce..105a585e 100644 --- a/src/resolvers/ratingsResolvers.ts +++ b/src/resolvers/ratingsResolvers.ts @@ -26,16 +26,55 @@ import { } from '../utils/sheets/extractSheetRatings' import Phase from '../models/phase.model' import Team from '../models/team.model' +import { fetchTraineeAttendance } from './attendance.resolvers' +import { isSameWeek } from 'date-fns' + +interface DayInterface { + date: string; + isValid?: boolean; + score?: string | null; +} +interface WeekdaysInterface { + mon: DayInterface; + tue: DayInterface; + wed: DayInterface; + thu: DayInterface; + fri: DayInterface; +} +interface TraineeAttendanceWeek { + week: number; + weekAverage: number; + daysStatus: WeekdaysInterface; +} + +interface PhaseInterface { + _id: string; + name: string; +} + +interface TraineeAttendancePhase { + phase: PhaseInterface; + phaseAverage: number; + weeks: TraineeAttendanceWeek[]; +} + +interface TraineeAttendanceInterface { + traineeId: string; + teamName: string; + allPhasesAverage: number; + phases: TraineeAttendancePhase[]; +} + const pubsub = new PubSub() const ratingEmailContent = generalTemplate({ message: - "We're excited to announce that your latest performance ratings are ready for review.", + 'We\'re excited to announce that your latest performance ratings are ready for review.', linkMessage: 'To access your new ratings, click the button below', buttonText: 'View Ratings', link: `${process.env.FRONTEND_LINK}/performance`, closingText: - "If you have any questions or require additional information about your ratings, please don't hesitate to reach out to us.", + 'If you have any questions or require additional information about your ratings, please don\'t hesitate to reach out to us.', }) let org: InstanceType @@ -201,7 +240,7 @@ const ratingResolvers: any = { }, async fetchRatingByCohort(_: any, { CohortName }: any, context: Context) { - ;(await checkUserLoggedIn(context))([ + ; (await checkUserLoggedIn(context))([ RoleOfUser.COORDINATOR, RoleOfUser.ADMIN, RoleOfUser.TRAINEE, @@ -260,8 +299,26 @@ const ratingResolvers: any = { populate: 'sender', }, ]) - .sort({ createdAt: -1 }) - return findRatings + .sort({ createdAt: -1 }); + + const sanitizedRatings = []; + const traineeAttendance: TraineeAttendanceInterface = await fetchTraineeAttendance(context.userId) as unknown as TraineeAttendanceInterface; + + for (const rating of findRatings) { + const mutableRating = rating as typeof rating & { attendance: number }; + for (const attendancePhase of traineeAttendance.phases) { + if (attendancePhase.phase.name.toLowerCase() === mutableRating.phase.toLowerCase()) { + for (const attendanceWeek of attendancePhase.weeks) { + if (isSameWeek(new Date(attendanceWeek.daysStatus.mon.date), new Date(mutableRating.createdAt), { weekStartsOn: 1 })) { + mutableRating.attendance = attendanceWeek.weekAverage; + } + } + } + } + sanitizedRatings.push(mutableRating); + } + + return sanitizedRatings; }, }, Mutation: { @@ -507,12 +564,12 @@ const ratingResolvers: any = { : [`${existingRating.quality}->`, row.quality], professional_Skills: existingRating.professional_Skills === - row.professional_skills.toString() + row.professional_skills.toString() ? [existingRating.professional_Skills] : [ - `${existingRating.professional_Skills}->`, - row.professional_skills, - ], + `${existingRating.professional_Skills}->`, + row.professional_skills, + ], feedbacks: [ ...existingRating.feedbacks.map((rating) => { if (rating.sender?.toString() === user._id.toString()) { @@ -633,7 +690,7 @@ const ratingResolvers: any = { oldData?.quality == quality[0].toString() && oldData?.professional_Skills == professional_Skills[0].toString() && (oldData?.feedbacks?.[0]?.content ?? '') == - (feedbackContent ?? '') && + (feedbackContent ?? '') && (feedbacks[0]?.toString() ?? '') == (feedbackContent ?? '') ) { throw new Error('No changes to update!') @@ -651,12 +708,12 @@ const ratingResolvers: any = { : [`${oldData?.quality} ->`, quality?.toString()], professional_Skills: oldData?.professional_Skills == - professional_Skills[0].toString() + professional_Skills[0].toString() ? oldData?.professional_Skills : [ - `${oldData?.professional_Skills} ->`, - professional_Skills?.toString(), - ], + `${oldData?.professional_Skills} ->`, + professional_Skills?.toString(), + ], feedbacks: oldData?.feedbacks.map((feedback) => { feedbackContent === feedback.content @@ -737,7 +794,7 @@ const ratingResolvers: any = { buttonText: 'View Ratings', link: `${process.env.FRONTEND_LINK}/performance`, closingText: - "If you have any questions or require additional information about your ratings, please don't hesitate to reach out to us.", + 'If you have any questions or require additional information about your ratings, please don\'t hesitate to reach out to us.', }) await sendEmails( diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index f0668756..0aa8b243 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -114,7 +114,7 @@ export async function loginsCount(organizationName: any, recentLocation: any) { const resolvers: any = { Query: { async getOrganizations(_: any, __: any, context: Context) { - ;(await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) + ; (await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) return Organization.find() }, @@ -168,7 +168,7 @@ const resolvers: any = { { organisation, username }: any, context: Context ) { - ;(await checkUserLoggedIn(context))([ + ; (await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.COORDINATOR, 'trainee', @@ -180,7 +180,7 @@ const resolvers: any = { name: organisation, }) if (!organisationExists) - throw new Error("This Organization doesn't exist") + throw new Error('This Organization doesn\'t exist') organisation = organisationExists.gitHubOrganisation @@ -403,7 +403,6 @@ const resolvers: any = { extensions: { code: 'AccountNotFound' }, }) } - // Check if account is active if (user.status?.status !== 'active') { throw new GraphQLError( @@ -452,12 +451,13 @@ const resolvers: any = { { expiresIn: '2h' } ) + const geoData = await logGeoActivity(user, clientIpAdress) // Log activity - + const organizationName = user.organizations[0] if (organizationName) { const location = - geoData.city && geoData.country_name + geoData && geoData.city && geoData.country_name ? `${geoData.city}-${geoData.country_name}` : null await loginsCount(organizationName, location) @@ -554,9 +554,9 @@ const resolvers: any = { ] const org = await checkLoggedInOrganization(orgToken) const roleExists = allRoles.includes(name) - if (!roleExists) throw new Error("This role doesn't exist") + if (!roleExists) throw new Error('This role doesn\'t exist') const userExists = await User.findById(id) - if (!userExists) throw new Error("User doesn't exist") + if (!userExists) throw new Error('User doesn\'t exist') const getAllUsers = await User.find({ role: RoleOfUser.ADMIN, @@ -793,7 +793,7 @@ const resolvers: any = { context: Context ) { // check if requester is super admin - ;(await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) + ; (await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) const orgExists = await Organization.findOne({ name: name }) if (action == 'approve') { if (!orgExists) { @@ -863,7 +863,7 @@ const resolvers: any = { context: Context ) { // the below commented line help to know if the user is an superAdmin to perform an action of creating an organization - ;(await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) + ; (await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN]) if (action == 'new') { const orgExists = await Organization.findOne({ name: name }) if (orgExists) { @@ -928,7 +928,7 @@ const resolvers: any = { { name, gitHubOrganisation }: any, context: Context ) { - ;(await checkUserLoggedIn(context))([ + ; (await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN, ]) @@ -1021,7 +1021,7 @@ const resolvers: any = { }, async deleteOrganization(_: any, { id }: any, context: Context) { - ;(await checkUserLoggedIn(context))([ + ; (await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN, ]) @@ -1029,7 +1029,7 @@ const resolvers: any = { const organizationExists = await Organization.findOne({ _id: id }) if (!organizationExists) - throw new Error("This Organization doesn't exist") + throw new Error('This Organization doesn\'t exist') await Cohort.deleteMany({ organization: id }) await Team.deleteMany({ organization: id }) await Phase.deleteMany({ organization: id }) @@ -1107,7 +1107,7 @@ const resolvers: any = { if (password === confirmPassword) { const user: any = await User.findOne({ email }) if (!user) { - throw new Error("User doesn't exist! ") + throw new Error('User doesn\'t exist! ') } user.password = password await user.save() @@ -1127,7 +1127,7 @@ const resolvers: any = { if (newPassword === confirmPassword) { const user: any = await User.findById(userId) if (!user) { - throw new Error("User doesn't exist! ") + throw new Error('User doesn\'t exist! ') } if (bcrypt.compareSync(currentPassword, user.password)) { diff --git a/src/schema/index.ts b/src/schema/index.ts index 7b6989b6..3c5c9ba0 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -191,7 +191,7 @@ const Schema = gql` description: String } - type Rating { + type Rating { id: ID! user: User! sprint: Int! @@ -205,6 +205,8 @@ const Schema = gql` cohort: Cohort! average: String feedbacks: [RatingMessageTemp] + createdAt: String + updatedAt: String } type AddRating {