Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

#214 Applicant stage #130

Open
wants to merge 2 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 10 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ import { attendanceResolver } from "./resolvers/attendanceResolver";
import { attendanceSchema } from "./schema/attendanceSchema";
import { performanceResolver } from "./resolvers/performanceResolver";
import { performanceSchema } from "./schema/performanceSchema";

import { applicationStageDefs } from './schema/applicationStage';
import { applicationStageResolvers } from './resolvers/applicationStageResolver';
import filterJobResolver from "./resolvers/filterJob";
import filterProgramResolver from "./resolvers/filterPrograms";
import filterRoleResolver from "./resolvers/filterRole";
Expand All @@ -68,6 +69,8 @@ import { SearchSchema } from "./schema/searchSchema";
import { searchResolver } from "./resolvers/searchResolver";
import {appliedJobResolver} from "./resolvers/appliedJobResolver";
import { appliedJobTypeDefs } from "./schema/appliedJobTypeDefs";
import { applicantStageResolvers } from "./resolvers/applicantStage";
import { applicantStageDefs } from "./schema/applicantStage";


const PORT = process.env.PORT || 3000;
Expand Down Expand Up @@ -105,7 +108,9 @@ const resolvers = mergeResolvers([
applicantNotificationResolver,
passwordResolvers,
searchResolver,
appliedJobResolver
appliedJobResolver,
applicationStageResolvers,
applicantStageResolvers
]);
const typeDefs = mergeTypeDefs([
applicationCycleTypeDefs,
Expand Down Expand Up @@ -137,7 +142,9 @@ const typeDefs = mergeTypeDefs([
SearchSchema,
appliedJobTypeDefs,
performanceSchema,
attendanceSchema
attendanceSchema,
applicationStageDefs,
applicantStageDefs
]);

const server = new ApolloServer({
Expand Down
83 changes: 83 additions & 0 deletions src/models/stageSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import mongoose, { Schema, Document } from "mongoose";

interface IStageTracking extends Document {
traineeApplicant: mongoose.Schema.Types.ObjectId;
stage:
| "Shortlisted"
| "Technical Assessment"
| "Interview Assessment"
| "Admitted"
| "Dismissed";
enteredAt: Date;
exitedAt?: Date;
comments?: string;
score?: number;
interviewScore?: number;
}

const stageTrackingSchema = new Schema<IStageTracking>({
traineeApplicant: {
type: Schema.Types.ObjectId,
ref: "Applicant",
required: true,
},
stage: {
type: String,
enum: [
"Shortlisted",
"Technical Assessment",
"Interview Assessment",
"Admitted",
"Dismissed",
],
required: true,
},
enteredAt: {
type: Date,
default: Date.now,
},
exitedAt: {
type: Date,
},
comments: {
type: String,
required: true,
},
score: {
type: Number,
required: function (this: IStageTracking) {
return this.stage === "Technical Assessment";
},
validate: {
validator: function (this: IStageTracking, value: number) {
return (
this.stage !== "Technical Assessment" || (value >= 0 && value <= 100)
);
},
message: "Score must be between 0 and 100 for Technical Assessment",
},
default:0,
},
interviewScore: {
type: Number,
required: function (this: IStageTracking) {
return this.stage === "Interview Assessment";
},
validate: {
validator: function (this: IStageTracking, value: number) {
return (
this.stage !== "Interview Assessment" || (value >= 0 && value <= 2)
);
},
message: "Interview score must be between 0 and 2",
},
default:0,
}
});

const StageTracking = mongoose.model<IStageTracking>(
"StageTracking",
stageTrackingSchema
);

export default StageTracking;
6 changes: 3 additions & 3 deletions src/models/traineeApplicant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,6 @@ const TraineeApplicant = mongoose.model(
},
email: {
type: String,
required: true,
unique: true,
},
firstName: {
type: String,
Expand All @@ -30,7 +28,7 @@ const TraineeApplicant = mongoose.model(
},
applicationPhase: {
type: String,
enum: ["Applied", "Interviewed", "Accepted", "Enrolled"],
enum: ["Applied", 'Shortlisted', 'Technical Assessment', 'Interview Assessment', 'Admitted', 'Dismissed', "Enrolled"],
default: "Applied",
},
status: {
Expand All @@ -41,6 +39,8 @@ const TraineeApplicant = mongoose.model(
type: Schema.Types.ObjectId,
ref: "cohortModel",
}
}, {
timestamps: true
})
);

Expand Down
70 changes: 70 additions & 0 deletions src/resolvers/applicantStage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import { RoleModel } from "../models/roleModel";
import TraineeApplicant from "../models/traineeApplicant";
import StageTracking from "../models/stageSchema";
import { CustomGraphQLError } from "../utils/customErrorHandler";

export const applicantStageResolvers: any = {
Query: {
getTraineeCyclesApplications: async (_: any, __: any, context: any) => {
try {
if (!context.currentUser) {
throw new CustomGraphQLError("You must be logged in to view your applications");
}

const applications = await TraineeApplicant.find({ user: context.currentUser._id })
.populate('cohort')
.populate('cycle_id');
return applications;
} catch (error: any) {
throw new CustomGraphQLError(error.message);
}
},
getCycleApplicationStages: async (_: any, { traineeApplicant }: any, context: any) => {
try {
if (!context.currentUser) {
throw new CustomGraphQLError("You must be logged in to view your application stages");
}
const stages = await StageTracking.find({ traineeApplicant });
if (!stages || stages.length === 0) {
throw new CustomGraphQLError("No stages found for this application");
}
return { stages, message: "Stages retrieved successfully!" }
} catch (error: any) {
throw new CustomGraphQLError(error.message);
}
}
},
Mutation: {
applyCycle: async (_: any, { cycle_id }: any, context: any) => {
try {
if (!context.currentUser) {
throw new CustomGraphQLError("You must be logged in to apply in the Cycle");
}

const [userRole, isUserAlreadyApplied] = await Promise.all([
RoleModel.findById(context.currentUser.role),
TraineeApplicant.findOne({ user: context.currentUser._id, cycle_id })
]);

if (isUserAlreadyApplied) {
throw new CustomGraphQLError("You have already applied to this Cycle");
}

let newApplicationData = {
user: context.currentUser._id,
email: context.currentUser.email,
firstName: context.currentUser.firstName || " ",
lastName: context.currentUser.lastName || " ",
cycle_id: cycle_id
};

const newApplication = new TraineeApplicant(newApplicationData);
await newApplication.save();

return { message: "Application submitted successfully" };
} catch (error: any) {
throw new CustomGraphQLError(error.message);
}
}
}
};
106 changes: 106 additions & 0 deletions src/resolvers/applicationStageResolver.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import { CustomGraphQLError } from "../utils/customErrorHandler";
import TraineeApplicant from "../models/traineeApplicant";
import StageTracking from "../models/stageSchema";
import { RoleModel } from "../models/roleModel";
export const applicationStageResolvers: any = {
Query: {
getStageHistory: async (_: any, { applicantId }: any) => {
return StageTracking.find({ traineeApplicant: applicantId });
},
},
Mutation: {
moveToNextStage: async (
_: any,
{ applicantId, nextStage, comments, score }: 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 currentStage = await StageTracking.findOne({
traineeApplicant: applicantId,
exitedAt: { $exists: false },
});

if (currentStage && currentStage.stage === nextStage) {
throw new CustomGraphQLError(
"The applicant is already in this stage."
);
}

if (currentStage) {
currentStage.exitedAt = new Date();
await currentStage.save();
}
let newStageData: any = {
traineeApplicant: applicantId,
stage: nextStage,
comments: comments,
enteredAt: new Date(),
};

if (nextStage === "Technical Assessment") {
if (score === undefined || score < 0 || score > 100) {
throw new CustomGraphQLError(
"Score must be between 0 and 100 for Technical Assessment."
);
}
newStageData.score = score;
} else if (nextStage === "Interview Assessment") {
if (score === undefined || score < 0 || score > 2) {
throw new CustomGraphQLError(
"Interview score must be between 0 and 2."
);
}

newStageData.interviewScore = score;
} else if (nextStage === "Admitted" || nextStage === "Dismissed") {
newStageData.score = 0;
}
const newStage = new StageTracking(newStageData);
await newStage.save();
const applicant = await TraineeApplicant.findById(applicantId);
if (applicant) {
applicant.applicationPhase = nextStage;
await applicant.save();
}

const stages = await StageTracking.find({
traineeApplicant: applicantId,
});

const formattedStages = stages.map((stage) => ({
stage: stage.stage,
comments: stage.comments,
score: stage.score,
enteredAt: stage.enteredAt.toLocaleString(),
exitedAt: stage.exitedAt ? stage.exitedAt.toLocaleString() : null,
}));

return {
id: applicant?._id,
applicationPhase: applicant?.applicationPhase,
stages: formattedStages,
};
} catch (error: any) {
return new Error(error.message);
}
},
},
};
Loading