From eb83c2babc0c2dd4a7f18bcb15161b612b013854 Mon Sep 17 00:00:00 2001 From: bahati yves <114049508+bahati10@users.noreply.github.com> Date: Mon, 9 Dec 2024 05:36:08 +0200 Subject: [PATCH] Ft schedule technical interview (#169) * Application cycle process on applicant * implement Technical Interview invitation * implement Technical Interview invitation * Implement Interview inviation * Pipeline envs (#163) * fix: remove the rm step * test: run on PR * test: test pusher cluster env * fix: add notification envs * test: add pusher key * fix: add the pusher app key * fix: log container start output * fix: remove the e flag * fix: revert changes * fix: re-add the pusher app key * fix: add pusher app key value * fix: typo * fix: test env file * fix: syntax * fix: remove the env from run cmd * fix: revert changes * fix: revert changes * fix: wrap run cmd in an if statement * Revert "test: test pusher cluster env" This reverts commit 57b2bbe4660885f8ba6b82679012950953c906c8. * test: create env file * test: add error handling * fix: run on push (#176) * fix(apply-jobpost): incorporate feedback (#168) - remove 'submitted' status - add comments for 'rejected status' * fix: don't exit on cmd failure (#177) * fix: don't exit on cmd failure * fix: re-add values * fix: add new variables * test: add container name * fix: match ports * fix: add devpulse email * fix: port * fix: port * fix: remove container name * fix: remove extraneous node env * fix: add jwt key * fix: run on push * #170 An author will be able to update a blog (#171) * fix (170): an author will be able to update a blog * fix (170): allow all users to create and update blogs * Fix to change status in application stage process (#165) * Ft: Enhance Backend documentation (#164) * fix: improve on notifications (#175) * Application cycle process on applicant * Application cycle process on applicant * Fixing conflicts and removing unnecessary files --------- Co-authored-by: Aime-Patrick Co-authored-by: Samuel Nishimwe Co-authored-by: Joslyn Manzi Karenzi Co-authored-by: mutsinziisaac <85931214+mutsinziisaac@users.noreply.github.com> Co-authored-by: Emmanuel MUGISHA Co-authored-by: ISHIMWE Jean Baptiste Co-authored-by: Christian Iradukunda <99505626+iChris-tian@users.noreply.github.com> --- .vscode/settings.json | 3 + src/helpers/bulkyMails.ts | 7 +- src/index.ts | 20 +- src/models/AuthUser.ts | 22 +- src/models/InterviewAssessmentStageSchema.ts | 54 +- src/models/ShortlistedSchema.ts | 43 +- src/models/admittedStageSchema.ts | 4 +- src/models/dismissedStageSchema.ts | 41 +- src/models/stageSchema.ts | 28 +- src/models/technicalAssessmentStage.ts | 63 +- src/models/technicalInterviewSchema.ts | 76 +++ src/models/technicalInterviewSchema.tsx | 74 +++ src/models/traineeApplicant.ts | 17 +- src/resolvers/applicationStageResolver.ts | 271 ++++++--- src/resolvers/loginUserResolver.ts | 191 +++--- src/resolvers/scheduleInterviewResolver.ts | 576 +++++++++++++++++++ src/resolvers/traineeApplicantResolver.ts | 38 +- src/schema/applicationStage.ts | 74 ++- src/schema/loggedUser.ts | 69 ++- src/schema/traineeApplicantSchema.ts | 16 +- tsconfig.json | 3 +- 21 files changed, 1350 insertions(+), 340 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 src/models/technicalInterviewSchema.ts create mode 100644 src/models/technicalInterviewSchema.tsx create mode 100644 src/resolvers/scheduleInterviewResolver.ts diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..55712c19 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "typescript.tsdk": "node_modules/typescript/lib" +} \ No newline at end of file diff --git a/src/helpers/bulkyMails.ts b/src/helpers/bulkyMails.ts index 6ad937a8..60414a93 100644 --- a/src/helpers/bulkyMails.ts +++ b/src/helpers/bulkyMails.ts @@ -111,13 +111,12 @@ export const sendUserCredentials = async (email: String, password: String) => { } }; - export const sendEmailTemplate = async ( email: string, subject: string, title: string, body: string, - button?: { url: string, text: string } + button?: { url: string; text: string } ) => { try { const logoText = "DevPulse"; @@ -171,7 +170,7 @@ export const sendEmailTemplate = async (

If you received this email by mistake, simply ignore it.
- For any questions, contact us at samuel.nishimwe@andela.com. + For any questions, contact us at samuel.nishimwe@andela.com.

{ console.log("Database connected!"); server.listen(PORT).then(({ url }) => console.info(`App on ${url}`)); -}); \ No newline at end of file +}); diff --git a/src/models/AuthUser.ts b/src/models/AuthUser.ts index 0ee5224c..1c4f3e01 100644 --- a/src/models/AuthUser.ts +++ b/src/models/AuthUser.ts @@ -17,9 +17,9 @@ const userSchema = new Schema( type: String, default: "", }, - isVerified:{ - type:Boolean, - default:false + isVerified: { + type: Boolean, + default: false, }, role: { type: Schema.Types.ObjectId, @@ -39,7 +39,15 @@ const userSchema = new Schema( }, applicationPhase: { type: String, - enum: ["Applied", 'Shortlisted', 'Technical Assessment', 'Interview Assessment', 'Admitted', 'Rejected', "Enrolled"], + enum: [ + "Applied", + "Shortlisted", + "Technical Assessment", + "Interview Assessment", + "Admitted", + "Rejected", + "Enrolled", + ], default: "Applied", }, isActive: { @@ -49,17 +57,15 @@ const userSchema = new Schema( isEmailVerified: { type: Boolean, default: false, - }, cohort: { type: Schema.Types.ObjectId, ref: "cohortModel", }, resetToken: String, - resetTokenExpiration:Date - + resetTokenExpiration: Date, }, { timestamps: true } ); -export const LoggedUserModel = model("LoggedUserModel", userSchema); \ No newline at end of file +export const LoggedUserModel = model("LoggedUserModel", userSchema); diff --git a/src/models/InterviewAssessmentStageSchema.ts b/src/models/InterviewAssessmentStageSchema.ts index 88c1cfc3..0218b0a7 100644 --- a/src/models/InterviewAssessmentStageSchema.ts +++ b/src/models/InterviewAssessmentStageSchema.ts @@ -7,32 +7,36 @@ interface IInterviewAssessment extends Document { comments?: string; } -const interviewAssessmentSchema = new Schema({ - applicantId: { - type: Schema.Types.ObjectId, - ref: "Trainees", - required: true, +const interviewAssessmentSchema = new Schema( + { + applicantId: { + type: Schema.Types.ObjectId, + ref: "Trainees", + required: true, + }, + status: { + type: String, + enum: ["No action", "Moved", "Rejected", "Admitted"], + default: "No action", + }, + interviewScore: { + type: Number, + min: 0, + max: 2, + validate: { + validator: (value: number) => + value === null || (value >= 0 && value <= 2), + message: "Score must be between 0 and 2.", + }, + }, + comments: { + type: String, + }, }, - status: { - type: String, - enum: ["No action", "Moved", "Rejected", "Admitted"], - default: "No action", - }, - interviewScore: { - type: Number, - min: 0, - max: 2, - validate: { - validator: (value : number) => value === null || (value >= 0 && value <= 2), - message: "Score must be between 0 and 2." - } - }, - comments: { - type: String, - }, -},{ - timestamps: true -}); + { + timestamps: true, + } +); const InterviewAssessment = mongoose.model( "InterviewAssessment", diff --git a/src/models/ShortlistedSchema.ts b/src/models/ShortlistedSchema.ts index 9f42aef2..f8b1d28e 100644 --- a/src/models/ShortlistedSchema.ts +++ b/src/models/ShortlistedSchema.ts @@ -2,28 +2,33 @@ import mongoose, { Schema, Document } from "mongoose"; interface IShortlisted extends Document { applicantId: mongoose.Schema.Types.ObjectId; - status: "No action" | "Invited" | "Moved" | "Rejected" | "Admitted"; + status: "No action" | "Moved" | "Rejected" | "Admitted"; comments?: string; } -const shortlistedSchema = new Schema({ - applicantId: { - type: Schema.Types.ObjectId, - ref: "Trainees", - required: true, - - }, - status: { - type: String, - enum: ["No action", "Invited", "Moved", "Rejected", "Admitted"], - default: "No action", - }, - comments: { - type: String, +const shortlistedSchema = new Schema( + { + applicantId: { + type: Schema.Types.ObjectId, + ref: "Trainees", + required: true, + }, + status: { + type: String, + enum: ["No action", "Invited", "Moved", "Rejected", "Admitted"], + default: "No action", + }, + comments: { + type: String, + }, }, -},{ - timestamps: true, -}); + { + timestamps: true, + } +); -const Shortlisted = mongoose.model("Shortlisted", shortlistedSchema); +const Shortlisted = mongoose.model( + "Shortlisted", + shortlistedSchema +); export default Shortlisted; diff --git a/src/models/admittedStageSchema.ts b/src/models/admittedStageSchema.ts index aef7bd8b..7282240d 100644 --- a/src/models/admittedStageSchema.ts +++ b/src/models/admittedStageSchema.ts @@ -10,7 +10,7 @@ interface IAdmitted extends Document { const admittedSchema = new Schema({ applicantId: { type: Schema.Types.ObjectId, - ref: "Trainees", + ref: "Trainee", required: true, }, status: { @@ -20,8 +20,6 @@ const admittedSchema = new Schema({ comments: { type: String, }, -},{ - timestamps: true }); const Admitted = mongoose.model("Admitted", admittedSchema); diff --git a/src/models/dismissedStageSchema.ts b/src/models/dismissedStageSchema.ts index 9796d837..1ee8c3ac 100644 --- a/src/models/dismissedStageSchema.ts +++ b/src/models/dismissedStageSchema.ts @@ -7,27 +7,30 @@ interface IDismissed extends Document { status: "Rejected"; // Added status field } -const rejectedSchema = new Schema({ - applicantId: { - type: Schema.Types.ObjectId, - ref: "Trainees", - required: true, +const rejectedSchema = new Schema( + { + applicantId: { + type: Schema.Types.ObjectId, + ref: "Trainees", + required: true, + }, + stageDismissedFrom: { + type: String, + required: true, + }, + comments: { + type: String, + }, + status: { + type: String, + default: "Rejected", + required: true, + }, }, - stageDismissedFrom: { - type: String, - required: true, - }, - comments: { - type: String, - }, - status:{ - type: String, - default: "Rejected", - required: true, + { + timestamps: true, } -},{ - timestamps: true -}); +); const Rejected = mongoose.model("Dismissed", rejectedSchema); export default Rejected; diff --git a/src/models/stageSchema.ts b/src/models/stageSchema.ts index 0413a959..9562aacd 100644 --- a/src/models/stageSchema.ts +++ b/src/models/stageSchema.ts @@ -2,7 +2,13 @@ import mongoose, { Schema, Document } from "mongoose"; interface IStageTracking extends Document { applicantId: mongoose.Schema.Types.ObjectId; - currentStage: "Applied" | "Shortlisted" | "Technical Assessment" | "Interview Assessment" | "Admitted" | "Rejected"; + currentStage: + | "Applied" + | "Shortlisted" + | "Technical Assessment" + | "Interview Assessment" + | "Admitted" + | "Rejected"; history: [ { stage: string; @@ -21,19 +27,29 @@ const stageTrackingSchema = new Schema({ }, currentStage: { type: String, - enum: ["Applied","Shortlisted", "Technical Assessment", "Interview Assessment", "Admitted", "Rejected"], + enum: [ + "Applied", + "Shortlisted", + "Technical Assessment", + "Interview Assessment", + "Admitted", + "Rejected", + ], required: true, default: "Applied", }, history: [ { stage: { type: String, required: true }, - comments:{type:String,required: true}, + comments: { type: String, required: true }, enteredAt: { type: Date, default: Date.now }, exitedAt: { type: Date }, - } - ] + }, + ], }); -const StageTracking = mongoose.model("StageTracking", stageTrackingSchema); +const StageTracking = mongoose.model( + "StageTracking", + stageTrackingSchema +); export default StageTracking; diff --git a/src/models/technicalAssessmentStage.ts b/src/models/technicalAssessmentStage.ts index ee3f8286..9be397f5 100644 --- a/src/models/technicalAssessmentStage.ts +++ b/src/models/technicalAssessmentStage.ts @@ -2,42 +2,45 @@ import mongoose, { Schema, Document } from "mongoose"; interface ITechnicalAssessment extends Document { applicantId: mongoose.Schema.Types.ObjectId; - status: "No action" | "Invited"| "Moved" | "Rejected" | "Admitted"; + status: "No action" | "Invited" | "Moved" | "Rejected" | "Admitted"; score: number; invitationLink: string; - platform:string; + platform: string; comments?: string; } -const technicalAssessmentSchema = new Schema({ - applicantId: { - type: Schema.Types.ObjectId, - ref: "Trainees", - required: true, +const technicalAssessmentSchema = new Schema( + { + applicantId: { + type: Schema.Types.ObjectId, + ref: "Trainees", + required: true, + }, + status: { + type: String, + enum: ["No action", "Invited", "Moved", "Rejected", "Admitted"], + default: "No action", + }, + score: { + type: Number, + min: 0, + max: 100, + default: null, + }, + platform: { + type: String, + }, + invitationLink: { + type: String, + }, + comments: { + type: String, + }, }, - status: { - type: String, - enum: ["No action", "Invited", "Moved","Passed", "Rejected", "Admitted"], - default: "No action", - }, - score: { - type: Number, - min: 0, - max: 100, - default:null - }, - platform:{ - type:String, - }, - invitationLink:{ - type:String, - }, - comments: { - type: String, - }, -},{ - timestamps: true -}); + { + timestamps: true, + } +); const TechnicalAssessment = mongoose.model( "TechnicalAssessment", diff --git a/src/models/technicalInterviewSchema.ts b/src/models/technicalInterviewSchema.ts new file mode 100644 index 00000000..5e1437bf --- /dev/null +++ b/src/models/technicalInterviewSchema.ts @@ -0,0 +1,76 @@ +import mongoose, { Schema, Document } from "mongoose"; + +interface ITechnicalInterview extends Document { + applicantId: mongoose.Schema.Types.ObjectId; + coordinatorId: mongoose.Schema.Types.ObjectId | null; + meetingLink: string; + scheduledDate: Date; + meetingPlatform: string; + status: string; + emailSent: boolean; + createdAt?: Date; + updatedAt?: Date; +} + +interface PopulatedCoordinator { + _id: string; + firstname: string; + lastname: string; + email: string; + role: { + roleName: string; + description: string; + permissions: string[]; + }; +} + +interface TechnicalInterview { + _id: string; + coordinatorId: PopulatedCoordinator; +} + +const technicalInterviewSchema = new Schema( + { + applicantId: { + type: Schema.Types.ObjectId, + ref: "Trainees", + required: true, + }, + coordinatorId: { + type: Schema.Types.ObjectId, + ref: "LoggedUserModel", + required: true, + }, + meetingLink: { + type: String, + required: true, + }, + scheduledDate: { + type: Date, + required: true, + }, + meetingPlatform: { + type: String, + enum: ["Zoom", "Teams"], + required: true, + }, + status: { + type: String, + enum: ["Scheduled", "Completed", "Cancelled", "No show"], + default: "Scheduled", + }, + emailSent: { + type: Boolean, + default: false, + }, + }, + { + timestamps: true, + } +); + +const TechnicalInterview = mongoose.model( + "TechnicalInterview", + technicalInterviewSchema +); +export default TechnicalInterview; diff --git a/src/models/technicalInterviewSchema.tsx b/src/models/technicalInterviewSchema.tsx new file mode 100644 index 00000000..af29e800 --- /dev/null +++ b/src/models/technicalInterviewSchema.tsx @@ -0,0 +1,74 @@ +import mongoose, { Schema, Document } from "mongoose"; + +interface ITechnicalInterview extends Document { + applicantId: mongoose.Schema.Types.ObjectId; + coordinatorId: mongoose.Schema.Types.ObjectId | null; + meetingLink: string; + scheduledDate: Date; + meetingPlatform: string; + status: string; + emailSent: boolean; +} + +interface PopulatedCoordinator { + _id: string; + firstname: string; + lastname: string; + email: string; + role: { + roleName: string; + description: string; + permissions: string[]; + }; +} + +interface TechnicalInterview { + _id: string; + coordinatorId: PopulatedCoordinator; +} + +const technicalInterviewSchema = new Schema( + { + applicantId: { + type: Schema.Types.ObjectId, + ref: "Trainees", + required: true, + }, + coordinatorId: { + type: Schema.Types.ObjectId, + ref: "LoggedUserModel", + required: true, + }, + meetingLink: { + type: String, + required: true, + }, + scheduledDate: { + type: Date, + required: true, + }, + meetingPlatform: { + type: String, + enum: ["Zoom", "Teams"], + required: true, + }, + status: { + type: String, + enum: ["Scheduled", "Completed", "Cancelled", "No show"], + default: "Scheduled", + }, + emailSent: { + type: Boolean, + default: false, + }, + }, + { + timestamps: true, + } +); + +const TechnicalInterview = mongoose.model( + "TechnicalInterview", + technicalInterviewSchema +); +export default TechnicalInterview; diff --git a/src/models/traineeApplicant.ts b/src/models/traineeApplicant.ts index a6d0c18a..cf0b78ec 100644 --- a/src/models/traineeApplicant.ts +++ b/src/models/traineeApplicant.ts @@ -65,19 +65,24 @@ const TraineeApplicantSchema = new Schema( coverLetterUrl: { type: String, required: false, - default: 'http://example.com/coverLetter.pdf' - }, + default: "http://example.com/coverLetter.pdf", + }, idDocumentUrl: { type: String, required: false, - default: 'http://example.com/idDocument.pdf' + default: "http://example.com/idDocument.pdf", }, resumeUrl: { type: String, required: false, - default: 'http://example.com/resume.pdf' - } - + default: "http://example.com/resume.pdf", + }, + technicalInterviews: [ + { + type: Schema.Types.ObjectId, + ref: "TechnicalInterview", + }, + ], }, { timestamps: true, diff --git a/src/resolvers/applicationStageResolver.ts b/src/resolvers/applicationStageResolver.ts index c1b767c6..f82022d5 100644 --- a/src/resolvers/applicationStageResolver.ts +++ b/src/resolvers/applicationStageResolver.ts @@ -13,6 +13,7 @@ import { LoggedUserModel } from "../models/AuthUser"; import { pusher } from "../helpers/pusher"; import { traineEAttributes } from "../models/traineeAttribute"; import mongoose from "mongoose"; +import TechnicalInterview from "../models/technicalInterviewSchema"; const validStages = [ "Shortlisted", @@ -70,32 +71,6 @@ export const applicationStageResolvers: any = { ); } }, - getApplicantsByStage: async (_: any, { stage }: { stage: string }) => { - try { - if (!validStages.includes(stage)) { - throw new Error("Invalid stage. Please choose a valid stage."); - } - const stageIndex = validStages.indexOf(stage); - const selectedModel = models[stageIndex]; - const allApplicantInStage = await getApplicantsByModel(selectedModel); - return allApplicantInStage - .filter((tracking: any) => tracking.applicantId !== null) - .map((tracking: any) => ({ - applicant: tracking.applicantId, - 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(), - })); - } catch (error: any) { - throw new Error( - `Failed to retrieve applicants for stage ${stage}: ${error.message}` - ); - } - }, getTraineeCyclesApplications: async (_: any, __: any, context: any) => { try { if (!context.currentUser) { @@ -135,47 +110,140 @@ export const applicationStageResolvers: any = { throw new CustomGraphQLError(error); } }, - getApplicationStages: async (_: any, { trainee_id }: any, context: any) => { + getApplicantsByStage: async (_: any, { stage }: { stage: string }) => { try { - if (!context.currentUser) { - throw new CustomGraphQLError( - "You must be logged in to view your applications" - ); + if (!validStages.includes(stage)) { + throw new Error("Invalid stage. Please choose a valid stage."); } + const stageIndex = validStages.indexOf(stage); + const selectedModel = models[stageIndex]; + const allApplicantInStage = await getApplicantsByModel(selectedModel); - const trainee = new mongoose.Types.ObjectId(trainee_id); - - const [ - shortlistStage, - technicalStage, - interviewStage, - admittedStage, - rejectedStage, - AllStages, - ] = await Promise.all([ - Shortlisted.findOne({ applicantId: trainee }), - TechnicalAssessment.findOne({ applicantId: trainee }), - InterviewAssessment.findOne({ applicantId: trainee }), - Admitted.findOne({ applicantId: trainee }), - Rejected.findOne({ applicantId: trainee }), - StageTracking.findOne({ applicantId: trainee }), - ]); + // Fetch interviews for each applicant + const allApplicantInStages = await Promise.all( + allApplicantInStage + .filter((tracking: any) => tracking.applicantId !== null) + .map(async (tracking: any) => { + const interviews = await TechnicalInterview.find({ + applicantId: tracking.applicantId, + }).populate({ + path: "coordinatorId", + model: LoggedUserModel, + select: "firstname lastname email", + }); - return { - shortlist: shortlistStage, - technical: technicalStage, - interview: interviewStage, - admitted: admittedStage, - rejected: rejectedStage, - allStages: AllStages, - }; - } catch (error) { - console.error("Error retrieving application stages:", error); - throw new CustomGraphQLError( - error || "An error occurred while retrieving applications" + return { + applicant: tracking.applicantId, + 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(), + technicalInterviews: interviews, + }; + }) + ); + + return allApplicantInStages; + } catch (error: any) { + throw new Error( + `Failed to retrieve applicants for stage ${stage}: ${error.message}` ); } }, + + // getInterviewStages: async (_: any, __: any, context: any) => { + // try { + // if (!context.currentUser) { + // throw new CustomGraphQLError( + // "You must be logged in to view your applications" + // ); + // } + + // const applicant = await InterviewAssessment.find() + // .populate("applicantId") + // .exec(); + // return applicant + // .filter((tracking: any) => tracking.applicantId !== null) + // .map((tracking: any) => ({ + // applicant: tracking.applicantId, + // status: tracking.status, + // score: tracking.score, + // comments: tracking.comments, + // createdAt: tracking.createdAt.toLocaleString(), + // updatedAt: tracking.updatedAt.toLocaleString(), + // })); + // } catch (err: any) { + // throw new Error(`Failed to retrieve applicants ${err.message}`); + // } + // }, + + // getInterviewStages: async (_: any, __: any, context: any) => { + // try { + // if (!validStages.includes(stage)) { + // throw new Error("Invalid stage. Please choose a valid stage."); + // } + // const stageIndex = validStages.indexOf(stage); + // const selectedModel = models[stageIndex]; + // const allApplicantInStage = await getApplicantsByModel(selectedModel); + + // const applicants = await InterviewAssessment.find() + // .populate({ + // path: "applicantId", + // model: "Trainees", + // populate: [ + // { + // path: "technicalInterviews", + // model: "TechnicalInterview", + // populate: { + // path: "coordinatorId", + // model: LoggedUserModel, + // select: "firstname lastname email role", + // }, + // }, + // ], + // }) + // .exec(); + + // return applicants + // .filter((tracking: any) => tracking.applicantId !== null) + // .map((tracking: any) => { + // const interviews = tracking.applicantId.technicalInterviews || []; + // return { + // applicant: { + // _id: tracking.applicantId._id, + // firstName: tracking.applicantId.firstName, + // lastName: tracking.applicantId.lastName, + // email: tracking.applicantId.email, + // applicationPhase: tracking.applicantId.applicationPhase, + // status: tracking.applicantId.status, + // }, + // interviews: interviews.map((interview: any) => ({ + // _id: interview._id, + // meetingLink: interview.meetingLink, + // meetingPlatform: interview.meetingPlatform, + // coordinator: interview.coordinatorId || null, + // scheduledDate: interview.scheduledDate?.toISOString(), + // status: interview.status, + // emailSent: interview.emailSent, + // createdAt: interview.createdAt?.toISOString(), + // updatedAt: interview.updatedAt?.toISOString(), + // })), + // status: tracking.status, + // score: tracking.interviewScore, + // comments: tracking.comments, + // createdAt: tracking.createdAt.toLocaleString(), + // updatedAt: tracking.updatedAt.toLocaleString(), + // }; + // }); + // } catch (err: any) { + // throw new CustomGraphQLError( + // `Failed to retrieve applicants: ${err.message}` + // ); + // } + // }, }, Mutation: { moveToNextStage: async ( @@ -417,28 +485,28 @@ export const applicationStageResolvers: any = { }, } ); - const updatedApplicant = await TraineeApplicant.findOne({ - _id: applicantId, - }) - .populate("email") - .lean(); - - const email = updatedApplicant?.email; - - if (email) { - await LoggedUserModel.updateOne( - { email }, - { - $set: { - applicationPhase: nextStage, - status: "Admitted", - role: traineeRole._id, - }, - } - ); - } else { - throw new Error("Email not found for the provided applicant ID"); - } + const updatedApplicant = await TraineeApplicant.findOne({ + _id: applicantId, + }) + .populate("email") + .lean(); + + const email = updatedApplicant?.email; + + if (email) { + await LoggedUserModel.updateOne( + { email }, + { + $set: { + applicationPhase: nextStage, + status: "Admitted", + role: traineeRole._id, + }, + } + ); + } else { + throw new Error("Email not found for the provided applicant ID"); + } const notification2 = await ApplicantNotificationsModel.create({ userId: user!._id, message, @@ -646,7 +714,21 @@ 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) => { + sendInvitation: async ( + _: any, + { + applicantId, + email, + platform, + invitationLink, + }: { + applicantId: string; + email: string; + platform: string; + invitationLink: string; + }, + context: any + ) => { try { if (!context.currentUser) { throw new CustomGraphQLError( @@ -677,19 +759,20 @@ export const applicationStageResolvers: any = { 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", + 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); - }); + .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", @@ -716,7 +799,7 @@ export const applicationStageResolvers: any = { ); await TechnicalAssessment.updateOne( { applicantId }, - { $set: { status: "Invited" ,invitationLink, platform} } + { $set: { status: "Invited", invitationLink, platform } } ); return { diff --git a/src/resolvers/loginUserResolver.ts b/src/resolvers/loginUserResolver.ts index f8b9a39c..89edf3d7 100644 --- a/src/resolvers/loginUserResolver.ts +++ b/src/resolvers/loginUserResolver.ts @@ -1,21 +1,21 @@ -import { AuthenticationError } from 'apollo-server'; -import { LoggedUserModel } from '../models/AuthUser'; -import { RoleModel } from '../models/roleModel'; -import { PermissionModel } from '../models/permissionModel'; -import { generateToken, verifyToken } from '../utils/generateToken'; -import BcryptUtil from '../utils/bcrypt'; -import { validateUserLogged } from '../validations/createUser.validation'; -import { validateLogin } from '../validations/login.validations'; -import { generateAutoGeneratedPassword } from '../utils/passwordAutoGenerate'; -import { sendEmailTemplate, sendUserCredentials } from '../helpers/bulkyMails'; -import { userModel } from '../models/user'; -import { sessionModel } from '../models/session'; -import { cohortModels } from '../models/cohortModel'; -import TraineeApplicant from '../models/traineeApplicant'; -import { publishNotification } from './adminNotificationsResolver'; -import { uploadImage } from '../utils/uploadImage'; - -const FrontendUrl = process.env.FRONTEND_URL +import { AuthenticationError } from "apollo-server"; +import { LoggedUserModel } from "../models/AuthUser"; +import { RoleModel } from "../models/roleModel"; +import { PermissionModel } from "../models/permissionModel"; +import { generateToken, verifyToken } from "../utils/generateToken"; +import BcryptUtil from "../utils/bcrypt"; +import { validateUserLogged } from "../validations/createUser.validation"; +import { validateLogin } from "../validations/login.validations"; +import { generateAutoGeneratedPassword } from "../utils/passwordAutoGenerate"; +import { sendEmailTemplate, sendUserCredentials } from "../helpers/bulkyMails"; +import { userModel } from "../models/user"; +import { sessionModel } from "../models/session"; +import { cohortModels } from "../models/cohortModel"; +import TraineeApplicant from "../models/traineeApplicant"; +import { publishNotification } from "./adminNotificationsResolver"; +import { uploadImage } from "../utils/uploadImage"; + +const FrontendUrl = process.env.FRONTEND_URL; export const loggedUserResolvers: any = { Query: { @@ -25,10 +25,9 @@ export const loggedUserResolvers: any = { } const id = args.ID; - const userWithRole = await LoggedUserModel.findById( - ctx.currentUser._id - ).populate("role") - .populate('cohort') + const userWithRole = await LoggedUserModel.findById(ctx.currentUser._id) + .populate("role") + .populate("cohort"); const userId = userWithRole?._id.toString(); @@ -37,8 +36,41 @@ export const loggedUserResolvers: any = { } // const upvalue = await LoggedUserModel.findById(id).populate("role"); // return upvalue; - return userWithRole + return userWithRole; }, + + async getInterviewCoordinators( + _: any, + args: any, + ctx: any, + amount: number + ) { + const adminRoles = await RoleModel.find({ + roleName: { $in: ["superAdmin", "admin"] }, + }); + + if (!adminRoles || adminRoles.length === 0) { + throw new Error("Admin roles not found."); + } + + const roleIds = adminRoles.map((role) => role._id); + + const users = await LoggedUserModel.find({ + role: { $in: roleIds }, + }) + .sort({ createdAt: -1 }) + .limit(amount) + .populate({ + path: "role", + populate: { + path: "permissions", + model: PermissionModel, + }, + }); + + return users; + }, + async getUsers_Logged(_: any, args: any, ctx: any, amount: any) { const users = await LoggedUserModel.find() .sort({ createdAt: -1 }) @@ -59,13 +91,13 @@ export const loggedUserResolvers: any = { }, currentUser: async (_: unknown, __: unknown, context: any) => { if (!context.currentUser) { - return null; + return null; } return { - firstName: context.currentUser.firstname, - lastName: context.currentUser.lastname, - email: context.currentUser.email + firstName: context.currentUser.firstname, + lastName: context.currentUser.lastname, + email: context.currentUser.email, }; }, getByFilter: async (_: any, { filter }: { filter: any }) => { @@ -80,7 +112,7 @@ export const loggedUserResolvers: any = { } const users = await LoggedUserModel.find(filterQuery); return users; - } + }, }, Mutation: { async createUser_Logged( @@ -102,7 +134,6 @@ export const loggedUserResolvers: any = { }: any, ctx: any ) { - if (!process.env.JWT_SECRET) { throw new Error( "Please ensure that the secret key is properly configured" @@ -255,8 +286,10 @@ export const loggedUserResolvers: any = { ); const savedSession = await newSession.save(); - await sendEmailTemplate(email, "Verify Account!", - `Hello ${firstname || email.split('@')[0]},`, + await sendEmailTemplate( + email, + "Verify Account!", + `Hello ${firstname || email.split("@")[0]},`, ` Welcome to Devpulse! We’re excited to have you join our community of developers, creators, and innovators.
@@ -268,11 +301,10 @@ export const loggedUserResolvers: any = {
Best regards,
- The Devpulse Team` - , + The Devpulse Team`, { text: "Verify Email", - url: FrontendUrl + `/#/verifyEmail/?token=${token}` + url: FrontendUrl + `/#/verifyEmail/?token=${token}`, } ); res.session = savedSession; @@ -394,17 +426,16 @@ export const loggedUserResolvers: any = { bio, }; - if(picture){ + if (picture) { try { const uploadResult = await uploadImage( picture, - 'profile_pictures', + "profile_pictures", `user_${ID}_profile` ); updateData.picture = uploadResult.url; - - } catch(err){ - throw new Error('Failed to upload picture'); + } catch (err) { + throw new Error("Failed to upload picture"); } } if (!ctx.currentUser) { @@ -426,25 +457,37 @@ export const loggedUserResolvers: any = { } const wasEdited = ( - await LoggedUserModel.updateOne( - { _id: ID }, - updateData - ) + await LoggedUserModel.updateOne({ _id: ID }, updateData) ).modifiedCount; return wasEdited; }, async updateUserSelf( _: any, - { ID, editUserInput: { firstname, lastname, gender, code, country, telephone, picture, bio } }: any, ctx: any) { - + { + ID, + editUserInput: { + firstname, + lastname, + gender, + code, + country, + telephone, + picture, + bio, + }, + }: any, + ctx: any + ) { if (!ctx.currentUser) { - throw new AuthenticationError('You must be logged in'); + throw new AuthenticationError("You must be logged in"); } if (ctx.currentUser._id.toString() !== ID) { - throw new AuthenticationError('You are only authorized to update your own account.'); + throw new AuthenticationError( + "You are only authorized to update your own account." + ); } - + const updateFields: any = {}; if (firstname) updateFields.firstname = firstname; if (lastname) updateFields.lastname = lastname; @@ -453,17 +496,17 @@ export const loggedUserResolvers: any = { if (country) updateFields.country = country; if (telephone) updateFields.telephone = telephone; if (picture) updateFields.picture = picture; - if(bio) updateFields.bio = bio; - + if (bio) updateFields.bio = bio; + const wasEdited = ( await LoggedUserModel.updateOne( { _id: ctx.currentUser._id }, - { $set: updateFields } + { $set: updateFields } ) ).modifiedCount; - - return wasEdited > 0; - }, + + return wasEdited > 0; + }, assignRoleToUser: async ( _: any, @@ -550,8 +593,8 @@ export const loggedUserResolvers: any = { login: async (_: any, { email, password }: any) => { const user = await LoggedUserModel.findOne({ email }); - if (user?.isVerified===false){ - throw new Error('User is not verified'); + if (user?.isVerified === false) { + throw new Error("User is not verified"); } if (!user) { @@ -670,18 +713,20 @@ export const loggedUserResolvers: any = { } return updatedUser; - }, - async verifyUser(_:any,{ID}:any){ - try{ - let user = await LoggedUserModel.findOne({_id:ID })as any; - if (user.isVerified===true){ - return + }, + async verifyUser(_: any, { ID }: any) { + try { + let user = (await LoggedUserModel.findOne({ _id: ID })) as any; + if (user.isVerified === true) { + return; } - const email=user.email - user.isVerified=true - await user?.save(); - const message=await sendEmailTemplate(email, " Welcome to Devpulse – Your Account is Ready!", - `Hello ${email.split('@')[0]},`, + const email = user.email; + user.isVerified = true; + await user?.save(); + const message = await sendEmailTemplate( + email, + " Welcome to Devpulse – Your Account is Ready!", + `Hello ${email.split("@")[0]},`, ` Welcome to Devpulse! We’re excited to have you join our community of developers, creators, and innovators.
@@ -715,17 +760,15 @@ export const loggedUserResolvers: any = { Best regards,
The Devpulse Team - ` - , + `, { text: "Continue", - url: FrontendUrl + '/#/login/' + url: FrontendUrl + "/#/login/", } ); - return user - } - catch(error){ - throw new Error("Error occured please try again") + return user; + } catch (error) { + throw new Error("Error occured please try again"); } }, }, diff --git a/src/resolvers/scheduleInterviewResolver.ts b/src/resolvers/scheduleInterviewResolver.ts new file mode 100644 index 00000000..28686e82 --- /dev/null +++ b/src/resolvers/scheduleInterviewResolver.ts @@ -0,0 +1,576 @@ +import { sendEmailTemplate } from "../helpers/bulkyMails"; +import { pusher } from "../helpers/pusher"; +import { ApplicantNotificationsModel } from "../models/applicantNotifications"; +import { LoggedUserModel } from "../models/AuthUser"; +import { RoleModel } from "../models/roleModel"; +import TechnicalInterview from "../models/technicalInterviewSchema"; +import TraineeApplicant from "../models/traineeApplicant"; +import { CustomGraphQLError } from "../utils/customErrorHandler"; +interface PopulatedCoordinator { + _id: string; + firstname: string; + lastname: string; + email: string; + role: { + roleName: string; + description?: string; + permissions?: string[]; + }; +} + +export const technicalInterviewResolvers = { + Query: { + getTechnicalInterviews: async ( + _: any, + { status }: { status?: string }, + context: any + ) => { + try { + if (!context.currentUser) { + throw new CustomGraphQLError( + "You must be logged in to perform this action" + ); + } + + const query = status ? { status } : {}; + const interviews = await TechnicalInterview.find(query) + .populate({ + path: "applicantId", + model: TraineeApplicant, + select: "firstName lastName email", + }) + .populate({ + path: "coordinatorId", + model: LoggedUserModel, + populate: { + path: "role", + model: RoleModel, + select: "roleName", + }, + select: "firstname lastname email role", + }) + .exec(); + console.log("Populated Interviews: ", interviews); + + // Optionally filter out coordinators who aren't admin + const filteredInterviews = interviews.map((interview) => { + const coordinator = + interview.coordinatorId as PopulatedCoordinator | null; + if ( + coordinator && + typeof coordinator === "object" && + coordinator.role && + coordinator.role.roleName === "admin" + ) { + interview.coordinatorId = null; // Remove non-admin coordinators + } + + return interview; + }); + + return filteredInterviews.map((interview) => ({ + _id: interview._id, + applicant: interview.applicantId, + coordinator: interview.coordinatorId || null, + meetingLink: interview.meetingLink, + scheduledDate: interview.scheduledDate.toISOString(), + meetingPlatform: interview.meetingPlatform, + status: interview.status, + emailSent: interview.emailSent, + createdAt: interview.createdAt, + updatedAt: interview.updatedAt, + })); + } catch (error: any) { + throw new Error( + `Failed to fetch technical interviews: ${error.message}` + ); + } + }, + + getTechnicalInterviewById: async ( + _: any, + { interviewId }: { interviewId: string }, + context: any + ) => { + try { + if (!context.currentUser) { + throw new CustomGraphQLError( + "You must be logged in to perform this action" + ); + } + + const interview = await TechnicalInterview.findById(interviewId) + .populate("applicantId") + .populate("coordinatorId") + .exec(); + + if (!interview) { + throw new Error("Interview not found"); + } + + return { + _id: interview._id, + applicant: interview.applicantId, + coordinator: interview.coordinatorId, + meetingLink: interview.meetingLink, + scheduledDate: interview.scheduledDate.toISOString(), + meetingPlatform: interview.meetingPlatform, + status: interview.status, + emailSent: interview.emailSent, + }; + } catch (error: any) { + throw new Error( + `Failed to fetch technical interview: ${error.message}` + ); + } + }, + }, + + Mutation: { + scheduleTechnicalInterview: async ( + _: any, + { input }: { input: any }, + context: any + ) => { + try { + if (!context.currentUser) { + throw new CustomGraphQLError( + "You must be logged in to perform this action" + ); + } + + const userRole = await RoleModel.findById({ + _id: context.currentUser.role, + }); + if ( + userRole?.roleName !== "admin" && + userRole?.roleName !== "superAdmin" + ) { + throw new CustomGraphQLError( + "Only admin and super admins are allowed" + ); + } + + const { + applicantId, + coordinatorId, + meetingLink, + scheduledDate, + meetingPlatform, + } = input; + + // Verify applicant is in the correct stage + const applicant = await TraineeApplicant.findById(applicantId); + if (!applicant) { + throw new CustomGraphQLError("Applicant Not found"); + } else if (applicant.applicationPhase !== "Interview Assessment") { + throw new CustomGraphQLError( + `Applicant must be in "Interview Assessment" stage` + ); + } + + const existingInterview = await TechnicalInterview.findOne({ + applicantId, + }); + if (existingInterview) { + throw new CustomGraphQLError( + "An interview has already been scheduled for this applicant." + ); + } + + // Verify coordinator role + const coordinator = await LoggedUserModel.findById( + coordinatorId + ).populate<{ + role: { roleName: string }; + }>({ + path: "role", + model: RoleModel, + select: "roleName", + }); + + if (!coordinator) { + throw new CustomGraphQLError("Coordinator not found"); + } + + console.log("COORDINATOR'S ROLE =>>>>>>>>>>", coordinator); + + // Allow both admin and superAdmin roles to be coordinators + if ( + coordinator.role.roleName !== "admin" && + coordinator.role.roleName !== "superAdmin" + ) { + throw new CustomGraphQLError( + "The selected coordinator must be an admin or super admin." + ); + } + + // Create interview + const interview = await TechnicalInterview.create({ + applicantId, + coordinatorId, + meetingLink, + scheduledDate: new Date(scheduledDate), + meetingPlatform, + }); + + await TraineeApplicant.findByIdAndUpdate(applicantId, { + $push: { technicalInterviews: interview._id }, + }); + + // Get applicant and coordinator details for email + const applicantData = await TraineeApplicant.findById(applicantId); + const coordinatorData = await LoggedUserModel.findById(coordinatorId); + + // Create notification for applicant + const applicantNotification = await ApplicantNotificationsModel.create({ + userId: applicantId, + message: `Technical Interview Scheduled for ${interview.scheduledDate.toLocaleString()}`, + eventType: "applicationUpdate", + relatedObjectId: interview._id, + }); + + // Create notification for coordinator + const coordinatorNotification = + await ApplicantNotificationsModel.create({ + userId: coordinatorId, + message: `Technical Interview Assigned: Candidate ${applicantData?.firstName} ${applicantData?.lastName}`, + eventType: "applicationUpdate", + relatedObjectId: interview._id, + }); + + // Send Pusher notifications + await Promise.all([ + pusher.trigger(`notifications-${applicantId}`, "new-notification", { + message: applicantNotification.message, + id: applicantNotification._id, + createdAt: applicantNotification.createdAt, + read: applicantNotification.read, + }), + pusher.trigger(`notifications-${coordinatorId}`, "new-notification", { + message: coordinatorNotification.message, + id: coordinatorNotification._id, + createdAt: coordinatorNotification.createdAt, + read: coordinatorNotification.read, + }), + ]).catch((error) => { + console.error("Error with Pusher triggers:", error); + }); + + if (applicantData?.email && coordinatorData?.email) { + const applicantEmailTemplate = ` + + + + + + + +
+ +`; + + const coordinatorEmailTemplate = ` + + + + + + + +
+
+

Technical Interview Assignment

+
+ +
+
+

Notice!

+

We are pleased to inform you that the candidate has successfully passed the technical assessment. Their performance demonstrated exceptional problem-solving skills and technical proficiency. + You have been selected as the interview coordinator.

+
+ +

Dear ${coordinatorData.firstname} ${coordinatorData.lastname},

+ +

A technical interview has been scheduled. Here are the details:

+ +

+ Date: ${interview.scheduledDate.toLocaleString( + "en-US", + { dateStyle: "full", timeStyle: "long" } + )} +
+ Platform: ${interview.meetingPlatform} +

+ +

Applicant Information:

+
    +
  • Name: ${applicantData.firstName} ${ + applicantData.lastName + }
  • +
  • Email: ${applicantData.email}
  • +
  • Application Phase: Interview Assessment
  • +
+ +

Interview Preparation Checklist:

+
    +
  1. Review applicant's profile
  2. +
  3. Prepare technical assessment criteria
  4. +
  5. Have a structured interview plan
  6. +
  7. Evaluate problem-solving skills
  8. +
  9. Document interview observations
  10. +
+ + +
+ + +
+ + +`; + + // Send emails + await Promise.all([ + sendEmailTemplate( + applicantData.email, + "Technical Interview Invitation", + "", + applicantEmailTemplate + ), + sendEmailTemplate( + coordinatorData.email, + "Technical Interview Assignment", + "", + coordinatorEmailTemplate + ), + ]); + + await TechnicalInterview.findByIdAndUpdate(interview._id, { + emailSent: true, + }); + } + + return { + success: true, + message: "Technical interview scheduled successfully", + }; + } catch (error: any) { + return new Error(error.message); + } + }, + + updateInterviewStatus: async ( + _: any, + { interviewId, status }: { interviewId: string; status: string }, + context: any + ) => { + try { + if (!context.currentUser) { + throw new CustomGraphQLError( + "You must be logged in to perform this action" + ); + } + + const userRole = await RoleModel.findById({ + _id: context.currentUser.role, + }); + if ( + userRole?.roleName !== "admin" && + userRole?.roleName !== "superAdmin" + ) { + throw new CustomGraphQLError( + "Only admin and super admins are allowed" + ); + } + + const interview = await TechnicalInterview.findByIdAndUpdate( + interviewId, + { status }, + { new: true } + ); + + if (!interview) { + throw new Error("Interview not found"); + } + + return { + success: true, + message: `Interview status updated to ${status}`, + }; + } catch (error: any) { + return new Error(error.message); + } + }, + }, +}; diff --git a/src/resolvers/traineeApplicantResolver.ts b/src/resolvers/traineeApplicantResolver.ts index fa2678ae..cf780300 100755 --- a/src/resolvers/traineeApplicantResolver.ts +++ b/src/resolvers/traineeApplicantResolver.ts @@ -3,8 +3,8 @@ import { traineEAttributes } from "../models/traineeAttribute"; import { applicationCycle } from "../models/applicationCycle"; import mongoose, { ObjectId } from "mongoose"; import { sendEmailTemplate } from "../helpers/bulkyMails"; -import { Types } from 'mongoose'; -import { AuthenticationError } from 'apollo-server'; +import { Types } from "mongoose"; +import { AuthenticationError } from "apollo-server"; import { LoggedUserModel } from "../models/AuthUser"; import { RoleModel } from "../models/roleModel"; @@ -15,7 +15,6 @@ import { cohortModels } from "../models/cohortModel"; import { publishNotification } from "./adminNotificationsResolver"; import { any } from "joi"; - interface Context { currentUser: { _id: string }; } @@ -46,6 +45,15 @@ export const traineeApplicantResolver: any = { const itemsToSkip = (pages - 1) * items; const allTrainee = await TraineeApplicant.find({ delete_at: false }) .populate("cycle_id") + .populate({ + path: "technicalInterviews", + model: "TechnicalInterview", + populate: { + path: "coordinatorId", + model: "LoggedUserModel", + select: "firstname lastname email", + }, + }) .skip(itemsToSkip) .limit(items); @@ -63,7 +71,12 @@ export const traineeApplicantResolver: any = { }, async getOneTrainee(_: any, { ID }: any) { - const trainee = await TraineeApplicant.findById(ID).populate("cycle_id"); + const trainee = await TraineeApplicant.findById(ID) + .populate("cycle_id") + .populate({ + path: "technicalInterviews", + model: "TechnicalInterview", + }); if (!trainee) throw new Error("No trainee is found, pleade provide the correct ID"); return trainee; @@ -128,7 +141,16 @@ export const traineeApplicantResolver: any = { } }, async createNewTraineeApplicant(_: any, { input }: any, context: any) { - const { lastName, firstName, email, cycle_id, attributes, coverLetterUrl, idDocumentUrl, resumeUrl } = input; + const { + lastName, + firstName, + email, + cycle_id, + attributes, + coverLetterUrl, + idDocumentUrl, + resumeUrl, + } = input; const userWithRole = await LoggedUserModel.findById( context.currentUser?._id ).populate("role"); @@ -193,7 +215,7 @@ export const traineeApplicantResolver: any = { ], idDocumentUrl, coverLetterUrl, - resumeUrl + resumeUrl, }); // Create the corresponding traineEAttributes if ( @@ -254,7 +276,9 @@ export const traineeApplicantResolver: any = { throw new CustomGraphQLError("Trainee already belongs to a cohort"); } - const cohort = await cohortModels.findById(cohortId).populate('trainees'); + const cohort = await cohortModels + .findById(cohortId) + .populate("trainees"); if (!cohort) { throw new CustomGraphQLError("Cohort not found"); } diff --git a/src/schema/applicationStage.ts b/src/schema/applicationStage.ts index 42af0103..6241ec19 100644 --- a/src/schema/applicationStage.ts +++ b/src/schema/applicationStage.ts @@ -34,12 +34,12 @@ export const applicationStageDefs = gql` type cycleApplication { email: String cycle_id: Cycles - firstName: String - lastName: String - user: String - applicationPhase: String - status: String - _id:String + firstName: String + lastName: String + user: String + applicationPhase: String + status: String + _id: String createdAt: String coverLetterUrl: String resumeUrl: String @@ -90,7 +90,7 @@ export const applicationStageDefs = gql` applicantId: String status: String score: String - platform:String + platform: String invitationLink: String comments: String createdAt: String @@ -131,18 +131,31 @@ export const applicationStageDefs = gql` applicant: Applicant status: String! score: Float - platform:String + platform: String invitationLink: String comments: String + technicalInterviews: [TechnicalInterview] createdAt: String updatedAt: String } + + type stageWithInterviews { + applicant: Applicant + status: String! + score: Float + comments: String + createdAt: String + updatedAt: String + interviews: [TechnicalInterview] + } + type Query { getStageHistoryByApplicant(applicantId: ID!): HistoryStage getApplicantsByStage(stage: String!): [stageByModel!]! getTraineeCyclesApplications: cycleApplication getApplicationsAttributes(trainee_id: String!): Attributes getApplicationStages(trainee_id: String!): Stages + getInterviewStages: [stageWithInterviews!]! } type response { @@ -162,6 +175,49 @@ export const applicationStageDefs = gql` applicantStage: String! score: Float! ): response! - sendInvitation(applicantId:ID!,email: String!, platform:String!, invitationLink:String!): response! + sendInvitation( + applicantId: ID! + email: String! + platform: String! + invitationLink: String! + ): response! + } +`; + +export const technicalInterviewDefs = gql` + type TechnicalInterview { + _id: ID! + applicant: Applicant! + coordinator: User + meetingLink: String! + scheduledDate: String! + meetingPlatform: String! + status: String! + emailSent: Boolean! + createdAt: String! + updatedAt: String! + } + + input ScheduleInterviewInput { + applicantId: ID! + coordinatorId: ID! + meetingLink: String! + scheduledDate: String! + meetingPlatform: String! + } + + extend type Query { + getTechnicalInterviews(status: String): [TechnicalInterview!]! + getTechnicalInterviewById(interviewId: ID!): TechnicalInterview + } + + input UpdateInterviewStatusInput { + interviewId: ID! + status: String! + } + + extend type Mutation { + scheduleTechnicalInterview(input: ScheduleInterviewInput!): response! + updateInterviewStatus(interviewId: ID!, status: String!): response! } `; diff --git a/src/schema/loggedUser.ts b/src/schema/loggedUser.ts index 8a967610..f7f2adb7 100644 --- a/src/schema/loggedUser.ts +++ b/src/schema/loggedUser.ts @@ -16,12 +16,12 @@ export const LoggedUserSchema = gql` telephone: String password: String token: String! - isActive: Boolean, - applicationPhase: String, + isActive: Boolean + applicationPhase: String cohort: Cohort - bio: String, + bio: String - isVerified:Boolean + isVerified: Boolean } type Role { @@ -31,25 +31,37 @@ export const LoggedUserSchema = gql` permissions: [Permission] } + type PopulatedCoordinator { + _id: ID! + firstname: String! + lastname: String! + email: String! + role: Role! + } + + type TechnicalInterview { + _id: ID! + coordinatorId: PopulatedCoordinator! + } + type Cohort { id: ID! title: String - program: String - cycle: String - start: String - end: String - phase: Int - trainees:[User_Logged!] + program: String + cycle: String + start: String + end: String + phase: Int + trainees: [User_Logged!] manager: String } - type CurrentUser{ + type CurrentUser { firstName: String! lastName: String! email: String! } - input UserInput_Logged { firstname: String lastname: String @@ -80,7 +92,7 @@ export const LoggedUserSchema = gql` password: String telephone: String code: String - picture:String + picture: String applicationPhase: String cohortId: ID bio: String @@ -90,34 +102,39 @@ export const LoggedUserSchema = gql` } input UserFilterInput { - email: String - firstName: String - lastName: String - country: String - telephone: String - gender: String - authMethod: String - isActive: Boolean - } + email: String + firstName: String + lastName: String + country: String + telephone: String + gender: String + authMethod: String + isActive: Boolean + } type Query { user_Logged(ID: ID!): User_Logged! getUsers_Logged(amount: Int): [User_Logged] + getInterviewCoordinators(amount: Int): [User_Logged] checkUserRole(email: String): Role! getByFilter(filter: UserFilterInput!): [User_Logged]! getCohort(id: ID!): cohort - getAllCohorts: [cohort!]! + getAllCohorts: [cohort!]! currentUser: CurrentUser } type Mutation { createUser_Logged(userInput: UserInput_Logged): User_Logged! resendVerifcationEmail(userInput: EmailInput!): String - verifyUser(ID:ID!):User_Logged + verifyUser(ID: ID!): User_Logged deleteUser_Logged(ID: ID!): Boolean updateUser_Logged(ID: ID!, editUserInput: EditUserInput_Logged): Boolean assignRoleToUser(ID: ID!, roleID: ID!): User_Logged updateUserStatus(ID: ID!): Boolean updateUserSelf(ID: ID!, editUserInput: EditUserSelfInput_Logged): Boolean - updateApplicationPhase(userID: ID!, newPhase: String!, cohortID: ID): User_Logged + updateApplicationPhase( + userID: ID! + newPhase: String! + cohortID: ID + ): User_Logged } -`; \ No newline at end of file +`; diff --git a/src/schema/traineeApplicantSchema.ts b/src/schema/traineeApplicantSchema.ts index 6d419e5b..d85248a5 100644 --- a/src/schema/traineeApplicantSchema.ts +++ b/src/schema/traineeApplicantSchema.ts @@ -43,6 +43,7 @@ export const typeDefsTrainee = gql` coverLetterUrl: String! idDocumentUrl: String! resumeUrl: String! + technicalInterviews: [TechnicalInterview!] } type CycleApplied { @@ -66,6 +67,15 @@ export const typeDefsTrainee = gql` message: String! } + type TechnicalInterview { + _id: ID! + meetingLink: String! + scheduledDate: String! + meetingPlatform: String! + status: String! + emailSent: Boolean! + } + enum ApplicationPhase { Applied Shortlisted @@ -83,9 +93,9 @@ export const typeDefsTrainee = gql` cycle_id: ID! role: ID attributes: traineeAttributeInput - coverLetterUrl:String - idDocumentUrl:String - resumeUrl:String + coverLetterUrl: String + idDocumentUrl: String + resumeUrl: String } input traineeApplicantEmail { diff --git a/tsconfig.json b/tsconfig.json index 615c0bbc..fb8a441c 100755 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,6 +9,7 @@ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ + "jsx": "react-jsx", /* Language and Environment */ "target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ @@ -100,4 +101,4 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ } -} +} \ No newline at end of file