From 328dfce26715cef022fa3b5ce5b76618dbe5c585 Mon Sep 17 00:00:00 2001 From: Aime-Patrick Date: Thu, 28 Nov 2024 23:59:19 +0200 Subject: [PATCH] Implemented invitation functionality (#149) --- src/models/AuthUser.ts | 2 +- src/models/ShortlistedSchema.ts | 4 +- src/models/technicalAssessmentStage.ts | 12 ++- src/resolvers/Doc.ts | 2 +- src/resolvers/applicationStageResolver.ts | 108 +++++++++++++++++-- src/schema/applicationStage.ts | 123 ++++++++++++---------- src/schema/traineeApplicantSchema.ts | 2 +- src/seeders/users.ts | 2 +- 8 files changed, 178 insertions(+), 77 deletions(-) diff --git a/src/models/AuthUser.ts b/src/models/AuthUser.ts index 8aefc6b..0ee5224 100644 --- a/src/models/AuthUser.ts +++ b/src/models/AuthUser.ts @@ -39,7 +39,7 @@ const userSchema = new Schema( }, applicationPhase: { type: String, - enum: ["Applied", "Interviewed", "Accepted", "Enrolled"], + enum: ["Applied", 'Shortlisted', 'Technical Assessment', 'Interview Assessment', 'Admitted', 'Rejected', "Enrolled"], default: "Applied", }, isActive: { diff --git a/src/models/ShortlistedSchema.ts b/src/models/ShortlistedSchema.ts index cec05ef..9f42aef 100644 --- a/src/models/ShortlistedSchema.ts +++ b/src/models/ShortlistedSchema.ts @@ -2,7 +2,7 @@ import mongoose, { Schema, Document } from "mongoose"; interface IShortlisted extends Document { applicantId: mongoose.Schema.Types.ObjectId; - status: "No action" | "Moved" | "Rejected" | "Admitted"; + status: "No action" | "Invited" | "Moved" | "Rejected" | "Admitted"; comments?: string; } @@ -15,7 +15,7 @@ const shortlistedSchema = new Schema({ }, status: { type: String, - enum: ["No action", "Moved", "Rejected", "Admitted"], + enum: ["No action", "Invited", "Moved", "Rejected", "Admitted"], default: "No action", }, comments: { diff --git a/src/models/technicalAssessmentStage.ts b/src/models/technicalAssessmentStage.ts index e0932e9..1dc80f6 100644 --- a/src/models/technicalAssessmentStage.ts +++ b/src/models/technicalAssessmentStage.ts @@ -2,8 +2,10 @@ import mongoose, { Schema, Document } from "mongoose"; interface ITechnicalAssessment extends Document { applicantId: mongoose.Schema.Types.ObjectId; - status: "No action" | "Moved" | "Rejected" | "Admitted"; + status: "No action" | "Invited"| "Moved" | "Rejected" | "Admitted"; score: number; + invitationLink: string; + platform:string; comments?: string; } @@ -15,7 +17,7 @@ const technicalAssessmentSchema = new Schema({ }, status: { type: String, - enum: ["No action", "Moved", "Rejected", "Admitted"], + enum: ["No action", "Invited", "Moved", "Rejected", "Admitted"], default: "No action", }, score: { @@ -24,6 +26,12 @@ const technicalAssessmentSchema = new Schema({ max: 100, default:null }, + platform:{ + type:String, + }, + invitationLink:{ + type:String, + }, comments: { type: String, }, diff --git a/src/resolvers/Doc.ts b/src/resolvers/Doc.ts index c949ac6..bbcae54 100644 --- a/src/resolvers/Doc.ts +++ b/src/resolvers/Doc.ts @@ -27,7 +27,7 @@ export const docResolver:any={ return doc; }, async deleteDoc(_:any,args:any){ - const doc= await docModels.findOneAndDelete({id:args.id}) + const doc= await docModels.findOneAndDelete({_id:args.id}) return doc; }, async updateDoc(_:any,args:any){ diff --git a/src/resolvers/applicationStageResolver.ts b/src/resolvers/applicationStageResolver.ts index ba00176..e5b3251 100644 --- a/src/resolvers/applicationStageResolver.ts +++ b/src/resolvers/applicationStageResolver.ts @@ -31,10 +31,10 @@ const models = [ async function getApplicantsByModel(model: any) { return await model.find().populate("applicantId").exec(); } -async function updateApplicantAfterDismissed(model: any, applicantId: string) { +async function updateApplicantAfterRejected(model: any, applicantId: string) { await model.updateOne({ applicantId }, { $set: { status: "Rejected" } }); } -async function updateApplicantAfterAdmitted(model: any,applicantId:string) { +async function updateApplicantAfterAdmitted(model: any, applicantId: string) { await model.updateOne({ applicantId }, { $set: { status: "Admitted" } }); } export const applicationStageResolvers: any = { @@ -85,6 +85,8 @@ export const applicationStageResolvers: any = { status: tracking.status, score: tracking.score || tracking.interviewScore, comments: tracking.comments, + platform: tracking.platform, + invitationLink: tracking.invitationLink, createdAt: tracking.createdAt.toLocaleString(), updatedAt: tracking.updatedAt.toLocaleString(), })); @@ -148,7 +150,7 @@ export const applicationStageResolvers: any = { technicalStage, interviewStage, admittedStage, - dismissedStage, + rejectedStage, AllStages, ] = await Promise.all([ Shortlisted.findOne({ applicantId: trainee }), @@ -164,7 +166,7 @@ export const applicationStageResolvers: any = { technical: technicalStage, interview: interviewStage, admitted: admittedStage, - dismissed: dismissedStage, + rejected: rejectedStage, allStages: AllStages, }; } catch (error) { @@ -301,7 +303,7 @@ export const applicationStageResolvers: any = { await TraineeApplicant.updateOne( { _id: applicantId }, { $set: { applicationPhase: nextStage, status: "No action" } } - ); + ); message = `You have advanced to the ${nextStage} stage.`; const notification = await ApplicantNotificationsModel.create({ userId: user!._id, @@ -477,23 +479,23 @@ export const applicationStageResolvers: any = { } await Promise.all( models.map((model) => - updateApplicantAfterDismissed(model, applicantId) + updateApplicantAfterRejected(model, applicantId) ) ); - const stageDismissedFrom = await TraineeApplicant.findOne({ + const stageRejectedFrom = await TraineeApplicant.findOne({ _id: applicantId, }); await Rejected.create({ applicantId, - stageDismissedFrom: stageDismissedFrom?.applicationPhase, + stageRejectedFrom: stageRejectedFrom?.applicationPhase, comments, }); await TraineeApplicant.updateOne( { _id: applicantId }, { $set: { applicationPhase: "Rejected", status: "Rejected" } } ); - message = `You have been rejected from the ${stageDismissedFrom?.applicationPhase} stage.`; - + message = `You have been rejected from the ${stageRejectedFrom?.applicationPhase} stage.`; + const notification3 = await ApplicantNotificationsModel.create({ userId: user!._id, message, @@ -505,7 +507,7 @@ export const applicationStageResolvers: any = { "Application Update", `Hello ${user!.email.split("@")[0]}, `, `We are sorry to inform you that - your application has been rejected from the ${stageDismissedFrom?.applicationPhase} stage. + your application has been rejected from the ${stageRejectedFrom?.applicationPhase} stage.

You can always apply again. @@ -639,5 +641,89 @@ export const applicationStageResolvers: any = { return new Error(error.message); } }, + sendInvitation: async ( _: any, { applicantId, email, platform,invitationLink,}: { applicantId: string; email: string; platform: string; invitationLink: string;}, context: any) => { + try { + if (!context.currentUser) { + throw new CustomGraphQLError( + "You must be logged in to perform this action." + ); + } + + if (!email || !invitationLink) { + throw new CustomGraphQLError( + "Email and invitation link are required." + ); + } + + // Find the applicant + const isApplicantExist = await TechnicalAssessment.findOne({ + applicantId, + }) + .populate("applicantId") + .exec(); + + if (!isApplicantExist || !isApplicantExist.applicantId) { + throw new Error("Applicant not found or applicantId is missing."); + } + + const user = await LoggedUserModel.findOne({ email }); + const applicant = isApplicantExist.applicantId as any; + const firstName = applicant.firstName; + const lastName = applicant.lastName; + const notification = await ApplicantNotificationsModel.create({ + userId: user!._id, + message:"Invitation link has sent to your email address. Please check your email address", + eventType: "general", + }); + await pusher + .trigger(`notifications-${user!._id}`, "new-notification", { + message: notification.message, + id: notification._id, + createdAt: notification.createdAt, + read: notification.read, + }) + .catch((error) => { + console.error("Error with Pusher trigger:", error); + }); + await sendEmailTemplate( + email, + "Invitation to Complete Technical Assessment", + `Dear ${firstName} ${lastName},`, + `

+ We are excited to invite you to take the next step in your application process!
+ Please complete the following technical assessment to continue:
+ ${invitationLink}
+ The assessment will be hosted on the ${platform} platform. Please ensure that you have the necessary access and requirements ready. +

+

+ Once you've finished the assessment, we'll review your results and follow up with the next steps. +

+ `, + { + text: "Invitation link", + url: invitationLink, + } + ); + + await TraineeApplicant.updateOne( + { _id: applicantId }, + { $set: { status: "Invited" } } + ); + await TechnicalAssessment.updateOne( + { applicantId }, + { $set: { status: "Invited" ,invitationLink, platform} } + ); + + return { + success: true, + message: "Invitation sent successfully", + }; + } catch (error: any) { + return { + success: false, + message: error.message, + }; + } + }, }, }; diff --git a/src/schema/applicationStage.ts b/src/schema/applicationStage.ts index d33e37e..42af010 100644 --- a/src/schema/applicationStage.ts +++ b/src/schema/applicationStage.ts @@ -1,7 +1,7 @@ import { gql } from "apollo-server-core"; export const applicationStageDefs = gql` - scalar JSON + scalar JSON type Applicant { _id: ID! email: String @@ -46,89 +46,93 @@ export const applicationStageDefs = gql` idDocumentUrl: String } type Cycles { - name: String - startDate: String - endDate: String - createdAt: String - } - + name: String + startDate: String + endDate: String + createdAt: String + } + type Attributes { gender: String birth_date: String Address: String - phone:String - field_of_study:String - education_level:String - province: String - district:String + phone: String + field_of_study: String + education_level: String + province: String + district: String sector: String - isEmployed: Boolean - haveLaptop:Boolean + isEmployed: Boolean + haveLaptop: Boolean isStudent: Boolean Hackerrank_score: String - interview:String - interview_decision:String + interview: String + interview_decision: String past_andela_programs: String understandTraining: Boolean trainee_id: String } type Stages { - shortlist:shortlist - technical:technical - interview:interview - admitted:admitted + shortlist: shortlist + technical: technical + interview: interview + admitted: admitted dismissed: dismissed - allStages:allStages + allStages: allStages } - type shortlist{ - applicantId:String - status:String - comments:String - createdAt:String + type shortlist { + applicantId: String + status: String + comments: String + createdAt: String } type technical { - applicantId:String - status:String - score:String - comments:String - createdAt:String + applicantId: String + status: String + score: String + platform:String + invitationLink: String + comments: String + createdAt: String } type interview { - applicantId:String - status:String - interviewScore:String - comments:String - createdAt:String + applicantId: String + status: String + interviewScore: String + comments: String + createdAt: String } type admitted { applicantId: String - status:String - comments:String - createdAt:String + status: String + comments: String + createdAt: String } type dismissed { - applicantId:String + applicantId: String stageDismissedFrom: String - comments:String - status:String - createdAt:String + comments: String + status: String + createdAt: String } type StageHistory { - stage: String - comments: String - enteredAt: String - exitedAt: String -} + stage: String + comments: String + enteredAt: String + exitedAt: String + } -type allStages { - applicantId: String - currentStage: String - history: [StageHistory] -} + type allStages { + applicantId: String + currentStage: String + history: [StageHistory] + } type stageByModel { applicant: Applicant status: String! score: Float + platform:String + invitationLink: String comments: String createdAt: String updatedAt: String @@ -137,8 +141,8 @@ type allStages { getStageHistoryByApplicant(applicantId: ID!): HistoryStage getApplicantsByStage(stage: String!): [stageByModel!]! getTraineeCyclesApplications: cycleApplication - getApplicationsAttributes(trainee_id:String!): Attributes - getApplicationStages(trainee_id:String!):Stages + getApplicationsAttributes(trainee_id: String!): Attributes + getApplicationStages(trainee_id: String!): Stages } type response { @@ -153,8 +157,11 @@ type allStages { comments: String ): response! - addScore(applicantId: ID! - applicantStage: String! - score: Float!): response! + addScore( + applicantId: ID! + applicantStage: String! + score: Float! + ): response! + sendInvitation(applicantId:ID!,email: String!, platform:String!, invitationLink:String!): response! } `; diff --git a/src/schema/traineeApplicantSchema.ts b/src/schema/traineeApplicantSchema.ts index e12e38b..6d419e5 100644 --- a/src/schema/traineeApplicantSchema.ts +++ b/src/schema/traineeApplicantSchema.ts @@ -35,7 +35,7 @@ export const typeDefsTrainee = gql` cycleApplied: [CycleApplied!]! delete_at: Boolean status: String! - applicationPhase: ApplicationPhase! + applicationPhase: String! cohort: ID role: Role createdAt: String! diff --git a/src/seeders/users.ts b/src/seeders/users.ts index 040a353..1ac447d 100644 --- a/src/seeders/users.ts +++ b/src/seeders/users.ts @@ -36,7 +36,7 @@ const seedUsers = async () => { isActive: true, gender: "male", cohort: cohort._id, - applicationPhase: "Accepted", + applicationPhase: "Admitted", isVerified: true, }, {