From 410ad3eadda443680f8060e13ae9b256350d47ad Mon Sep 17 00:00:00 2001 From: aimedivin Date: Sun, 13 Oct 2024 23:16:16 +0200 Subject: [PATCH 1/3] fix(attendance): move attendance calculation logic from client to server --- package-lock.json | 29 ++ package.json | 2 + src/index.ts | 1 + src/models/attendance.model.ts | 30 +- src/models/team.model.ts | 8 + src/models/user.ts | 24 +- src/resolvers/attendance.resolvers.ts | 446 +++++++++++++++++++++----- src/resolvers/cohort.resolvers.ts | 20 +- src/resolvers/coordinatorResolvers.ts | 90 +++--- src/resolvers/team.resolvers.ts | 29 +- src/resolvers/userResolver.ts | 32 +- src/schema/index.ts | 76 ++++- src/utils/cron-jobs/team-jobs.ts | 124 +++++++ src/utils/getDateForDays.ts | 77 +++++ 14 files changed, 794 insertions(+), 194 deletions(-) create mode 100644 src/utils/cron-jobs/team-jobs.ts create mode 100644 src/utils/getDateForDays.ts diff --git a/package-lock.json b/package-lock.json index 169d05ee..5ccf245d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@graphql-tools/utils": "^10.0.4", "@octokit/graphql": "^7.0.1", "@octokit/rest": "^19.0.13", + "@types/node-cron": "^3.0.11", "apollo-server": "^3.13.0", "axios": "^1.7.7", "bcryptjs": "^2.4.3", @@ -35,6 +36,7 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^4.17.1", "mongoose": "^6.6.1", + "node-cron": "^3.0.3", "node-fetch": "^2.6.12", "nodemailer": "^6.7.8", "normalize-mongoose": "^1.0.0", @@ -3067,6 +3069,12 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-13.13.52.tgz", "integrity": "sha512-s3nugnZumCC//n4moGGe6tkNMyYEdaDBitVjwPxXmR5lnMG5dHePinH2EdxkG3Rh1ghFHHixAG4NJhpJW1rthQ==" }, + "node_modules/@types/node-cron": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/node-cron/-/node-cron-3.0.11.tgz", + "integrity": "sha512-0ikrnug3/IyneSHqCBeslAhlK2aBfYek1fGo4bP4QnZPmiqSGRK+Oy7ZMisLWkesffJvQ1cqAcBnJC+8+nxIAg==", + "license": "MIT" + }, "node_modules/@types/node-fetch": { "version": "2.6.11", "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", @@ -6810,6 +6818,27 @@ "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==" }, + "node_modules/node-cron": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/node-cron/-/node-cron-3.0.3.tgz", + "integrity": "sha512-dOal67//nohNgYWb+nWmg5dkFdIwDm8EpeGYMekPMrngV3637lqnX0lbUcCtgibHTz6SEz7DAIjKvKDFYCnO1A==", + "license": "ISC", + "dependencies": { + "uuid": "8.3.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/node-cron/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", diff --git a/package.json b/package.json index ee4ed910..976841b5 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "@graphql-tools/utils": "^10.0.4", "@octokit/graphql": "^7.0.1", "@octokit/rest": "^19.0.13", + "@types/node-cron": "^3.0.11", "apollo-server": "^3.13.0", "axios": "^1.7.7", "bcryptjs": "^2.4.3", @@ -79,6 +80,7 @@ "jsonwebtoken": "^9.0.2", "mongodb": "^4.17.1", "mongoose": "^6.6.1", + "node-cron": "^3.0.3", "node-fetch": "^2.6.12", "nodemailer": "^6.7.8", "normalize-mongoose": "^1.0.0", diff --git a/src/index.ts b/src/index.ts index e7125124..f5762ea8 100644 --- a/src/index.ts +++ b/src/index.ts @@ -50,6 +50,7 @@ import { IResolvers } from '@graphql-tools/utils' import invitationSchema from './schema/invitation.schema' import TableViewInvitationResolver from './resolvers/TableViewInvitationResolver' import eventSchema from './schema/event.schema' +import './utils/cron-jobs/team-jobs' const PORT: number = parseInt(process.env.PORT!) || 4000 diff --git a/src/models/attendance.model.ts b/src/models/attendance.model.ts index d869f949..4dfa863c 100644 --- a/src/models/attendance.model.ts +++ b/src/models/attendance.model.ts @@ -18,8 +18,13 @@ const AttendanceSchema = new Schema({ teams: [ { + date: { + type: Date, + required: false, + default: () => new Date(), + }, team: { - type: mongoose.Types.ObjectId, + type: mongoose.Schema.Types.ObjectId, ref: 'Team', required: true }, @@ -34,24 +39,33 @@ const AttendanceSchema = new Schema({ day: { type: String, enum: ['mon', 'tue', 'wed', 'thu', 'fri'], - required: true + required: true }, date: { type: Date, - required: true + required: true }, score: { type: String, - enum: ['0', '1', '2'], - required: true + enum: [0, 1, 2], + required: true }, }, ], }, - ],} + ], + } ], - -}) +}, { timestamps: true }) + +AttendanceSchema.index( + { + phase: 1, + cohort: 1, + createdAt: 1 + }, + { unique: true } +); const Attendance = mongoose.model('Attendance', AttendanceSchema) export { Attendance } diff --git a/src/models/team.model.ts b/src/models/team.model.ts index b4af4170..5c140f02 100644 --- a/src/models/team.model.ts +++ b/src/models/team.model.ts @@ -1,10 +1,13 @@ import mongoose, { Schema } from 'mongoose' import { User } from './user' import { CohortInterface } from './cohort.model'; +import { PhaseInterface } from './phase.model'; export interface TeamInterface { + _id: mongoose.Types.ObjectId; name: string; cohort?: CohortInterface; + phase?: PhaseInterface; ttl?: mongoose.Types.ObjectId; members: mongoose.Types.ObjectId[]; startingPhase: Date; @@ -57,6 +60,11 @@ const teamSchema = new Schema( type: mongoose.Types.ObjectId, ref: 'Program', }, + isJobActive: { + type: Boolean, + default: true, + required: true, + }, }, { statics: { diff --git a/src/models/user.ts b/src/models/user.ts index bc59a9ed..3eee3ae1 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -9,17 +9,19 @@ export interface UserStatus { } export interface UserInterface { - _id: mongoose.Types.ObjectId - email: string - password: string - role: string - team?: mongoose.Types.ObjectId - status: UserStatus - cohort?: mongoose.Types.ObjectId - program?: mongoose.Types.ObjectId - organizations: string[] - pushNotifications: boolean - emailNotifications: boolean + _id: mongoose.Types.ObjectId; + email: string; + password: string; + role: string; + team?: mongoose.Types.ObjectId; + status: UserStatus; + cohort?: mongoose.Types.ObjectId; + program?: mongoose.Types.ObjectId; + organizations: string[]; + pushNotifications: boolean; + emailNotifications: boolean; + profile?: mongoose.Types.ObjectId; + ratings?: mongoose.Types.ObjectId[]; } export enum RoleOfUser { diff --git a/src/resolvers/attendance.resolvers.ts b/src/resolvers/attendance.resolvers.ts index 4573e09c..8cfde952 100644 --- a/src/resolvers/attendance.resolvers.ts +++ b/src/resolvers/attendance.resolvers.ts @@ -2,73 +2,260 @@ import { Attendance } from '../models/attendance.model' import { IntegerType, ObjectId } from 'mongodb' import { Context } from './../context' -import mongoose, { Error, Types } from 'mongoose' +import mongoose, { Document, Error, Types } from 'mongoose' import { checkUserLoggedIn } from '../helpers/user.helpers' import { pushNotification } from '../utils/notification/pushNotification' -import Phase from '../models/phase.model' +import Phase, { PhaseInterface } from '../models/phase.model' import { RoleOfUser, User, UserInterface } from '../models/user' -import Team from '../models/team.model' +import Team, { TeamInterface } from '../models/team.model' import { CohortInterface } from '../models/cohort.model' import { GraphQLError } from 'graphql' import { checkLoggedInOrganization } from '../helpers/organization.helper' +import { getDateForDays } from '../utils/getDateForDays' +import { format, isSameWeek } from 'date-fns' +import { id } from 'date-fns/locale' +import { AnyAaaaRecord } from 'dns' +import { addNewAttendanceWeek } from '../utils/cron-jobs/team-jobs' interface TraineeAttendanceStatus { day: 'mon' | 'tue' | 'wed' | 'thu' | 'fri' - score: '0' | '1' | '2' + date: string + score: 0 | 1 | 2 } interface TraineeAttendanceData { trainee: ObjectId - status: TraineeAttendanceStatus + score: number } +interface TeamAttendanceData { + week: number + phase: PhaseInterface + cohort: CohortInterface + teams: Array<{ + team: TeamInterface + date: string + trainees: Array<{ + trainee: UserInterface + status: Array + } + > + }> + createdAt: string +} + interface AttendanceInput { week: string + day: 'mon' | 'tue' | 'wed' | 'thu' | 'fri' team: string - date?: string phase?: string + today: boolean + yesterday: boolean trainees: TraineeAttendanceData[] orgToken: string } +interface TraineeAttendanceDataInterface { + trainee?: { + id: string; + email: string; + status: { + status: string + }; + profile: { + name: string; + }; + }; + score?: number; +} +interface DayInterface { + date: string + isValid: boolean +} +export interface WeekdaysInterface { + mon: DayInterface; + tue: DayInterface; + wed: DayInterface; + thu: DayInterface; + fri: DayInterface; +} +interface TraineeAttendanceDayInterface { + week: number; + phase: { + id: string + name: string + }; + dates: WeekdaysInterface; + days: { + mon: TraineeAttendanceDataInterface[]; + tue: TraineeAttendanceDataInterface[]; + wed: TraineeAttendanceDataInterface[]; + thu: TraineeAttendanceDataInterface[]; + fri: TraineeAttendanceDataInterface[]; + }; +} + +interface AttendanceWeeksInterface { + phase: { + name: string, + id: string + } + weeks: Array +} + +const formatAttendanceData = (data: TeamAttendanceData[], teamData: TeamInterface) => { + const tempPhases: PhaseInterface[] = []; + const attendanceWeeks: AttendanceWeeksInterface[] = [] + const phase = { + id: teamData.phase?._id.toString() || teamData.cohort!.phase._id.toString(), + name: teamData.phase?.name || teamData.cohort!.phase.name + } + + const attendanceResult: TraineeAttendanceDayInterface[] = []; + data.forEach(attendance => { + const result: TraineeAttendanceDayInterface = { + week: attendance.week, + dates: { + mon: { + date: '', + isValid: false + }, + tue: { + date: '', + isValid: false + }, + wed: { + date: '', + isValid: false + }, + thu: { + date: '', + isValid: false + }, + fri: { + date: '', + isValid: false + }, + }, + phase: { + id: attendance.phase._id.toString(), + name: attendance.phase.name + }, + days: { + mon: [], + tue: [], + wed: [], + thu: [], + fri: [], + }, + }; + + // Store all attendance weeks + let isWeekSet = false + attendanceWeeks.forEach((week, index) => { + if (week.phase.id === attendance.phase._id.toString()) { + isWeekSet = true; + attendanceWeeks[index].weeks.push(attendance.week) + } + }) + + !isWeekSet && attendanceWeeks.push({ + phase: { + id: attendance.phase._id.toString(), + name: attendance.phase.name + }, + weeks: [attendance.week] + }) + + if (!tempPhases.find((p) => p._id.equals(attendance.phase._id))) + tempPhases.push(attendance.phase); + let date = attendance.teams[0].date; + + attendance.teams[0].trainees.forEach((traineeData) => { + if (traineeData.status.length && traineeData.trainee.status.status !== 'drop') { + traineeData.status.forEach((traineeStatus) => { + if (traineeStatus.date && !date) { + date = traineeStatus.date; + } + + result.days[ + traineeStatus.day as 'mon' | 'tue' | 'wed' | 'thu' | 'fri' + ].push({ + trainee: { + ...(traineeData.trainee as unknown as Document).toObject(), + profile: { + name: (traineeData.trainee.profile! as any).name + }, + id: traineeData.trainee._id.toString(), + }, + score: traineeStatus.score, + }); + }); + } + }); + result.dates = getDateForDays(date); + attendanceResult.push(result); + }); + + + const today = new Date(); + const yesterday = new Date().getDay() === 1 ? new Date().setDate(new Date().getDate() - 3) : new Date().setDate(new Date().getDate() - 1); + + return { attendanceWeeks, attendance: attendanceResult, today, yesterday } +}; + const validateAttendance = async ( team: string, orgToken: string, trainees: TraineeAttendanceData[], - context: Context + context: Context, + isUpdating = false ) => { const org = await checkLoggedInOrganization(orgToken) if (!org) { - throw new Error("Orgnasation doesn't exist") + throw new Error('Organisation doesn\'t exist') } - ;(await checkUserLoggedIn(context))(['coordinator']) + const { userId }: any = (await checkUserLoggedIn(context))(['coordinator', 'ttl']) const teamData = await Team.findById(team) + .populate({ + path: 'members', + match: { role: 'trainee' } + }) .populate('cohort') + .populate('phase') .populate('cohort.phase') if (!teamData) { - throw new Error("Team provided doesn't exist") + throw new Error('Team provided doesn\'t exist') } + const phaseData = await Phase.findById( - (teamData.cohort as CohortInterface).phase._id + teamData.phase || (teamData.cohort as CohortInterface).phase._id ) if (!phaseData) { - throw new Error("Phase provided doesn't exist") + throw new Error('Phase provided doesn\'t exist') } - trainees.forEach((trainee) => { - if ( - trainee.status.day.toLowerCase() !== trainees[0].status.day.toLowerCase() - ) { - throw new GraphQLError( - 'Please make sure, you submit same date for each trainee ', - { + teamData.members.forEach((member) => { + const trainee = member as UserInterface; + if (trainee.role === 'trainee' && trainee.status.status === 'active') { + const sentTestTrainee = trainees.find(traineeData => trainee._id.equals(traineeData.trainee)) + if (!sentTestTrainee && !isUpdating) { + throw new GraphQLError('Please ensure attendance is taken for all active trainees in the team', { extensions: { - code: 'INCONSISTENT_TRAINEE_ATTENDANCE_DATE', + code: 'INCONSISTENT_TRAINEE_ATTENDANCE', }, - } - ) + }) + } + if (sentTestTrainee && ![0, 1, 2].includes(sentTestTrainee.score)) { + throw new GraphQLError('Attendance cannot be recorded due to an invalid score for one of trainees.', { + extensions: { + code: 'INVALID_TRAINEE_SCORE', + }, + }) + } } }) return { teamData, phaseData, + userId } } @@ -77,16 +264,21 @@ const returnAttendanceData = async (teamData: any) => { .populate('phase') .populate('cohort') .populate('teams.team') - .populate('teams.trainees.trainee', '-password') + .populate({ + path: 'teams.trainees.trainee', + select: '-password', + populate: { + path: 'profile', + } + }) const sanitizedAttendance: any[] = [] attendances.forEach((attendance) => { + const result = attendance.teams.find((teamAttendanceData) => (teamAttendanceData.team as ObjectId).equals(teamData.id) ) - const filteredTrainees = result?.trainees.filter( - (trainee) => (trainee.trainee as UserInterface).status.status !== 'drop' - ) + const filteredTrainees = result?.trainees.filter(trainee => (trainee.trainee as UserInterface).status.status !== 'drop') result && sanitizedAttendance.push({ @@ -100,18 +292,10 @@ const returnAttendanceData = async (teamData: any) => { ...(attendance.phase as mongoose.Document).toObject(), id: (attendance.phase as mongoose.Document)._id, }, - teams: [ - { - team: { - ...(result.team as mongoose.Document).toObject(), - id: (result.team as mongoose.Document)._id, - }, - trainees: filteredTrainees, - }, - ], + teams: [{ date: result.date, team: { ...(result.team as unknown as mongoose.Document).toObject(), id: (result.team as unknown as mongoose.Document)._id }, trainees: filteredTrainees }], }) }) - return sanitizedAttendance + return formatAttendanceData(sanitizedAttendance, teamData) } const attendanceResolver = { @@ -121,7 +305,7 @@ const attendanceResolver = { { traineeEmail }: any, context: Context ) { - ;(await checkUserLoggedIn(context))([RoleOfUser.TRAINEE]) + ; (await checkUserLoggedIn(context))([RoleOfUser.TRAINEE]) const attendance = await Attendance.find() const weeklyAttendance = attendance.map((week: any) => { @@ -140,25 +324,29 @@ const attendanceResolver = { { team }: { team: string }, context: Context ) { - ;(await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR]) - const { userId } = (await checkUserLoggedIn(context))([ - RoleOfUser.COORDINATOR, - ]) + (await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR, RoleOfUser.TTL]) + + await addNewAttendanceWeek(); const teamData = await Team.findById(team) + .populate('phase') + .populate({ + path: 'cohort', + populate: { + path: 'phase' + } + }) if (!teamData) { - throw new Error("Team provided doesn't exist") + throw new Error('Team provided doesn\'t exist') } return returnAttendanceData(teamData) }, async getAttendanceStats(_: any, args: any, context: Context) { - ;(await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR]) - const { userId } = (await checkUserLoggedIn(context))([ - RoleOfUser.COORDINATOR, - ]) + ; (await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR]) + const { userId } = (await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR]) const attendances: any = await Attendance.find({ coordinatorId: userId }) //calculate statistic @@ -213,17 +401,68 @@ const attendanceResolver = { }, Mutation: { + async pauseAndResumeTeamAttendance(_: any, { orgToken, team }: { orgToken: string, team: string }, context: Context) { + (await checkUserLoggedIn(context))(['coordinator', 'ttl']); + + const teamData = await Team.findById(team).populate({ + path: 'members', + match: { role: 'trainee' } + }) + .populate('cohort') + .populate('phase') + .populate('cohort.phase'); + + if (!teamData) { + throw new Error('Team provided doesn\'t exist') + } + const tempIsJobActive = teamData.isJobActive + teamData.isJobActive = !tempIsJobActive; + const savedTeam = await teamData.save(); + + !tempIsJobActive && await addNewAttendanceWeek(); + return { team: savedTeam, sanitizedAttendance: returnAttendanceData(teamData) } + }, + async recordAttendance( _: any, - { week, trainees, team, date, orgToken }: AttendanceInput, + { week, trainees, team, today, yesterday, orgToken }: AttendanceInput, context: Context ) { - const { teamData, phaseData } = await validateAttendance( + + const { teamData, phaseData, userId } = await validateAttendance( team, orgToken, trainees, context ) + + if (!today && !yesterday) { + throw new Error('Recording attendance is only allowed for today and the day before within work days.') + } + if (today && yesterday) { + throw new Error('Please select either today or yesterday, not both.') + } + let date = (today && new Date()).toString(); + + if (yesterday) { + const today = new Date(); + if (today.getDay() === 1) { + const lastFriday = new Date(today); + + lastFriday.setDate(today.getDate() - 3); + date = lastFriday.toString() + } else { + const previousDay = new Date(today); + previousDay.setDate(today.getDate() - 1); + date = previousDay.toString() + } + } + + // Check if the day is among work days + if (![1, 2, 3, 4, 5].includes(new Date(date).getDay())) { + throw new Error('Attendance can only be recorded on workdays.') + } + const attendance = await Attendance.findOne({ phase: phaseData.id, week, @@ -249,9 +488,9 @@ const attendanceResolver = { trainee: traineeData, status: [ { - ...trainees[i].status, - date: new Date(date!), - day: trainees[i].status.day.toLowerCase(), + date: new Date(date), + score: trainees[i].score, + day: new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase(), }, ], }) @@ -260,7 +499,7 @@ const attendanceResolver = { if (!attendants.length) { throw new Error( - "Invalid Ids for trainees or trainees doesn't belong to the team" + 'Invalid Ids for trainees or trainees doesn\'t belong to the team' ) } @@ -277,8 +516,8 @@ const attendanceResolver = { ], } - const savedAttendance = await Attendance.create(newAttendance) - return savedAttendance.teams[0] + await Attendance.create(newAttendance) + return returnAttendanceData(teamData) } // Adding new team to week attendance @@ -297,12 +536,17 @@ const attendanceResolver = { } ) .populate('teams.team') - .populate('teams.trainees.trainee', '-password') + .populate('teams.trainees.trainee', '-password'); + + attendants.forEach(attendant => { + pushNotification(attendant.trainee._id, `Your attendance for ${today ? 'today' : 'yesterday'} has been recorded, Kindly review your score.`, userId, 'attendance') + }); return savedAttendance?.teams[savedAttendance?.teams.length - 1] } let traineeStatusUpdated = false + const traineeIdsNotification = [] for (let i = 0; i < trainees.length; i++) { const traineeIndex = attendance.teams[ @@ -310,28 +554,27 @@ const attendanceResolver = { ].trainees.findIndex((traineeData) => (traineeData.trainee as UserInterface)._id.equals(trainees[i].trainee) ) - if (traineeIndex === -1) { + traineeStatusUpdated = true; const traineeData = await User.findOne( { _id: new ObjectId(trainees[i].trainee), team: teamData.id }, { password: 0 } ) if (traineeData) { - ;(attendance.teams[attendanceTeamIndex!].trainees as any[]).push({ + (attendance.teams[attendanceTeamIndex!].trainees as any[]).push({ trainee: traineeData, status: [ { - day: trainees[i].status.day.toLowerCase() as - | 'mon' - | 'tue' - | 'wed' - | 'thu' - | 'fri', - date: new Date(date!), - score: trainees[i].status.score as '0' | '1' | '2', + day: new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase(), + date: new Date(date), + score: trainees[i].score, }, ], }) + traineeIdsNotification.push({ + id: traineeData._id, + score: trainees[i].score + }); } } else { if ( @@ -350,7 +593,7 @@ const attendanceResolver = { const existingDay = attendance.teams[attendanceTeamIndex!].trainees[ traineeIndex - ].status.find((s) => s.day === trainees[i].status.day.toLowerCase()) + ].status.find((s) => s.day === new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase()) if ( ( @@ -363,15 +606,19 @@ const attendanceResolver = { attendance.teams[attendanceTeamIndex!].trainees[ traineeIndex ].status.push({ - day: trainees[i].status.day.toLowerCase() as + day: new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase() as | 'mon' | 'tue' | 'wed' | 'thu' | 'fri', date: new Date(date!), - score: trainees[i].status.score, - }) + score: trainees[i].score, + }); + traineeIdsNotification.push({ + id: (attendance.teams[attendanceTeamIndex!].trainees[traineeIndex].trainee as UserInterface)._id, + score: trainees[i].score + }); } } } @@ -386,27 +633,32 @@ const attendanceResolver = { ) } - const savedTeamAttendance = await ( - await attendance.save() - ).populate('teams.team') - return savedTeamAttendance.teams[attendanceTeamIndex!] + await attendance.save(); + traineeIdsNotification.forEach(trainee => { + pushNotification(trainee.id, `Your attendance for ${today ? 'today,' : 'yesterday,'} ${format(new Date(date), 'MMMM dd, yyyy')} has been recorded, Your score is ${trainee.score}.`, userId, 'attendance') + }) + return returnAttendanceData(teamData) }, async updateAttendance( _: any, - { week, trainees, team, orgToken, phase }: AttendanceInput, + { week, trainees, team, orgToken, day, phase }: AttendanceInput, context: Context ) { - const { teamData } = await validateAttendance( + + const { teamData, userId } = await validateAttendance( team, orgToken, trainees, - context + context, + true ) + const phaseData = await Phase.findById(phase) + if (!phaseData) { - throw new Error("Phase provided doesn't exist") + throw new Error('Phase provided doesn\'t exist') } const attendance = await Attendance.findOne({ phase: phaseData.id, @@ -417,6 +669,7 @@ const attendanceResolver = { (teamAttendanceData) => (teamAttendanceData.team as ObjectId).equals(teamData.id) ) + if (!attendance || teamToUpdateIndex === -1) { throw new GraphQLError('Invalid week or team', { extensions: { @@ -424,7 +677,9 @@ const attendanceResolver = { }, }) } - const teamAttendanceTrainees = attendance.teams[teamToUpdateIndex!] + const teamAttendanceTrainees = attendance.teams[teamToUpdateIndex!]; + const date = teamAttendanceTrainees.date ? format(new Date(getDateForDays(teamAttendanceTrainees.date.getTime().toString())[day].date), 'MMMM dd, yyyy') : day; + const traineeIdsNotification: { id: Types.ObjectId, score: number }[] = []; trainees.forEach((sentTrainee) => { let isDropped = false @@ -447,19 +702,28 @@ const attendanceResolver = { } ) } + if (traineeIndex !== -1 && !isDropped) { - const traineeToUpdateStatus = - teamAttendanceTrainees.trainees[traineeIndex].status + const traineeToUpdateStatus = teamAttendanceTrainees.trainees[traineeIndex].status; + traineeToUpdateStatus.forEach((status) => { if ( - status.day === trainees[traineeIndex].status.day.toLowerCase() + status.day === day.toLowerCase() ) { - status.score = trainees[traineeIndex].status.score + (status.score != sentTrainee.score) && traineeIdsNotification.push({ + id: (teamAttendanceTrainees.trainees[traineeIndex].trainee as UserInterface)._id, + score: sentTrainee.score + }); + status.score = sentTrainee.score; } }) } }) - await attendance.save() + + await attendance.save(); + traineeIdsNotification.forEach(trainee => { + pushNotification(trainee.id, `Your attendance record for ${date}, has been updated, Your new score is ${trainee.score}.`, userId, 'attendance') + }) return returnAttendanceData(teamData) }, @@ -468,31 +732,35 @@ const attendanceResolver = { { week, day, team }: { week: string; day: string; team: string }, context: Context ) { - ;(await checkUserLoggedIn(context))(['coordinator']) + (await checkUserLoggedIn(context))(['coordinator', 'ttl']) const teamData = await Team.findById(team) .populate('cohort') .populate('cohort.phase') if (!teamData) { - throw new Error("Team provided doesn't exist") + throw new Error('Team provided doesn\'t exist') } + const phase = teamData.phase || (teamData.cohort as CohortInterface).phase._id + const attendance = await Attendance.findOne({ - phase: (teamData?.cohort as CohortInterface).phase._id, + phase: phase, week, cohort: teamData.cohort, - }).populate('teams.trainees.trainee', '-password') + }).populate('teams.trainees.trainee', '-password'); + const attendanceTeamIndex = attendance?.teams.findIndex( (teamAttendanceData) => (teamAttendanceData.team as ObjectId).equals(teamData.id) ) if (!attendance || attendanceTeamIndex === -1) { - throw new Error("Can't find the Attendance for this day") + throw new Error('Can\'t find the Attendance for this day') } let removedAttendances = 0 + // attendance.teams[attendanceTeamIndex!].date attendance.teams[attendanceTeamIndex!].trainees.forEach((trainee) => { const statusIndex = trainee.status.findIndex( (s) => s.day === day.toLowerCase() @@ -508,7 +776,7 @@ const attendanceResolver = { return returnAttendanceData(teamData) } - throw new Error("Can't find the Attendance for this day") + throw new Error('Can\'t find the Attendance for this day') }, }, } diff --git a/src/resolvers/cohort.resolvers.ts b/src/resolvers/cohort.resolvers.ts index 83358dad..7e7c94b0 100644 --- a/src/resolvers/cohort.resolvers.ts +++ b/src/resolvers/cohort.resolvers.ts @@ -12,6 +12,7 @@ import { ProgramType } from './program.resolvers' import { OrganizationType } from './userResolver' import { pushNotification } from '../utils/notification/pushNotification' import { Types } from 'mongoose' +import { addNewAttendanceWeek } from '../utils/cron-jobs/team-jobs' export type CohortType = InstanceType @@ -100,8 +101,8 @@ const resolvers = { orgToken, } = args - // some validations - ;(await checkUserLoggedIn(context))([ + // some validations + ; (await checkUserLoggedIn(context))([ RoleOfUser.SUPER_ADMIN, RoleOfUser.ADMIN, RoleOfUser.MANAGER, @@ -147,7 +148,7 @@ const resolvers = { endDate && isAfter(new Date(startDate.toString()), new Date(endDate.toString())) ) { - throw new GraphQLError("End Date can't be before Start Date", { + throw new GraphQLError('End Date can\'t be before Start Date', { extensions: { code: 'VALIDATION_ERROR', }, @@ -180,7 +181,7 @@ const resolvers = { `You\'ve been assigned a new cohort "${name}"`, senderId ) - + addNewAttendanceWeek() return newCohort } catch (error) { const { message } = error as { message: any } @@ -291,7 +292,7 @@ const resolvers = { new Date(endDate) )) ) { - throw new GraphQLError("End Date can't be before Start Date", { + throw new GraphQLError('End Date can\'t be before Start Date', { extensions: { code: 'VALIDATION_ERROR', }, @@ -371,7 +372,8 @@ const resolvers = { notificationChanges.push('Name') } if (phaseName && cohort.phase.toString() !== phase.id.toString()) { - cohort.phase = phase.id + cohort.phase = phase.id; + addNewAttendanceWeek(); notificationChanges.push('Phase') } @@ -399,10 +401,8 @@ const resolvers = { if (notificationChanges.length) { pushNotification( coordinator.id, - `${ - role[0].toUpperCase() + role.slice(1) - } has made the following changes to "${ - cohort.name + `${role[0].toUpperCase() + role.slice(1) + } has made the following changes to "${cohort.name }": ${notificationChanges.join(', ')}`, senderId ) diff --git a/src/resolvers/coordinatorResolvers.ts b/src/resolvers/coordinatorResolvers.ts index b21e64ec..25c75037 100644 --- a/src/resolvers/coordinatorResolvers.ts +++ b/src/resolvers/coordinatorResolvers.ts @@ -434,51 +434,51 @@ const manageStudentResolvers = { if (!user.team) { // add trainee to attendance - if (role === RoleOfUser.COORDINATOR) { - const attendanceRecords: any = Attendance.find({ - coordinatorId: userId, - }) - - const traineeArray = (await attendanceRecords).map( - (data: any) => data.trainees - ) - - let traineeEmailExists = false - for (const weekTrainees of traineeArray) { - for (const trainee of weekTrainees) { - if (trainee.traineeEmail === email) { - traineeEmailExists = true - break - } - } - } - if (!traineeEmailExists) { - // create new trainee - const newTrainee: Trainee = { - traineeId: user.id, - traineeEmail: email, - status: [], - } - - const attendanceLength: any = await Attendance.find({ - coordinatorId: userId, - }) - - if (attendanceLength.length > 0) { - for (const attendData of attendanceLength) { - attendData.trainees.push(newTrainee) - await attendData.save() - } - } else { - const newAttendRecord = new Attendance({ - week: 1, - coordinatorId: [userId], - trainees: [newTrainee], - }) - await newAttendRecord.save() - } - } - } + // if (role === RoleOfUser.COORDINATOR) { + // const attendanceRecords: any = Attendance.find({ + // coordinatorId: userId, + // }) + + // const traineeArray = (await attendanceRecords).map( + // (data: any) => data.trainees + // ) + + // let traineeEmailExists = false + // for (const weekTrainees of traineeArray) { + // for (const trainee of weekTrainees) { + // if (trainee.traineeEmail === email) { + // traineeEmailExists = true + // break + // } + // } + // } + // if (!traineeEmailExists) { + // // create new trainee + // const newTrainee: Trainee = { + // traineeId: user.id, + // traineeEmail: email, + // status: [], + // } + + // const attendanceLength: any = await Attendance.find({ + // coordinatorId: userId, + // }) + + // if (attendanceLength.length > 0) { + // for (const attendData of attendanceLength) { + // attendData.trainees.push(newTrainee) + // await attendData.save() + // } + // } else { + // const newAttendRecord = new Attendance({ + // week: 1, + // coordinatorId: [userId], + // trainees: [newTrainee], + // }) + // await newAttendRecord.save() + // } + // } + // } user.team = team.id user.cohort = team.cohort.id diff --git a/src/resolvers/team.resolvers.ts b/src/resolvers/team.resolvers.ts index 7e67e68d..d6ef7b82 100644 --- a/src/resolvers/team.resolvers.ts +++ b/src/resolvers/team.resolvers.ts @@ -14,6 +14,7 @@ import { Rating } from '../models/ratings' import { pushNotification } from '../utils/notification/pushNotification' import { Types } from 'mongoose' import { GraphQLError } from 'graphql' +import { addNewAttendanceWeek } from '../utils/cron-jobs/team-jobs' const resolvers = { Team: { @@ -48,6 +49,23 @@ const resolvers = { }, }, Query: { + getTTLTeams: async (_: any, { orgToken }: any, context: Context) => { + try { + const { userId } = (await checkUserLoggedIn(context))([RoleOfUser.TTL]) + const org = await checkLoggedInOrganization(orgToken) + + const teams = await Team.find({ organization: org, ttl: userId }).populate('phase') + return teams; + + } catch (error) { + const { message } = error as { message: any } + throw new GraphQLError(message.toString(), { + extensions: { + code: '500', + }, + }) + } + }, getAllTeams: async (_: any, { orgToken }: any, context: Context) => { try { // some validations @@ -156,7 +174,6 @@ const resolvers = { }, }) ).filter((item: any) => { - console.log(item) const org = (item.program as InstanceType) ?.organization @@ -271,8 +288,8 @@ const resolvers = { try { const { name, cohortName, orgToken, startingPhase, ttlEmail } = args - // some validations - ;(await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN, RoleOfUser.ADMIN, RoleOfUser.MANAGER]) + // some validations + ; (await checkUserLoggedIn(context))([RoleOfUser.SUPER_ADMIN, RoleOfUser.ADMIN, RoleOfUser.MANAGER]) const cohort = await Cohort.findOne({ name: cohortName }) const organ = await checkLoggedInOrganization(orgToken) @@ -335,6 +352,7 @@ const resolvers = { senderId ) + addNewAttendanceWeek(); return newTeam } catch (error: any) { const { message } = error as { message: any } @@ -346,7 +364,7 @@ const resolvers = { } }, deleteTeam: async (parent: any, args: any, context: Context) => { - ;(await checkUserLoggedIn(context))([RoleOfUser.ADMIN, RoleOfUser.MANAGER]) + ; (await checkUserLoggedIn(context))([RoleOfUser.ADMIN, RoleOfUser.MANAGER]) const findTeam = await Team.findById(args.id) if (!findTeam) throw new Error('The Team you want to delete does not exist') @@ -362,8 +380,6 @@ const resolvers = { await Team.findByIdAndDelete({ _id: args.id }) cohort ? (cohort.teams = cohort.teams - 1) : null cohort?.save() - cohort && - console.log('done-----------------', cohort.coordinator.toString()) const senderId = new Types.ObjectId(context.userId) cohort && @@ -591,6 +607,7 @@ const resolvers = { strictPopulate: false, }) + addNewAttendanceWeek(); return updatedteam }, }, diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index 9c69c06e..c1b25467 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -34,6 +34,7 @@ import organizationRejectedTemplate from '../utils/templates/organizationRejecte import registrationRequest from '../utils/templates/registrationRequestTemplate' import { EmailPattern } from '../utils/validation.utils' import { Context } from './../context' +import { UserInputError } from 'apollo-server' const octokit = new Octokit({ auth: `${process.env.Org_Repo_Access}` }) const SECRET: string = process.env.SECRET as string @@ -87,7 +88,7 @@ async function logGeoActivity(user: 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() }, @@ -141,7 +142,7 @@ const resolvers: any = { { organisation, username }: any, context: Context ) { - ;(await checkUserLoggedIn(context))([ + ; (await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.COORDINATOR, 'trainee', @@ -153,7 +154,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 @@ -234,6 +235,12 @@ const resolvers: any = { } }, }, + Login: { + user: async (parent: any) => { + const user = await User.findById(parent.user.id) + return user; + } + }, Mutation: { async createUser( _: any, @@ -364,8 +371,7 @@ const resolvers: any = { }) } else if (user?.status?.status !== 'active') { throw new GraphQLError( - `Your account have been ${ - user?.status?.status ?? user?.status + `Your account have been ${user?.status?.status ?? user?.status }, please contact your organization admin for assistance`, { extensions: { @@ -622,9 +628,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, @@ -850,7 +856,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) { @@ -920,7 +926,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) { @@ -985,7 +991,7 @@ const resolvers: any = { { name, gitHubOrganisation }: any, context: Context ) { - ;(await checkUserLoggedIn(context))([ + ; (await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN, ]) @@ -1078,7 +1084,7 @@ const resolvers: any = { }, async deleteOrganization(_: any, { id }: any, context: Context) { - ;(await checkUserLoggedIn(context))([ + ; (await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN, ]) @@ -1086,7 +1092,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 }) @@ -1164,7 +1170,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() diff --git a/src/schema/index.ts b/src/schema/index.ts index 37d77761..ae74e63a 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -57,6 +57,7 @@ const Schema = gql` members: [User] startingPhase: DateTime active: Boolean + isJobActive: Boolean organization: Organization phase: Phase manager: User @@ -301,7 +302,8 @@ const Schema = gql` fetchRatingByCohort(CohortName: String): [Rating] fetchCohortsCoordinator(cohortName: ID!): [Cohort] verifyResetPasswordToken(token: String!): String - getAllTeams(orgToken: String): [Team!] + getTTLTeams(orgToken: String): [Team!]! + getAllTeams(orgToken: String): [Team!]! getAllTeamInCohort(orgToken: String, cohort: String): [Team!] gitHubActivity(organisation: String!, username: String!): GitHubActivity! } @@ -628,8 +630,52 @@ const Schema = gql` attendancePerc: String! } + type AttendanceDatesData { + date: String! + isValid: Boolean! + } + type AttendanceDates { + mon: AttendanceDatesData! + tue: AttendanceDatesData! + wed: AttendanceDatesData! + thu: AttendanceDatesData! + fri: AttendanceDatesData! + } + type TraineeAttendanceData { + trainee: User! + score: Int! + } + type AttendanceDays { + mon: [TraineeAttendanceData]! + tue: [TraineeAttendanceData]! + wed: [TraineeAttendanceData]! + thu: [TraineeAttendanceData]! + fri: [TraineeAttendanceData]! + } + type AttendanceWeeks { + phase: Phase! + weeks: [Int!] + } + type FilteredAttendance { + week: Int! + phase: Phase! + dates: AttendanceDates! + days: AttendanceDays! + } + type SanitizedAttendance { + today: String! + yesterday: String! + attendanceWeeks: [AttendanceWeeks!]! + attendance: [FilteredAttendance!]! + } + + type PauseAndResumeTeamAttendance { + team: Team! + sanitizedAttendance: SanitizedAttendance! + } + type Query { - getTeamAttendance(orgToken: String, team: String!): [Attendance] + getTeamAttendance(orgToken: String, team: String!): SanitizedAttendance getTraineeAttendanceByID(traineeEmail: String!): [weeklyAttendance] getAttendanceStats(orgToken: String!): [AttendanceStats] } @@ -637,30 +683,36 @@ const Schema = gql` recordAttendance( week: Int! team: String! - date: String! + today: Boolean! + yesterday: Boolean! trainees: [TraineeInput!]! orgToken: String! - ): AttendanceTeam + ): SanitizedAttendance + + pauseAndResumeTeamAttendance( + team: String! + orgToken: String + ): PauseAndResumeTeamAttendance updateAttendance( week: Int! + day: String! team: String! phase: String! trainees: [TraineeInput!]! orgToken: String! - ): [Attendance] + ): SanitizedAttendance - deleteAttendance(week: String!, team: String!, day: String!): [Attendance] - } - - input StatusInput { - day: String! - score: String! + deleteAttendance( + week: Int!, + team: String!, + day: String! + ): SanitizedAttendance } input TraineeInput { trainee: ID! - status: StatusInput! + score: Int! } type Session { id: String diff --git a/src/utils/cron-jobs/team-jobs.ts b/src/utils/cron-jobs/team-jobs.ts new file mode 100644 index 00000000..489a9f39 --- /dev/null +++ b/src/utils/cron-jobs/team-jobs.ts @@ -0,0 +1,124 @@ +import cron from 'node-cron'; +import Team from '../../models/team.model'; +import { Attendance } from '../../models/attendance.model'; +import Cohort, { CohortInterface } from '../../models/cohort.model'; +import { isSameWeek } from 'date-fns'; +import mongoose from 'mongoose'; + +export const addNewAttendanceWeek = async () => { + try { + const completedTeamsId: string[] = []; + const cohorts = await Cohort.find({ active: true }); + for (const cohort of cohorts) { + const attendances = await Attendance.find({ cohort: cohort._id, phase: cohort.phase }); + if (!attendances.length) { + const teams = await Team.find({ cohort, active: true, isJobActive: true }); + await Attendance.create({ + week: 1, + phase: cohort.phase, + cohort: cohort, + teams: teams.map(team => { + const phase = (team.phase as mongoose.Types.ObjectId); + if (phase && phase.equals(cohort.phase.toString())) { + completedTeamsId.push(team._id.toString()); + return { team, trainees: [] }; + } + if (!phase) { + completedTeamsId.push(team._id.toString()); + return { team, trainees: [] }; + } + }).filter(team => team) + }) + } + } + + const teams = await Team.find({ active: true, isJobActive: true }).populate('cohort'); + + for (const team of teams) { + const phase = team.phase || (team.cohort as CohortInterface).phase + const attendances = await Attendance.find({ cohort: (team.cohort as CohortInterface)._id, phase: phase }); + + let lastWeek = 0; + let attendanceIndex: number | undefined; + let teamAttendanceDate; + for (let index = 0; index < attendances.length; index++) { + const attendance = attendances[index]; + if (attendance.week > lastWeek) { + for (const teamAttendance of attendance.teams) { + if (team._id.equals(teamAttendance.team)) { + lastWeek = attendance.week + attendanceIndex = index; + teamAttendanceDate = teamAttendance.date; + } + } + } + } + + if (lastWeek && lastWeek < 43 && teamAttendanceDate && (attendanceIndex! >= 0)) { + + const isInSameWeek = isSameWeek( + new Date(), + new Date(teamAttendanceDate), + { + weekStartsOn: 1 + } + ); + if (!isInSameWeek && (new Date().getTime() > new Date(teamAttendanceDate).getTime())) { + const attendanceExist = await Attendance.findOne({ week: (lastWeek + 1), phase: phase, cohort: (team.cohort as CohortInterface)._id }) + if (attendanceExist) { + completedTeamsId.push(team._id.toString()); + attendanceExist.teams.push({ + team: team._id, + trainees: [] + }) + await attendanceExist.save() + } else { + const tempTeams = await Team.find({ active: true, isJobActive: true, cohort: (team.cohort as CohortInterface)._id }).populate('cohort') + await Attendance.create({ + week: (lastWeek + 1), + phase, + cohort: (team.cohort as CohortInterface)._id, + teams: tempTeams.map(team => { + if (!completedTeamsId.includes(team._id.toString()) && team.phase && (team.phase as mongoose.Types.ObjectId).equals(phase.toString())) { + return { team, trainees: [] }; + } + if (!completedTeamsId.includes(team._id.toString())) { + return { team, trainees: [] }; + } + }).filter(team => team) + }) + } + } + } + if (attendances.length && attendanceIndex === undefined) { + const tempAttendance = await Attendance.findOne({ week: (lastWeek + 1), phase, cohort: (team.cohort as CohortInterface)._id }).populate('cohort') + tempAttendance?.teams.push({ + team: team._id, + trainees: [] + }) + await tempAttendance?.save(); + } + if (!attendances.length) { + const tempTeams = await Team.find({ active: true, isJobActive: true, cohort: (team.cohort as CohortInterface)._id }).populate('cohort') + await Attendance.create({ + week: (lastWeek + 1), + phase, + cohort: (team.cohort as CohortInterface)._id, + teams: tempTeams.map(team => { + const isPhaseTrue = (team.phase && (team.phase as mongoose.Types.ObjectId).equals(phase.toString())) || ((team.cohort as CohortInterface).phase as unknown as mongoose.Types.ObjectId).equals(phase.toString()) + if (!completedTeamsId.includes(team._id.toString()) && isPhaseTrue) { + return { team, trainees: [] }; + } + }).filter(team => team) + }) + } + } + } catch (error) { + console.error('Error in scheduled job:', error); + } +} + +// Schedule a job to add a new week on monday +cron.schedule('0 0 * * 1', async () => { + addNewAttendanceWeek(); +}); \ No newline at end of file diff --git a/src/utils/getDateForDays.ts b/src/utils/getDateForDays.ts new file mode 100644 index 00000000..08b0500c --- /dev/null +++ b/src/utils/getDateForDays.ts @@ -0,0 +1,77 @@ +import { WeekdaysInterface } from '../resolvers/attendance.resolvers'; + +const handleAttendanceDay = (dayDate: string) => { + const today = new Date(); + const input = new Date(dayDate); + + today.setHours(0, 0, 0, 0); + input.setHours(0, 0, 0, 0); + + const previousDay = new Date(today); + previousDay.setDate(today.getDate() - 1); + + if (input.getTime() === today.getTime()) { + return true; + } + + if (input.getTime() === previousDay.getTime()) { + return true; + } + + // Check if today is Monday and selectedDayDate is last Friday + if (today.getDay() === 1 && input.getDay() === 5) { + const lastFriday = new Date(today); + lastFriday.setDate(today.getDate() - 3); + + return input.getTime() === lastFriday.getTime(); + } + + return false; +}; + +const days: ('mon' | 'tue' | 'wed' | 'thu' | 'fri')[] = ['mon', 'tue', 'wed', 'thu', 'fri']; + +export const getDateForDays = (inputDate: string) => { + try { + const date = new Date(Number(inputDate)); + let dayOfWeek = date.getDay(); + + if (date.getUTCHours() >= 22) { + date.setUTCDate(date.getUTCDate() + 1); + date.setUTCHours(0, 0, 0, 0); // Set the time to midnight of the next day + } + + if (dayOfWeek === 0) { + dayOfWeek = 7 + } + const dateObj: WeekdaysInterface = { + mon: { date: '', isValid: false }, + tue: { date: '', isValid: false }, + wed: { date: '', isValid: false }, + thu: { date: '', isValid: false }, + fri: { date: '', isValid: false } + }; + + for (let i = 1; i <= 5; i++) { + const daysToAdd = i - dayOfWeek; + const weekdayDate = new Date(date); + weekdayDate.setDate(date.getDate() + daysToAdd); + + dateObj[days[i - 1]].date = weekdayDate.toISOString().split('T')[0]; + dateObj[days[i - 1]].isValid = handleAttendanceDay(weekdayDate.toISOString()) + } + + return dateObj; + } catch (error) { + return { + mon: { date: '', isValid: false }, + tue: { date: '', isValid: false }, + wed: { date: '', isValid: false }, + thu: { date: '', isValid: false }, + fri: { date: '', isValid: false } + } + } + +}; + + From 1c5467c0ac80c6923f8b6051840a75e95009bb65 Mon Sep 17 00:00:00 2001 From: aimedivin Date: Sun, 13 Oct 2024 23:16:16 +0200 Subject: [PATCH 2/3] fix(attendance): move attendance calculation logic from client to server --- src/resolvers/attendance.resolvers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolvers/attendance.resolvers.ts b/src/resolvers/attendance.resolvers.ts index 8cfde952..a59267f6 100644 --- a/src/resolvers/attendance.resolvers.ts +++ b/src/resolvers/attendance.resolvers.ts @@ -12,7 +12,7 @@ import { CohortInterface } from '../models/cohort.model' import { GraphQLError } from 'graphql' import { checkLoggedInOrganization } from '../helpers/organization.helper' import { getDateForDays } from '../utils/getDateForDays' -import { format, isSameWeek } from 'date-fns' +import { isSameWeek } from 'date-fns' import { id } from 'date-fns/locale' import { AnyAaaaRecord } from 'dns' import { addNewAttendanceWeek } from '../utils/cron-jobs/team-jobs' From 26fa2e4a62b8a8a06a05ed528dfda86c47388cb3 Mon Sep 17 00:00:00 2001 From: "Gisa M. Caleb Pacifique" Date: Tue, 29 Oct 2024 18:21:13 +0200 Subject: [PATCH 3/3] fix-trainee-attendance --- src/resolvers/attendance.resolvers.ts | 549 ++++++++++++++++---------- src/resolvers/coordinatorResolvers.ts | 4 +- src/resolvers/userResolver.ts | 31 +- src/schema/index.ts | 27 +- src/seeders/users.seed.ts | 12 +- 5 files changed, 380 insertions(+), 243 deletions(-) diff --git a/src/resolvers/attendance.resolvers.ts b/src/resolvers/attendance.resolvers.ts index a59267f6..7d993525 100644 --- a/src/resolvers/attendance.resolvers.ts +++ b/src/resolvers/attendance.resolvers.ts @@ -1,10 +1,9 @@ /* eslint-disable indent */ import { Attendance } from '../models/attendance.model' -import { IntegerType, ObjectId } from 'mongodb' +import { ObjectId } from 'mongodb' import { Context } from './../context' import mongoose, { Document, Error, Types } from 'mongoose' import { checkUserLoggedIn } from '../helpers/user.helpers' -import { pushNotification } from '../utils/notification/pushNotification' import Phase, { PhaseInterface } from '../models/phase.model' import { RoleOfUser, User, UserInterface } from '../models/user' import Team, { TeamInterface } from '../models/team.model' @@ -12,10 +11,9 @@ import { CohortInterface } from '../models/cohort.model' import { GraphQLError } from 'graphql' import { checkLoggedInOrganization } from '../helpers/organization.helper' import { getDateForDays } from '../utils/getDateForDays' -import { isSameWeek } from 'date-fns' -import { id } from 'date-fns/locale' -import { AnyAaaaRecord } from 'dns' import { addNewAttendanceWeek } from '../utils/cron-jobs/team-jobs' +import { pushNotification } from '../utils/notification/pushNotification' +import { format } from 'date-fns' interface TraineeAttendanceStatus { day: 'mon' | 'tue' | 'wed' | 'thu' | 'fri' @@ -26,6 +24,17 @@ interface TraineeAttendanceData { trainee: ObjectId score: number } + +interface TraineeAttendanceWeek { + week: number + daysStatus: WeekdaysInterface +} + +interface TraineeAttendancePhase { + phase: PhaseInterface + weeks: TraineeAttendanceWeek[] +} + interface TeamAttendanceData { week: number phase: PhaseInterface @@ -36,8 +45,7 @@ interface TeamAttendanceData { trainees: Array<{ trainee: UserInterface status: Array - } - > + }> }> createdAt: string } @@ -55,89 +63,93 @@ interface AttendanceInput { interface TraineeAttendanceDataInterface { trainee?: { - id: string; - email: string; + id: string + email: string status: { status: string - }; + } profile: { - name: string; - }; - }; - score?: number; + name: string + } + } + score?: number } interface DayInterface { date: string isValid: boolean + score?: string | null } export interface WeekdaysInterface { - mon: DayInterface; - tue: DayInterface; - wed: DayInterface; - thu: DayInterface; - fri: DayInterface; + mon: DayInterface + tue: DayInterface + wed: DayInterface + thu: DayInterface + fri: DayInterface } interface TraineeAttendanceDayInterface { - week: number; + week: number phase: { id: string name: string - }; - dates: WeekdaysInterface; + } + dates: WeekdaysInterface days: { - mon: TraineeAttendanceDataInterface[]; - tue: TraineeAttendanceDataInterface[]; - wed: TraineeAttendanceDataInterface[]; - thu: TraineeAttendanceDataInterface[]; - fri: TraineeAttendanceDataInterface[]; - }; + mon: TraineeAttendanceDataInterface[] + tue: TraineeAttendanceDataInterface[] + wed: TraineeAttendanceDataInterface[] + thu: TraineeAttendanceDataInterface[] + fri: TraineeAttendanceDataInterface[] + } } interface AttendanceWeeksInterface { phase: { - name: string, + name: string id: string } weeks: Array } -const formatAttendanceData = (data: TeamAttendanceData[], teamData: TeamInterface) => { - const tempPhases: PhaseInterface[] = []; +const formatAttendanceData = ( + data: TeamAttendanceData[], + teamData: TeamInterface +) => { + const tempPhases: PhaseInterface[] = [] const attendanceWeeks: AttendanceWeeksInterface[] = [] const phase = { id: teamData.phase?._id.toString() || teamData.cohort!.phase._id.toString(), - name: teamData.phase?.name || teamData.cohort!.phase.name + name: teamData.phase?.name || teamData.cohort!.phase.name, } - const attendanceResult: TraineeAttendanceDayInterface[] = []; - data.forEach(attendance => { + const attendanceResult: TraineeAttendanceDayInterface[] = [] + data.forEach((attendance) => { const result: TraineeAttendanceDayInterface = { week: attendance.week, dates: { mon: { date: '', - isValid: false + isValid: false, }, tue: { date: '', - isValid: false + isValid: false, }, wed: { date: '', - isValid: false + isValid: false, }, thu: { date: '', - isValid: false + isValid: false, }, fri: { date: '', - isValid: false + isValid: false, }, }, phase: { id: attendance.phase._id.toString(), - name: attendance.phase.name + name: attendance.phase.name, }, days: { mon: [], @@ -146,34 +158,38 @@ const formatAttendanceData = (data: TeamAttendanceData[], teamData: TeamInterfac thu: [], fri: [], }, - }; + } // Store all attendance weeks let isWeekSet = false attendanceWeeks.forEach((week, index) => { if (week.phase.id === attendance.phase._id.toString()) { - isWeekSet = true; + isWeekSet = true attendanceWeeks[index].weeks.push(attendance.week) } }) - !isWeekSet && attendanceWeeks.push({ - phase: { - id: attendance.phase._id.toString(), - name: attendance.phase.name - }, - weeks: [attendance.week] - }) + !isWeekSet && + attendanceWeeks.push({ + phase: { + id: attendance.phase._id.toString(), + name: attendance.phase.name, + }, + weeks: [attendance.week], + }) if (!tempPhases.find((p) => p._id.equals(attendance.phase._id))) - tempPhases.push(attendance.phase); - let date = attendance.teams[0].date; + tempPhases.push(attendance.phase) + let date = attendance.teams[0].date attendance.teams[0].trainees.forEach((traineeData) => { - if (traineeData.status.length && traineeData.trainee.status.status !== 'drop') { + if ( + traineeData.status.length && + traineeData.trainee.status.status !== 'drop' + ) { traineeData.status.forEach((traineeStatus) => { if (traineeStatus.date && !date) { - date = traineeStatus.date; + date = traineeStatus.date } result.days[ @@ -182,25 +198,27 @@ const formatAttendanceData = (data: TeamAttendanceData[], teamData: TeamInterfac trainee: { ...(traineeData.trainee as unknown as Document).toObject(), profile: { - name: (traineeData.trainee.profile! as any).name + name: (traineeData.trainee.profile! as any).name, }, id: traineeData.trainee._id.toString(), }, score: traineeStatus.score, - }); - }); + }) + }) } - }); + }) result.dates = getDateForDays(date); attendanceResult.push(result); - }); - + }) - const today = new Date(); - const yesterday = new Date().getDay() === 1 ? new Date().setDate(new Date().getDate() - 3) : new Date().setDate(new Date().getDate() - 1); + const today = new Date() + const yesterday = + new Date().getDay() === 1 + ? new Date().setDate(new Date().getDate() - 3) + : new Date().setDate(new Date().getDate() - 1) return { attendanceWeeks, attendance: attendanceResult, today, yesterday } -}; +} const validateAttendance = async ( team: string, @@ -213,11 +231,14 @@ const validateAttendance = async ( if (!org) { throw new Error('Organisation doesn\'t exist') } - const { userId }: any = (await checkUserLoggedIn(context))(['coordinator', 'ttl']) + const { userId }: any = (await checkUserLoggedIn(context))([ + 'coordinator', + 'ttl', + ]) const teamData = await Team.findById(team) .populate({ path: 'members', - match: { role: 'trainee' } + match: { role: 'trainee' }, }) .populate('cohort') .populate('phase') @@ -233,29 +254,37 @@ const validateAttendance = async ( throw new Error('Phase provided doesn\'t exist') } teamData.members.forEach((member) => { - const trainee = member as UserInterface; + const trainee = member as UserInterface if (trainee.role === 'trainee' && trainee.status.status === 'active') { - const sentTestTrainee = trainees.find(traineeData => trainee._id.equals(traineeData.trainee)) + const sentTestTrainee = trainees.find((traineeData) => + trainee._id.equals(traineeData.trainee) + ) if (!sentTestTrainee && !isUpdating) { - throw new GraphQLError('Please ensure attendance is taken for all active trainees in the team', { - extensions: { - code: 'INCONSISTENT_TRAINEE_ATTENDANCE', - }, - }) + throw new GraphQLError( + 'Please ensure attendance is taken for all active trainees in the team', + { + extensions: { + code: 'INCONSISTENT_TRAINEE_ATTENDANCE', + }, + } + ) } if (sentTestTrainee && ![0, 1, 2].includes(sentTestTrainee.score)) { - throw new GraphQLError('Attendance cannot be recorded due to an invalid score for one of trainees.', { - extensions: { - code: 'INVALID_TRAINEE_SCORE', - }, - }) + throw new GraphQLError( + 'Attendance cannot be recorded due to an invalid score for one of trainees.', + { + extensions: { + code: 'INVALID_TRAINEE_SCORE', + }, + } + ) } } }) return { teamData, phaseData, - userId + userId, } } @@ -269,16 +298,17 @@ const returnAttendanceData = async (teamData: any) => { select: '-password', populate: { path: 'profile', - } + }, }) const sanitizedAttendance: any[] = [] attendances.forEach((attendance) => { - const result = attendance.teams.find((teamAttendanceData) => (teamAttendanceData.team as ObjectId).equals(teamData.id) ) - const filteredTrainees = result?.trainees.filter(trainee => (trainee.trainee as UserInterface).status.status !== 'drop') + const filteredTrainees = result?.trainees.filter( + (trainee) => (trainee.trainee as UserInterface).status.status !== 'drop' + ) result && sanitizedAttendance.push({ @@ -292,7 +322,16 @@ const returnAttendanceData = async (teamData: any) => { ...(attendance.phase as mongoose.Document).toObject(), id: (attendance.phase as mongoose.Document)._id, }, - teams: [{ date: result.date, team: { ...(result.team as unknown as mongoose.Document).toObject(), id: (result.team as unknown as mongoose.Document)._id }, trainees: filteredTrainees }], + teams: [ + { + date: result.date, + team: { + ...(result.team as unknown as mongoose.Document).toObject(), + id: (result.team as unknown as mongoose.Document)._id, + }, + trainees: filteredTrainees, + }, + ], }) }) return formatAttendanceData(sanitizedAttendance, teamData) @@ -300,23 +339,99 @@ const returnAttendanceData = async (teamData: any) => { const attendanceResolver = { Query: { - async getTraineeAttendanceByID( - _: any, - { traineeEmail }: any, - context: Context - ) { - ; (await checkUserLoggedIn(context))([RoleOfUser.TRAINEE]) + + async getTraineeAttendance(_: any, __: any, context: Context) { + const { userId } = (await checkUserLoggedIn(context))([ + RoleOfUser.TRAINEE, + ]) + const user = await User.findById(userId) + const userTeamId = user?.team + const teamData = await Team.findById(userTeamId) const attendance = await Attendance.find() + const phases: TraineeAttendancePhase[] = [] - const weeklyAttendance = attendance.map((week: any) => { - const weekNumber = week.week - const trainee = week.trainees.filter((trainee: any) => - trainee.traineeEmail.includes(traineeEmail) - ) - const traineeAttendance = trainee[0]?.status - return { weekNumber, traineeAttendance } - }) - return weeklyAttendance + if (!teamData) { + throw new Error('Team provided doesn\'t exist') + } + + if (attendance.length) { + 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 + + attendanceRecord.teams.forEach((traineeAttendanceData) => { + if ( + traineeAttendanceData.team.equals( + (userTeamId as string).toString() + ) + ) { + traineeAttendanceData.trainees.forEach((traineeData) => { + if ( + (traineeData.trainee as string).toString() === userId && + traineeAttendanceData.date + ) { + 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() + } + } + }) + + 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.weeks.push({ + week: attendanceRecord.week, + daysStatus: weekDays, + }) + phaseExists = true + } + }) + + if (!phaseExists) { + phases.push({ + phase: phaseObj, + weeks: [ + { + week: attendanceRecord.week, + daysStatus: weekDays, + }, + ], + }) + } + } + }) + } + }) + } + } + + return { + traineeId: userId, + teamName: teamData.name, + phases, + } }, async getTeamAttendance( @@ -324,17 +439,20 @@ const attendanceResolver = { { team }: { team: string }, context: Context ) { - (await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR, RoleOfUser.TTL]) + ; (await checkUserLoggedIn(context))([ + RoleOfUser.COORDINATOR, + RoleOfUser.TTL, + ]) - await addNewAttendanceWeek(); + await addNewAttendanceWeek() const teamData = await Team.findById(team) .populate('phase') .populate({ path: 'cohort', populate: { - path: 'phase' - } + path: 'phase', + }, }) if (!teamData) { @@ -344,83 +462,37 @@ const attendanceResolver = { return returnAttendanceData(teamData) }, - async getAttendanceStats(_: any, args: any, context: Context) { - ; (await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR]) - const { userId } = (await checkUserLoggedIn(context))([RoleOfUser.COORDINATOR]) - const attendances: any = await Attendance.find({ coordinatorId: userId }) - - //calculate statistic - const attendanceStats: { - week: any - traineesStatistics: { traineeId: any; attendancePerc: number }[] - }[] = [] - - attendances.forEach((weekData: any) => { - const week = weekData.week - const trainees = weekData.trainees - const weekAttendanceStats: { - traineeId: any - attendancePerc: number - }[] = [] - - // Iterate through trainees - trainees.forEach((trainee: any) => { - const traineeId = trainee.traineeId - const attendanceRecods = trainee.status - let attendedCount: any = 0 - const totalCount: any = 0 - - //count attendance recods - attendanceRecods.forEach((recods: any) => { - if (recods.value === 2) { - attendedCount++ - } - // if (recods.value !== 0) { - // totalCount++ - // } - }) - - // calculate attendance per trainee in one week - const attendancePerc = - totalCount > 0 ? (attendedCount / totalCount) * 100 : 0 - - weekAttendanceStats.push({ - traineeId, - attendancePerc: attendancePerc > 50 ? 1 : 0, - }) - }) - - attendanceStats.push({ - week, - traineesStatistics: weekAttendanceStats, - }) - }) - - return attendanceStats - }, }, Mutation: { - async pauseAndResumeTeamAttendance(_: any, { orgToken, team }: { orgToken: string, team: string }, context: Context) { - (await checkUserLoggedIn(context))(['coordinator', 'ttl']); + async pauseAndResumeTeamAttendance( + _: any, + { orgToken, team }: { orgToken: string; team: string }, + context: Context + ) { + ; (await checkUserLoggedIn(context))(['coordinator', 'ttl']) - const teamData = await Team.findById(team).populate({ - path: 'members', - match: { role: 'trainee' } - }) + const teamData = await Team.findById(team) + .populate({ + path: 'members', + match: { role: 'trainee' }, + }) .populate('cohort') .populate('phase') - .populate('cohort.phase'); + .populate('cohort.phase') if (!teamData) { throw new Error('Team provided doesn\'t exist') } const tempIsJobActive = teamData.isJobActive - teamData.isJobActive = !tempIsJobActive; - const savedTeam = await teamData.save(); + teamData.isJobActive = !tempIsJobActive + const savedTeam = await teamData.save() - !tempIsJobActive && await addNewAttendanceWeek(); - return { team: savedTeam, sanitizedAttendance: returnAttendanceData(teamData) } + !tempIsJobActive && (await addNewAttendanceWeek()) + return { + team: savedTeam, + sanitizedAttendance: returnAttendanceData(teamData), + } }, async recordAttendance( @@ -428,7 +500,6 @@ const attendanceResolver = { { week, trainees, team, today, yesterday, orgToken }: AttendanceInput, context: Context ) { - const { teamData, phaseData, userId } = await validateAttendance( team, orgToken, @@ -437,23 +508,25 @@ const attendanceResolver = { ) if (!today && !yesterday) { - throw new Error('Recording attendance is only allowed for today and the day before within work days.') + throw new Error( + 'Recording attendance is only allowed for today and the day before within work days.' + ) } if (today && yesterday) { throw new Error('Please select either today or yesterday, not both.') } - let date = (today && new Date()).toString(); + let date = (today && new Date()).toString() if (yesterday) { - const today = new Date(); + const today = new Date() if (today.getDay() === 1) { - const lastFriday = new Date(today); + const lastFriday = new Date(today) - lastFriday.setDate(today.getDate() - 3); + lastFriday.setDate(today.getDate() - 3) date = lastFriday.toString() } else { - const previousDay = new Date(today); - previousDay.setDate(today.getDate() - 1); + const previousDay = new Date(today) + previousDay.setDate(today.getDate() - 1) date = previousDay.toString() } } @@ -490,7 +563,9 @@ const attendanceResolver = { { date: new Date(date), score: trainees[i].score, - day: new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase(), + day: new Date(date) + .toLocaleDateString('en-US', { weekday: 'short' }) + .toLowerCase(), }, ], }) @@ -536,11 +611,17 @@ const attendanceResolver = { } ) .populate('teams.team') - .populate('teams.trainees.trainee', '-password'); - - attendants.forEach(attendant => { - pushNotification(attendant.trainee._id, `Your attendance for ${today ? 'today' : 'yesterday'} has been recorded, Kindly review your score.`, userId, 'attendance') - }); + .populate('teams.trainees.trainee', '-password') + + attendants.forEach((attendant) => { + pushNotification( + attendant.trainee._id, + `Your attendance for ${today ? 'today' : 'yesterday' + } has been recorded, Kindly review your score.`, + userId, + 'attendance' + ) + }) return savedAttendance?.teams[savedAttendance?.teams.length - 1] } @@ -555,17 +636,19 @@ const attendanceResolver = { (traineeData.trainee as UserInterface)._id.equals(trainees[i].trainee) ) if (traineeIndex === -1) { - traineeStatusUpdated = true; + traineeStatusUpdated = true const traineeData = await User.findOne( { _id: new ObjectId(trainees[i].trainee), team: teamData.id }, { password: 0 } ) if (traineeData) { - (attendance.teams[attendanceTeamIndex!].trainees as any[]).push({ + ; (attendance.teams[attendanceTeamIndex!].trainees as any[]).push({ trainee: traineeData, status: [ { - day: new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase(), + day: new Date(date) + .toLocaleDateString('en-US', { weekday: 'short' }) + .toLowerCase(), date: new Date(date), score: trainees[i].score, }, @@ -573,8 +656,8 @@ const attendanceResolver = { }) traineeIdsNotification.push({ id: traineeData._id, - score: trainees[i].score - }); + score: trainees[i].score, + }) } } else { if ( @@ -593,7 +676,13 @@ const attendanceResolver = { const existingDay = attendance.teams[attendanceTeamIndex!].trainees[ traineeIndex - ].status.find((s) => s.day === new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase()) + ].status.find( + (s) => + s.day === + new Date(date) + .toLocaleDateString('en-US', { weekday: 'short' }) + .toLowerCase() + ) if ( ( @@ -606,19 +695,19 @@ const attendanceResolver = { attendance.teams[attendanceTeamIndex!].trainees[ traineeIndex ].status.push({ - day: new Date(date).toLocaleDateString('en-US', { weekday: 'short' }).toLowerCase() as - | 'mon' - | 'tue' - | 'wed' - | 'thu' - | 'fri', + day: new Date(date) + .toLocaleDateString('en-US', { weekday: 'short' }) + .toLowerCase() as 'mon' | 'tue' | 'wed' | 'thu' | 'fri', date: new Date(date!), score: trainees[i].score, - }); + }) traineeIdsNotification.push({ - id: (attendance.teams[attendanceTeamIndex!].trainees[traineeIndex].trainee as UserInterface)._id, - score: trainees[i].score - }); + id: ( + attendance.teams[attendanceTeamIndex!].trainees[traineeIndex] + .trainee as UserInterface + )._id, + score: trainees[i].score, + }) } } } @@ -633,9 +722,17 @@ const attendanceResolver = { ) } - await attendance.save(); - traineeIdsNotification.forEach(trainee => { - pushNotification(trainee.id, `Your attendance for ${today ? 'today,' : 'yesterday,'} ${format(new Date(date), 'MMMM dd, yyyy')} has been recorded, Your score is ${trainee.score}.`, userId, 'attendance') + await attendance.save() + traineeIdsNotification.forEach((trainee) => { + pushNotification( + trainee.id, + `Your attendance for ${today ? 'today,' : 'yesterday,'} ${format( + new Date(date), + 'MMMM dd, yyyy' + )} has been recorded, Your score is ${trainee.score}.`, + userId, + 'attendance' + ) }) return returnAttendanceData(teamData) }, @@ -645,7 +742,6 @@ const attendanceResolver = { { week, trainees, team, orgToken, day, phase }: AttendanceInput, context: Context ) { - const { teamData, userId } = await validateAttendance( team, orgToken, @@ -656,7 +752,6 @@ const attendanceResolver = { const phaseData = await Phase.findById(phase) - if (!phaseData) { throw new Error('Phase provided doesn\'t exist') } @@ -677,9 +772,18 @@ const attendanceResolver = { }, }) } - const teamAttendanceTrainees = attendance.teams[teamToUpdateIndex!]; - const date = teamAttendanceTrainees.date ? format(new Date(getDateForDays(teamAttendanceTrainees.date.getTime().toString())[day].date), 'MMMM dd, yyyy') : day; - const traineeIdsNotification: { id: Types.ObjectId, score: number }[] = []; + const teamAttendanceTrainees = attendance.teams[teamToUpdateIndex!] + const date = teamAttendanceTrainees.date + ? format( + new Date( + getDateForDays(teamAttendanceTrainees.date.getTime().toString())[ + day + ].date + ), + 'MMMM dd, yyyy' + ) + : day + const traineeIdsNotification: { id: Types.ObjectId; score: number }[] = [] trainees.forEach((sentTrainee) => { let isDropped = false @@ -704,25 +808,33 @@ const attendanceResolver = { } if (traineeIndex !== -1 && !isDropped) { - const traineeToUpdateStatus = teamAttendanceTrainees.trainees[traineeIndex].status; + const traineeToUpdateStatus = + teamAttendanceTrainees.trainees[traineeIndex].status traineeToUpdateStatus.forEach((status) => { - if ( - status.day === day.toLowerCase() - ) { - (status.score != sentTrainee.score) && traineeIdsNotification.push({ - id: (teamAttendanceTrainees.trainees[traineeIndex].trainee as UserInterface)._id, - score: sentTrainee.score - }); - status.score = sentTrainee.score; + if (status.day === day.toLowerCase()) { + status.score != sentTrainee.score && + traineeIdsNotification.push({ + id: ( + teamAttendanceTrainees.trainees[traineeIndex] + .trainee as UserInterface + )._id, + score: sentTrainee.score, + }) + status.score = sentTrainee.score } }) } }) - await attendance.save(); - traineeIdsNotification.forEach(trainee => { - pushNotification(trainee.id, `Your attendance record for ${date}, has been updated, Your new score is ${trainee.score}.`, userId, 'attendance') + await attendance.save() + traineeIdsNotification.forEach((trainee) => { + pushNotification( + trainee.id, + `Your attendance record for ${date}, has been updated, Your new score is ${trainee.score}.`, + userId, + 'attendance' + ) }) return returnAttendanceData(teamData) }, @@ -732,7 +844,7 @@ const attendanceResolver = { { week, day, team }: { week: string; day: string; team: string }, context: Context ) { - (await checkUserLoggedIn(context))(['coordinator', 'ttl']) + ; (await checkUserLoggedIn(context))(['coordinator', 'ttl']) const teamData = await Team.findById(team) .populate('cohort') @@ -742,13 +854,14 @@ const attendanceResolver = { throw new Error('Team provided doesn\'t exist') } - const phase = teamData.phase || (teamData.cohort as CohortInterface).phase._id + const phase = + teamData.phase || (teamData.cohort as CohortInterface).phase._id const attendance = await Attendance.findOne({ phase: phase, week, cohort: teamData.cohort, - }).populate('teams.trainees.trainee', '-password'); + }).populate('teams.trainees.trainee', '-password') const attendanceTeamIndex = attendance?.teams.findIndex( (teamAttendanceData) => diff --git a/src/resolvers/coordinatorResolvers.ts b/src/resolvers/coordinatorResolvers.ts index 25c75037..24ff2ee7 100644 --- a/src/resolvers/coordinatorResolvers.ts +++ b/src/resolvers/coordinatorResolvers.ts @@ -149,9 +149,7 @@ const manageStudentResolvers = { return trainees.filter((user: any) => { if (role === RoleOfUser.ADMIN) { - return ( - user.team?.cohort?.program?.organization.name == org?.name - ) + return user.team?.cohort?.program?.organization.name == org?.name } if (role === RoleOfUser.MANAGER) { return ( diff --git a/src/resolvers/userResolver.ts b/src/resolvers/userResolver.ts index c1b25467..ba1c72f3 100644 --- a/src/resolvers/userResolver.ts +++ b/src/resolvers/userResolver.ts @@ -56,7 +56,7 @@ async function logGeoActivity(user: any) { const profile = await Profile.findOne({ user: user._id }) if (!profile) { - return; + return } if (geoData.country_code && geoData.city) { @@ -88,7 +88,7 @@ async function logGeoActivity(user: 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() }, @@ -142,7 +142,7 @@ const resolvers: any = { { organisation, username }: any, context: Context ) { - ; (await checkUserLoggedIn(context))([ + ;(await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.COORDINATOR, 'trainee', @@ -154,7 +154,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 @@ -238,8 +238,8 @@ const resolvers: any = { Login: { user: async (parent: any) => { const user = await User.findById(parent.user.id) - return user; - } + return user + }, }, Mutation: { async createUser( @@ -371,7 +371,8 @@ const resolvers: any = { }) } else if (user?.status?.status !== 'active') { throw new GraphQLError( - `Your account have been ${user?.status?.status ?? user?.status + `Your account have been ${ + user?.status?.status ?? user?.status }, please contact your organization admin for assistance`, { extensions: { @@ -628,9 +629,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, @@ -856,7 +857,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) { @@ -926,7 +927,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) { @@ -991,7 +992,7 @@ const resolvers: any = { { name, gitHubOrganisation }: any, context: Context ) { - ; (await checkUserLoggedIn(context))([ + ;(await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN, ]) @@ -1084,7 +1085,7 @@ const resolvers: any = { }, async deleteOrganization(_: any, { id }: any, context: Context) { - ; (await checkUserLoggedIn(context))([ + ;(await checkUserLoggedIn(context))([ RoleOfUser.ADMIN, RoleOfUser.SUPER_ADMIN, ]) @@ -1092,7 +1093,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 }) @@ -1170,7 +1171,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() diff --git a/src/schema/index.ts b/src/schema/index.ts index ae74e63a..d8d5c20d 100644 --- a/src/schema/index.ts +++ b/src/schema/index.ts @@ -7,6 +7,7 @@ const Schema = gql` coordinator: User } type Phase { + _id: ID name: String description: String } @@ -547,6 +548,22 @@ const Schema = gql` subDocuments: [Doc]! } + type traineeAttendanceWeek { + week: Int! + daysStatus: AttendanceDates! + } + + type traineeAttendancePhase { + phase: Phase! + weeks: [traineeAttendanceWeek]! + } + + type traineeAttendance { + traineeId: String! + teamName: String! + phases: [traineeAttendancePhase]! + } + type DocumentationInput { title: String! for: String! @@ -554,6 +571,7 @@ const Schema = gql` } type Query { getDocumentations: [Documentation] + getTraineeAttendance: traineeAttendance } type Mutation { @@ -633,6 +651,7 @@ const Schema = gql` type AttendanceDatesData { date: String! isValid: Boolean! + score: String } type AttendanceDates { mon: AttendanceDatesData! @@ -676,8 +695,6 @@ const Schema = gql` type Query { getTeamAttendance(orgToken: String, team: String!): SanitizedAttendance - getTraineeAttendanceByID(traineeEmail: String!): [weeklyAttendance] - getAttendanceStats(orgToken: String!): [AttendanceStats] } type Mutation { recordAttendance( @@ -704,10 +721,10 @@ const Schema = gql` ): SanitizedAttendance deleteAttendance( - week: Int!, - team: String!, + week: Int! + team: String! day: String! - ): SanitizedAttendance + ): SanitizedAttendance } input TraineeInput { diff --git a/src/seeders/users.seed.ts b/src/seeders/users.seed.ts index 0f959ea6..659e56d1 100644 --- a/src/seeders/users.seed.ts +++ b/src/seeders/users.seed.ts @@ -39,6 +39,12 @@ const seedUsers = async () => { email: 'muhedarius@gmail.com', githubUserName: '', }, + { + firstName: 'Kagabo', + lastName: 'Darius', + email: 'kagabodarius@gmail.com', + githubUserName: '', + }, { firstName: 'Ndayambaje', lastName: 'Virgile', @@ -101,7 +107,8 @@ const seedUsers = async () => { if ( registerUsers.filter( (user) => - user.organizations.includes(orgName) && user.role === RoleOfUser.ADMIN + user.organizations.includes(orgName) && + user.role === RoleOfUser.ADMIN ).length === usersTypes.admin ) break @@ -118,7 +125,8 @@ const seedUsers = async () => { if ( registerUsers.filter( (user) => - user.organizations.includes(orgName) && user.role === RoleOfUser.MANAGER + user.organizations.includes(orgName) && + user.role === RoleOfUser.MANAGER ).length === usersTypes.manager ) break