Skip to content

Commit f61a60f

Browse files
authored
Merge branch 'develop' into #2028-delete-design-review-hook
2 parents f3845f0 + 76e13bd commit f61a60f

46 files changed

Lines changed: 1541 additions & 554 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/backend/src/controllers/design-reviews.controllers.ts

Lines changed: 29 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,15 @@ export default class DesignReviewsController {
1313
}
1414
}
1515

16+
static async getAllTeamTypes(req: Request, res: Response, next: NextFunction) {
17+
try {
18+
const teamTypes = await DesignReviewsService.getAllTeamTypes();
19+
return res.status(200).json(teamTypes);
20+
} catch (error: unknown) {
21+
next(error);
22+
}
23+
}
24+
1625
static async deleteDesignReview(req: Request, res: Response, next: NextFunction) {
1726
try {
1827
const drId: string = req.params.designReviewId;
@@ -27,33 +36,16 @@ export default class DesignReviewsController {
2736
static async createDesignReview(req: Request, res: Response, next: NextFunction) {
2837
try {
2938
const submitter: User = await getCurrentUser(res);
30-
const {
31-
dateScheduled,
32-
teamTypeId,
33-
requiredMemberIds,
34-
optionalMemberIds,
35-
location,
36-
isOnline,
37-
isInPerson,
38-
zoomLink,
39-
docTemplateLink,
40-
wbsNum,
41-
meetingTimes
42-
} = req.body;
39+
const { dateScheduled, teamTypeId, requiredMemberIds, optionalMemberIds, wbsNum, meetingTimes } = req.body;
4340

4441
const createdDesignReview = await DesignReviewsService.createDesignReview(
4542
submitter,
4643
dateScheduled,
4744
teamTypeId,
4845
requiredMemberIds,
4946
optionalMemberIds,
50-
isOnline,
51-
isInPerson,
52-
docTemplateLink,
5347
wbsNum,
54-
meetingTimes,
55-
zoomLink,
56-
location
48+
meetingTimes
5749
);
5850
return res.status(200).json(createdDesignReview);
5951
} catch (error: unknown) {
@@ -78,8 +70,8 @@ export default class DesignReviewsController {
7870
const {
7971
dateScheduled,
8072
teamType,
81-
requiredMembers,
82-
optionalMembers,
73+
requiredMembersIds,
74+
optionalMembersIds,
8375
isOnline,
8476
isInPerson,
8577
zoomLink,
@@ -100,8 +92,8 @@ export default class DesignReviewsController {
10092
designReviewId,
10193
dateScheduled,
10294
teamType.teamTypeId,
103-
requiredMembers,
104-
optionalMembers,
95+
requiredMembersIds,
96+
optionalMembersIds,
10597
isOnline,
10698
isInPerson,
10799
zoomLink,
@@ -116,4 +108,18 @@ export default class DesignReviewsController {
116108
next(error);
117109
}
118110
}
111+
112+
// Mark the current user as confirmed for the given design review
113+
static async markUserConfirmed(req: Request, res: Response, next: NextFunction) {
114+
try {
115+
const { availability } = req.body;
116+
const { designReviewId } = req.params;
117+
const user = await getCurrentUser(res);
118+
119+
const updatedDesignReview = await DesignReviewsService.markUserConfirmed(designReviewId, availability, user);
120+
return res.status(200).json(updatedDesignReview);
121+
} catch (error: unknown) {
122+
next(error);
123+
}
124+
}
119125
}

src/backend/src/routes/design-reviews.routes.ts

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ const designReviewsRouter = express.Router();
77

88
designReviewsRouter.get('/', DesignReviewsController.getAllDesignReviews);
99

10+
designReviewsRouter.get('/teamType/all', DesignReviewsController.getAllTeamTypes);
11+
1012
designReviewsRouter.delete('/:designReviewId/delete', DesignReviewsController.deleteDesignReview);
1113
designReviewsRouter.get('/:designReviewId', DesignReviewsController.getSingleDesignReview);
1214

@@ -18,12 +20,9 @@ designReviewsRouter.post(
1820
intMinZero(body('requiredMemberIds.*')),
1921
body('optionalMemberIds').isArray(),
2022
intMinZero(body('optionalMemberIds.*')),
21-
nonEmptyString(body('location').optional()),
22-
body('isOnline').isBoolean(),
23-
body('isInPerson').isBoolean(),
24-
nonEmptyString(body('zoomLink').optional()),
25-
nonEmptyString(body('docTemplateLink')).optional(),
26-
body('wbsNum'),
23+
intMinZero(body('wbsNum.carNumber')),
24+
intMinZero(body('wbsNum.projectNumber')),
25+
intMinZero(body('wbsNum.workPackageNumber')),
2726
body('meetingTimes').isArray(),
2827
intMinZero(body('meetingTimes.*')),
2928
validateInputs,
@@ -52,4 +51,12 @@ designReviewsRouter.post(
5251
DesignReviewsController.editDesignReviews
5352
);
5453

54+
designReviewsRouter.post(
55+
'/:designReviewId/confirm-schedule',
56+
body('availability').isArray(),
57+
intMinZero(body('availability.*')),
58+
validateInputs,
59+
DesignReviewsController.markUserConfirmed
60+
);
61+
5562
export default designReviewsRouter;

src/backend/src/services/design-reviews.services.ts

Lines changed: 95 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { Design_Review_Status, User } from '@prisma/client';
2-
import { DesignReview, WbsNumber, isAdmin, isLeadership, isNotLeadership } from 'shared';
2+
import { DesignReview, TeamType, WbsNumber, isAdmin, isLeadership, isNotLeadership } from 'shared';
33
import prisma from '../prisma/prisma';
44
import {
55
NotFoundException,
@@ -10,7 +10,7 @@ import {
1010
AccessDeniedException
1111
} from '../utils/errors.utils';
1212
import { getUsers, getPrismaQueryUserIds } from '../utils/users.utils';
13-
import { validateMeetingTimes } from '../utils/design-reviews.utils';
13+
import { isUserOnDesignReview, validateMeetingTimes } from '../utils/design-reviews.utils';
1414
import designReviewQueryArgs from '../prisma-query-args/design-reviews.query-args';
1515
import { designReviewTransformer } from '../transformers/design-reviews.transformer';
1616
import { sendSlackDesignReviewNotification } from '../utils/slack.utils';
@@ -27,6 +27,11 @@ export default class DesignReviewsService {
2727
return designReviews.map(designReviewTransformer);
2828
}
2929

30+
static async getAllTeamTypes(): Promise<TeamType[]> {
31+
const teamTypes = await prisma.teamType.findMany();
32+
return teamTypes;
33+
}
34+
3035
/**
3136
* Deletes a design review
3237
* @param submitter the user who deleted the design review
@@ -55,34 +60,24 @@ export default class DesignReviewsService {
5560
}
5661

5762
/**
58-
* Creates a design review
59-
* @param submitter user who submitted the design review
63+
* Create a design review
64+
* @param submitter User submitting the design review
6065
* @param dateScheduled when the design review is scheduled for
61-
* @param teamTypeId team type id of the design review
62-
* @param requiredMemberIds ids of the required members to attend the design review
63-
* @param optionalMemberIds ids of the optional members to attend the design reivew
64-
* @param isOnline if design review is online
65-
* @param isInPerson if design review is in person
66-
* @param docTemplateLink link to the doc template
67-
* @param wbsNum wbs number for the design review
68-
* @param meetingTimes the meeting times for the design review
69-
* @param zoomLink link for the zoom if design review is online
70-
* @param location location of the design review if in person
71-
* @returns a design review
66+
* @param teamTypeId team type id
67+
* @param requiredMemberIds ids of members who are required to go
68+
* @param optionalMemberIds ids of members who do not have to go
69+
* @param wbsNum wbs num related to the design review
70+
* @param meetingTimes meeting times of the design review
71+
* @returns a new design review
7272
*/
7373
static async createDesignReview(
7474
submitter: User,
7575
dateScheduled: Date,
7676
teamTypeId: string,
7777
requiredMemberIds: number[],
7878
optionalMemberIds: number[],
79-
isOnline: boolean,
80-
isInPerson: boolean,
81-
docTemplateLink: string,
8279
wbsNum: WbsNumber,
83-
meetingTimes: number[],
84-
zoomLink?: string,
85-
location?: string
80+
meetingTimes: number[]
8681
): Promise<DesignReview> {
8782
if (!isLeadership(submitter.role)) throw new AccessDeniedException('create design review');
8883

@@ -125,14 +120,6 @@ export default class DesignReviewsService {
125120
}
126121
}
127122

128-
if (isOnline && !zoomLink) {
129-
throw new HttpException(400, 'If the design review is online then there needs to be a zoom link');
130-
}
131-
132-
if (isInPerson && !location) {
133-
throw new HttpException(400, 'If the design review is in person then there needs to be a location');
134-
}
135-
136123
if (dateScheduled.valueOf() < new Date().valueOf()) {
137124
throw new HttpException(400, 'Design review cannot be scheduled for a past day');
138125
}
@@ -142,11 +129,8 @@ export default class DesignReviewsService {
142129
dateScheduled,
143130
dateCreated: new Date(),
144131
status: Design_Review_Status.UNCONFIRMED,
145-
location,
146-
isOnline,
147-
isInPerson,
148-
zoomLink,
149-
docTemplateLink,
132+
isOnline: false,
133+
isInPerson: false,
150134
userCreated: { connect: { userId: submitter.userId } },
151135
teamType: { connect: { teamTypeId: teamType.teamTypeId } },
152136
requiredMembers: { connect: requiredMemberIds.map((memberId) => ({ userId: memberId })) },
@@ -178,7 +162,11 @@ export default class DesignReviewsService {
178162
for (const memberUserSetting of memberUserSettings) {
179163
if (memberUserSetting.slackId) {
180164
try {
181-
await sendSlackDesignReviewNotification(memberUserSetting.slackId, designReview.designReviewId);
165+
await sendSlackDesignReviewNotification(
166+
memberUserSetting.slackId,
167+
designReview.designReviewId,
168+
designReview.wbsElement.name
169+
);
182170
} catch (err: unknown) {
183171
if (err instanceof Error) {
184172
throw new HttpException(500, `Failed to send slack notification: ${err.message}`);
@@ -316,4 +304,76 @@ export default class DesignReviewsService {
316304
});
317305
return designReviewTransformer(updateDesignReview);
318306
}
307+
308+
/**
309+
* Edits a design review by confirming a given user's availability and also updating their schedule settings with the given availability
310+
* @param submitter the member that is being confirmed
311+
* @param designReviewId the id of the design review
312+
* @param availability the given member's availabilities
313+
* @returns the modified design review with its updated confirmedMembers
314+
*/
315+
static async markUserConfirmed(designReviewId: string, availability: number[], submitter: User): Promise<DesignReview> {
316+
const designReview = await prisma.design_Review.findUnique({
317+
where: { designReviewId },
318+
...designReviewQueryArgs
319+
});
320+
321+
if (!designReview) throw new NotFoundException('Design Review', designReviewId);
322+
323+
if (designReview.dateDeleted) throw new DeletedException('Design Review', designReviewId);
324+
325+
if (!isUserOnDesignReview(submitter, designReviewTransformer(designReview)))
326+
throw new HttpException(400, 'Current user is not in the list of this design reviews members');
327+
328+
availability.forEach((time) => {
329+
if (time < 0 || time > 83) {
330+
throw new HttpException(400, 'Availability times have to be in range 0-83');
331+
}
332+
});
333+
334+
await prisma.schedule_Settings.upsert({
335+
where: { userId: submitter.userId },
336+
update: {
337+
availability
338+
},
339+
create: {
340+
userId: submitter.userId,
341+
personalGmail: '',
342+
personalZoomLink: '',
343+
availability
344+
}
345+
});
346+
347+
// set submitter as confirmed if they're not already
348+
if (!designReview.confirmedMembers.map((user) => user.userId).includes(submitter.userId)) {
349+
const updatedDesignReview = await prisma.design_Review.update({
350+
where: { designReviewId },
351+
...designReviewQueryArgs,
352+
data: {
353+
confirmedMembers: {
354+
connect: {
355+
userId: submitter.userId
356+
}
357+
}
358+
}
359+
});
360+
361+
// If all requested attendees have confirmed their schedule, mark design review as confirmed
362+
if (
363+
updatedDesignReview.confirmedMembers.length ===
364+
designReview.requiredMembers.length + designReview.optionalMembers.length
365+
) {
366+
await prisma.design_Review.update({
367+
where: { designReviewId },
368+
...designReviewQueryArgs,
369+
data: {
370+
status: Design_Review_Status.CONFIRMED
371+
}
372+
});
373+
}
374+
375+
return designReviewTransformer(updatedDesignReview);
376+
}
377+
return designReviewTransformer(designReview);
378+
}
319379
}
Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
1+
import { DesignReview, User } from 'shared';
12
import { HttpException } from './errors.utils';
23

34
/**
45
* Validate meeting times
56
* @param nums the meeting times
67
* @returns the meeting times
78
*/
8-
export function validateMeetingTimes(nums: number[]): number[] {
9-
for (let i = 1; i < nums.length; i++) {
9+
export const validateMeetingTimes = (nums: number[]): number[] => {
10+
for (let i = 0; i < nums.length; i++) {
1011
if (nums[i] < 0 || nums[i] > 83) {
11-
throw new HttpException(400, 'meeting time must be between 0-83');
12+
throw new HttpException(400, 'Meeting times have to be in range 0-83');
1213
}
13-
if (nums[i] !== nums[i - 1] + 1) {
14-
throw new HttpException(400, 'meeting times must be consecutive');
14+
if (i > 0 && nums[i] !== nums[i - 1] + 1) {
15+
throw new HttpException(400, 'Meeting times have to be consecutive');
1516
}
1617
}
1718
return nums;
18-
}
19+
};
20+
21+
export const isUserOnDesignReview = (user: User, designReview: DesignReview): boolean => {
22+
const requiredMembers = designReview.requiredMembers.map((user) => user.userId);
23+
const optionalMembers = designReview.optionalMembers.map((user) => user.userId);
24+
return requiredMembers.includes(user.userId) || optionalMembers.includes(user.userId);
25+
};

src/backend/src/utils/slack.utils.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,11 +91,15 @@ export const sendReimbursementRequestDeniedNotification = async (slackId: string
9191
}
9292
};
9393

94-
export const sendSlackDesignReviewNotification = async (slackId: string, designReviewId: string) => {
94+
export const sendSlackDesignReviewNotification = async (
95+
slackId: string,
96+
designReviewId: string,
97+
designReviewName: string
98+
) => {
9599
if (process.env.NODE_ENV !== 'production') return; // don't send msgs unless in prod
96-
const msg = `You have been invited to a Design Review!`;
97-
const fullLink = `https://finishlinebyner.com/design-reviews/${designReviewId}`;
98-
const linkButtonText = 'RSVP for the Design Review';
100+
const msg = `You have been invited to the ${designReviewName} Design Review!`;
101+
const fullLink = `https://finishlinebyner.com/settings/preferences?drId=${designReviewId}`;
102+
const linkButtonText = 'Confirm Availability';
99103

100104
try {
101105
await sendMessage(slackId, msg, fullLink, linkButtonText);

0 commit comments

Comments
 (0)