Skip to content

Commit d1e2e9b

Browse files
committed
Merge branch 'main' of https://github.com/uwblueprint/website-bp-be into INTF25-review-status-mutator
2 parents 5d84732 + c5ef398 commit d1e2e9b

14 files changed

+278
-36
lines changed

backend/typescript/graphql/resolvers/applicantRecordResolvers.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,41 @@
11
import ApplicantRecordService from "../../services/implementations/applicantRecordService";
22
import IApplicantRecordService from "../../services/interfaces/applicantRecordService";
3-
import { ApplicantRecordDTO } from "../../types";
3+
import { ApplicantRecordDTO, ApplicationStatus } from "../../types";
44
import { getErrorMessage } from "../../utilities/errorUtils";
55

66
const applicantRecordService: IApplicantRecordService =
77
new ApplicantRecordService();
88

99
const applicantRecordResolvers = {
1010
Mutation: {
11+
updateApplicantStatus: async (
12+
_parent: undefined,
13+
{
14+
applicantRecordId,
15+
status,
16+
}: { applicantRecordId: string; status: ApplicationStatus },
17+
): Promise<ApplicantRecordDTO> => {
18+
const applicantRecord =
19+
await applicantRecordService.updateApplicantStatus(
20+
applicantRecordId,
21+
status,
22+
);
23+
return applicantRecord;
24+
},
25+
bulkUpdateApplicantStatus: async (
26+
_parent: undefined,
27+
{
28+
applicantRecordIds,
29+
status,
30+
}: { applicantRecordIds: string[]; status: ApplicationStatus },
31+
): Promise<ApplicantRecordDTO[]> => {
32+
const applicantRecords =
33+
await applicantRecordService.bulkUpdateApplicantStatus(
34+
applicantRecordIds,
35+
status,
36+
);
37+
return applicantRecords;
38+
},
1139
setApplicantRecordFlag: async (
1240
_parent: undefined,
1341
{

backend/typescript/graphql/resolvers/reviewDashboardResolvers.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
import ReviewDashboardService from "../../services/implementations/reviewDashboardService";
2-
import { ReviewDashboardRowDTO } from "../../types";
2+
import {
3+
ReviewDashboardRowDTO,
4+
ReviewDashboardSidePanelDTO,
5+
} from "../../types";
36
import { getErrorMessage } from "../../utilities/errorUtils";
47

58
const reviewDashboardService = new ReviewDashboardService();
@@ -19,6 +22,18 @@ const reviewDashboardResolvers = {
1922
throw new Error(getErrorMessage(error));
2023
}
2124
},
25+
reviewDashboardSidePanel: async (
26+
_parent: undefined,
27+
args: { applicantId: string },
28+
): Promise<ReviewDashboardSidePanelDTO> => {
29+
try {
30+
return await reviewDashboardService.getReviewDashboardSidePanel(
31+
args.applicantId,
32+
);
33+
} catch (error) {
34+
throw new Error(getErrorMessage(error));
35+
}
36+
},
2237
},
2338
};
2439

backend/typescript/graphql/types/applicantRecordType.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,37 @@
11
import { gql } from "apollo-server-express";
22

33
const applicantRecordType = gql`
4+
enum ApplicationStatus {
5+
Applied
6+
InReview
7+
Reviewed
8+
Selected
9+
Interviewed
10+
Offer
11+
Rejected
12+
}
13+
414
type ApplicantRecordDTO {
515
id: String!
616
applicantId: String!
717
position: String!
818
roleSpecificQuestions: [String!]!
919
choice: Int!
10-
status: String!
20+
status: ApplicationStatus!
1121
skillCategory: String
1222
combined_score: Int
1323
isApplicantFlagged: Boolean!
1424
}
1525
1626
extend type Mutation {
27+
updateApplicantStatus(
28+
applicantRecordId: String!
29+
status: ApplicationStatus!
30+
): ApplicantRecordDTO!
31+
bulkUpdateApplicantStatus(
32+
applicantRecordIds: [String!]!
33+
status: ApplicationStatus!
34+
): [ApplicantRecordDTO!]!
1735
setApplicantRecordFlag(
1836
applicantRecordId: String!
1937
flagValue: Boolean!

backend/typescript/graphql/types/reviewDashboardType.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,30 @@ const reviewDashboardType = gql`
1717
totalScore: Int
1818
}
1919
20+
type ReviewDetails {
21+
reviewerFirstName: String!
22+
reviewerLastName: String!
23+
review: Review!
24+
}
25+
26+
type ReviewDashboardSidePanelDTO {
27+
firstName: String!
28+
lastName: String!
29+
positionTitle: String!
30+
program: String!
31+
resumeUrl: String!
32+
applicationStatus: String!
33+
skillCategory: String
34+
reviewDetails: [ReviewDetails!]!
35+
}
36+
2037
extend type Query {
2138
reviewDashboard(
2239
pageNumber: Int!
2340
resultsPerPage: Int!
2441
): [ReviewDashboardRowDTO!]!
42+
43+
reviewDashboardSidePanel(applicantId: String!): ReviewDashboardSidePanelDTO!
2544
}
2645
`;
2746

backend/typescript/graphql/types/reviewPageType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { gql } from "apollo-server-express";
22

33
const reviewPageType = gql`
44
type ApplicationDTO {
5-
id: Int!
5+
id: String!
66
academicOrCoop: String!
77
academicYear: String!
88
email: String!

backend/typescript/graphql/types/reviewedApplicantRecordTypes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const reviewedApplicantRecordTypes = gql`
2020
desireToLearn: Int
2121
skill: Int
2222
skillCategory: SkillCategory
23+
comments: String
2324
}
2425
2526
input ReviewInput {

backend/typescript/models/applicant.model.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,11 @@ import ApplicantRecord from "./applicantRecord.model";
77
@Table({ tableName: "applicants" })
88
export default class Applicant extends Model {
99
@Column({
10-
type: DataType.INTEGER,
10+
type: DataType.UUIDV4,
1111
primaryKey: true,
1212
unique: true,
13-
autoIncrement: true,
1413
})
15-
id!: number;
14+
id!: string;
1615

1716
@Column({ type: DataType.STRING })
1817
academicOrCoop!: string;

backend/typescript/models/applicantRecord.model.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ export default class ApplicantRecord extends Model {
3030
id!: string;
3131

3232
@ForeignKey(() => Applicant)
33-
@Column({ type: DataType.INTEGER })
34-
applicantId!: number;
33+
@Column({ type: DataType.UUIDV4 })
34+
applicantId!: string;
3535

3636
@ForeignKey(() => Position)
3737
@Column({ type: DataType.STRING })

backend/typescript/services/implementations/applicantRecordService.ts

Lines changed: 79 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,98 @@
1-
import { ApplicantRecordDTO, PositionTitle } from "../../types";
1+
import {
2+
ApplicantRecordDTO,
3+
PositionTitle,
4+
ApplicationStatus,
5+
} from "../../types";
26
import { getErrorMessage } from "../../utilities/errorUtils";
37
import logger from "../../utilities/logger";
48
import ApplicantRecord from "../../models/applicantRecord.model";
59
import IApplicantRecordService from "../interfaces/applicantRecordService";
10+
import { sequelize } from "../../models";
611

712
const Logger = logger(__filename);
813

14+
function toDTO(applicantRecord: ApplicantRecord): ApplicantRecordDTO {
15+
return {
16+
id: String(applicantRecord.id),
17+
applicantId: String(applicantRecord.applicantId),
18+
position: applicantRecord.position as PositionTitle,
19+
roleSpecificQuestions: applicantRecord.roleSpecificQuestions,
20+
choice: applicantRecord.choice,
21+
status: applicantRecord.status,
22+
skillCategory: applicantRecord.skillCategory,
23+
combined_score: applicantRecord.combined_score,
24+
isApplicantFlagged: applicantRecord.isApplicantFlagged,
25+
};
26+
}
27+
28+
export const getApplicantRecord = async (
29+
applicantRecordId: string,
30+
): Promise<ApplicantRecord> => {
31+
const applicantRecord = await ApplicantRecord.findByPk(applicantRecordId);
32+
if (!applicantRecord) {
33+
throw new Error(`ApplicantRecord with id ${applicantRecordId} not found.`);
34+
}
35+
return applicantRecord;
36+
};
37+
938
class ApplicantRecordService implements IApplicantRecordService {
1039
/* eslint-disable class-methods-use-this */
40+
41+
async updateApplicantStatus(
42+
applicantRecordId: string,
43+
status: ApplicationStatus,
44+
): Promise<ApplicantRecordDTO> {
45+
try {
46+
const applicantRecord = await getApplicantRecord(applicantRecordId);
47+
applicantRecord.status = status;
48+
await applicantRecord.save();
49+
return toDTO(applicantRecord);
50+
} catch (error: unknown) {
51+
Logger.error(
52+
`Failed to update applicant record status. Reason = ${getErrorMessage(
53+
error,
54+
)}`,
55+
);
56+
throw error;
57+
}
58+
}
59+
60+
async bulkUpdateApplicantStatus(
61+
applicantRecordIds: string[],
62+
status: ApplicationStatus,
63+
): Promise<ApplicantRecordDTO[]> {
64+
const transaction = await sequelize.transaction();
65+
try {
66+
const updatedRecords = await Promise.all(
67+
applicantRecordIds.map(async (id) => {
68+
const applicantRecord = await getApplicantRecord(id);
69+
applicantRecord.status = status;
70+
await applicantRecord.save({ transaction });
71+
return toDTO(applicantRecord);
72+
}),
73+
);
74+
await transaction.commit();
75+
return updatedRecords;
76+
} catch (error: unknown) {
77+
await transaction.rollback();
78+
Logger.error(
79+
`Failed to update applicant record statuses. Reason = ${getErrorMessage(
80+
error,
81+
)}`,
82+
);
83+
throw error;
84+
}
85+
}
86+
1187
async setApplicantRecordFlag(
1288
applicantRecordId: string,
1389
flagValue: boolean,
1490
): Promise<ApplicantRecordDTO> {
1591
try {
16-
const applicantRecord = await ApplicantRecord.findByPk(applicantRecordId);
17-
if (!applicantRecord) {
18-
throw new Error(
19-
`ApplicantRecord with id ${applicantRecordId} not found.`,
20-
);
21-
}
92+
const applicantRecord = await getApplicantRecord(applicantRecordId);
2293
applicantRecord.isApplicantFlagged = flagValue;
2394
await applicantRecord.save();
24-
return {
25-
id: String(applicantRecord.id),
26-
applicantId: String(applicantRecord.applicantId),
27-
position: applicantRecord.position as PositionTitle,
28-
roleSpecificQuestions: applicantRecord.roleSpecificQuestions,
29-
choice: applicantRecord.choice,
30-
status: applicantRecord.status,
31-
skillCategory: applicantRecord.skillCategory,
32-
combined_score: applicantRecord.combined_score,
33-
isApplicantFlagged: applicantRecord.isApplicantFlagged,
34-
};
95+
return toDTO(applicantRecord);
3596
} catch (error: unknown) {
3697
Logger.error(
3798
`Failed to set applicant record flag. Reason = ${getErrorMessage(

backend/typescript/services/implementations/reviewDashboardService.ts

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { ReviewDashboardRowDTO } from "../../types";
1+
import {
2+
PositionTitle,
3+
ReviewDashboardRowDTO,
4+
ReviewDashboardSidePanelDTO,
5+
} from "../../types";
26
import IReviewDashboardService from "../interfaces/IReviewDashboardService";
37
import { getErrorMessage } from "../../utilities/errorUtils";
48
import logger from "../../utilities/logger";
@@ -22,6 +26,26 @@ function toDTO(model: ApplicantRecord): ReviewDashboardRowDTO {
2226
};
2327
}
2428

29+
function toSidePanelDTO(model: ApplicantRecord): ReviewDashboardSidePanelDTO {
30+
const reviewDetails =
31+
model.reviewedApplicantRecords?.map((reviewRecord) => ({
32+
reviewerFirstName: reviewRecord.user?.first_name || "",
33+
reviewerLastName: reviewRecord.user?.last_name || "",
34+
review: reviewRecord.review,
35+
})) || [];
36+
37+
return {
38+
firstName: model.applicant!.firstName,
39+
lastName: model.applicant!.lastName,
40+
positionTitle: model.position as PositionTitle,
41+
program: model.applicant!.program,
42+
resumeUrl: model.applicant!.resumeUrl,
43+
applicationStatus: model.status,
44+
skillCategory: model.skillCategory,
45+
reviewDetails,
46+
};
47+
}
48+
2549
class ReviewDashboardService implements IReviewDashboardService {
2650
/* eslint-disable class-methods-use-this */
2751
async getReviewDashboard(
@@ -72,6 +96,47 @@ class ReviewDashboardService implements IReviewDashboardService {
7296
throw error;
7397
}
7498
}
99+
100+
async getReviewDashboardSidePanel(
101+
applicantId: string,
102+
): Promise<ReviewDashboardSidePanelDTO> {
103+
try {
104+
const applicantRecord: ApplicantRecord | null =
105+
await ApplicantRecord.findOne({
106+
where: { applicantId },
107+
attributes: { exclude: ["createdAt", "updatedAt"] },
108+
include: [
109+
{
110+
attributes: { exclude: ["createdAt", "updatedAt"] },
111+
association: "reviewedApplicantRecords",
112+
include: [
113+
{
114+
attributes: { exclude: ["createdAt", "updatedAt"] },
115+
association: "user",
116+
},
117+
],
118+
},
119+
{
120+
attributes: { exclude: ["createdAt", "updatedAt"] },
121+
association: "applicant",
122+
},
123+
],
124+
});
125+
126+
if (!applicantRecord || !applicantRecord.applicant) {
127+
throw new Error(`Applicant with ID ${applicantId} not found`);
128+
}
129+
130+
return toSidePanelDTO(applicantRecord);
131+
} catch (error: unknown) {
132+
Logger.error(
133+
`Failed to get review dashboard side panel for applicant ${applicantId}. Reason = ${getErrorMessage(
134+
error,
135+
)}`,
136+
);
137+
throw error;
138+
}
139+
}
75140
}
76141

77142
export default ReviewDashboardService;

0 commit comments

Comments
 (0)