Skip to content

Commit 1ec3cbc

Browse files
authored
Merge pull request #3459 from Northeastern-Electric-Racing/#3458-part-page-notifications
#3458 notifications
2 parents 500044a + 3bc3514 commit 1ec3cbc

9 files changed

Lines changed: 286 additions & 77 deletions

File tree

src/backend/src/controllers/part-review.controllers.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,26 @@ export default class PartReviewController {
372372
}
373373
}
374374

375+
static async notifyReviewer(req: Request, res: Response, next: NextFunction) {
376+
try {
377+
const { reviewerId, partId } = req.body;
378+
await PartReviewService.notifyReviewer(reviewerId, partId, req.organization.organizationId);
379+
res.status(200).json({ message: 'Successfully notified reviewer' });
380+
} catch (error) {
381+
next(error);
382+
}
383+
}
384+
385+
static async notifyAssignee(req: Request, res: Response, next: NextFunction) {
386+
try {
387+
const { assigneeId, partId } = req.body;
388+
await PartReviewService.notifyAssignee(assigneeId, partId, req.organization.organizationId);
389+
res.status(200).json({ message: 'Successfully notified assignee' });
390+
} catch (error) {
391+
next(error);
392+
}
393+
}
394+
375395
static async createPartReviewPopup(req: Request, res: Response, next: NextFunction) {
376396
try {
377397
const user = req.currentUser;

src/backend/src/routes/parts.routes.ts

Lines changed: 10 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -166,50 +166,28 @@ partsRouter.post('/popup/:popupId/delete', PartReviewController.deletePartReview
166166
partsRouter.post('/reviewRequest/:reviewRequestId/delete', PartReviewController.deletePartReviewRequest);
167167

168168
partsRouter.post(
169-
'/:partId/reviewRequest/create',
169+
'/notifyReviewer',
170170
nonEmptyString(body('reviewerId')),
171+
nonEmptyString(body('partId')),
171172
validateInputs,
172-
PartReviewController.createPartReviewRequest
173+
PartReviewController.notifyReviewer
173174
);
174-
partsRouter.post('/reviewRequest/:reviewRequestId/delete', PartReviewController.deletePartReviewRequest);
175-
176-
partsRouter.post('/:partId/upload-preview', upload.single('image'), PartReviewController.uploadPreview);
177175

178176
partsRouter.post(
179-
'/:partId/update',
180-
intMinZero(body('index')),
181-
nonEmptyString(body('commonName')),
182-
body('description').optional().isString(),
183-
body('reviewStatus').custom((value) => Object.values(Review_Status).includes(value)),
184-
body('tagIds').isArray(),
185-
body('assigneeIds').isArray(),
186-
body('reviewerIds').isArray(),
177+
'/notifyAssignee',
178+
nonEmptyString(body('assigneeId')),
179+
nonEmptyString(body('partId')),
187180
validateInputs,
188-
PartReviewController.updatePart
181+
PartReviewController.notifyAssignee
189182
);
190183

191-
partsRouter.post('/:partId/delete', PartReviewController.deletePart);
192-
193-
partsRouter.get('/:wbsNum', PartReviewController.getAllPartsForProject);
194-
195-
partsRouter.post('/:partId/upload-preview', upload.single('image'), PartReviewController.uploadPreview);
196-
197184
partsRouter.post(
198-
'/:partId/update',
199-
intMinZero(body('index')),
200-
nonEmptyString(body('commonName')),
201-
body('description').optional().isString(),
202-
body('reviewStatus').custom((value) => Object.values(Review_Status).includes(value)),
203-
body('tagIds').isArray(),
204-
body('assigneeIds').isArray(),
185+
'/:partId/reviewRequest/create',
186+
nonEmptyString(body('reviewerId')),
205187
validateInputs,
206-
PartReviewController.updatePart
188+
PartReviewController.createPartReviewRequest
207189
);
208190

209-
partsRouter.post('/:partId/delete', PartReviewController.deletePart);
210-
211-
partsRouter.get('/:wbsNum', PartReviewController.getAllPartsForProject);
212-
213191
partsRouter.post('/:partId/upload-preview', upload.single('image'), PartReviewController.uploadPreview);
214192

215193
partsRouter.post(

src/backend/src/services/part-review.services.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
import { isUserPartOfTeams } from '../utils/teams.utils';
4343
import { uploadFile, downloadFile } from '../utils/google-integration.utils';
4444
import ProjectsService from './projects.services';
45+
import { sendPartAssignmentPopUp, sendPartReviewRequestPopUp } from '../utils/pop-up.utils';
4546

4647
export default class PartReviewService {
4748
/**
@@ -1091,6 +1092,82 @@ export default class PartReviewService {
10911092
return partReviewRequestTransformer(softDeletedRequest);
10921093
}
10931094

1095+
/**
1096+
* Sends a notification to the reviewer of a part review request
1097+
* @param reviewerId id of the reviewer
1098+
* @param partId id of the part
1099+
* @param creator id of the creator
1100+
* @param organizationId id of the organization
1101+
*/
1102+
static async notifyReviewer(reviewerId: string, partId: string, organizationId: string) {
1103+
const part = await prisma.part.findUnique({
1104+
where: { partId },
1105+
include: {
1106+
project: {
1107+
include: {
1108+
wbsElement: true
1109+
}
1110+
},
1111+
reviewRequests: true
1112+
}
1113+
});
1114+
1115+
if (!part) {
1116+
throw new NotFoundException('Part', partId);
1117+
}
1118+
1119+
if (part.dateDeleted) {
1120+
throw new DeletedException('Part', partId);
1121+
}
1122+
1123+
if (!part.reviewRequests.some((request) => request.reviewerId === reviewerId)) {
1124+
throw new HttpException(400, 'User is not a reviewer for this part');
1125+
}
1126+
1127+
const wbsNum = `${part.project.wbsElement.carNumber}.${part.project.wbsElement.projectNumber}.0`;
1128+
const partLink = `/projects/${wbsNum}/part/${part.index}`;
1129+
1130+
await sendPartReviewRequestPopUp(partLink, part.commonName, reviewerId, organizationId);
1131+
}
1132+
1133+
/**
1134+
* Sends a notification to the assignee of a part
1135+
* @param assigneeId id of the assignee
1136+
* @param partId id of the part
1137+
* @param creator id of the creator
1138+
* @param organizationId id of the organization
1139+
*/
1140+
static async notifyAssignee(assigneeId: string, partId: string, organizationId: string) {
1141+
const part = await prisma.part.findUnique({
1142+
where: { partId },
1143+
include: {
1144+
project: {
1145+
include: {
1146+
wbsElement: true
1147+
}
1148+
},
1149+
assignees: true
1150+
}
1151+
});
1152+
1153+
if (!part) {
1154+
throw new NotFoundException('Part', partId);
1155+
}
1156+
1157+
if (part.dateDeleted) {
1158+
throw new DeletedException('Part', partId);
1159+
}
1160+
1161+
if (!part.assignees.some((assignee) => assignee.userId === assigneeId)) {
1162+
throw new HttpException(400, 'User is not an assignee for this part');
1163+
}
1164+
1165+
const wbsNum = `${part.project.wbsElement.carNumber}.${part.project.wbsElement.projectNumber}.0`;
1166+
const partLink = `/projects/${wbsNum}/part/${part.index}`;
1167+
1168+
await sendPartAssignmentPopUp(partLink, part.commonName, assigneeId, organizationId);
1169+
}
1170+
10941171
/**
10951172
* Creates a part review popup
10961173
*

src/backend/src/utils/pop-up.utils.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,3 +67,47 @@ export const sendCrRequestReviewPopUp = async (changeRequest: Change_Request, re
6767
changeRequestLink
6868
);
6969
};
70+
71+
/**
72+
* Sends a finishline pop up to a user whose review was requested on a part
73+
* @param partLink link to the part
74+
* @param partName name of the part
75+
* @param reviewer user whose review was requested
76+
* @param organizationId id of the organization of the part
77+
*/
78+
export const sendPartReviewRequestPopUp = async (
79+
partLink: string,
80+
partName: string,
81+
reviewerId: string,
82+
organizationId: string
83+
) => {
84+
await PopUpService.sendPopUpToUsers(
85+
`Your review has been requested on ${partName}`,
86+
'edit_note',
87+
[reviewerId],
88+
organizationId,
89+
partLink
90+
);
91+
};
92+
93+
/**
94+
* Sends a finishline pop up to a user who is assigned to a part
95+
* @param partLink link to the part
96+
* @param partName name of the part
97+
* @param assignee user who is assigned to the part
98+
* @param organizationId id of the organization of the part
99+
*/
100+
export const sendPartAssignmentPopUp = async (
101+
partLink: string,
102+
partName: string,
103+
assigneeId: string,
104+
organizationId: string
105+
) => {
106+
await PopUpService.sendPopUpToUsers(
107+
`You have been assigned to ${partName}`,
108+
'edit_note',
109+
[assigneeId],
110+
organizationId,
111+
partLink
112+
);
113+
};

src/frontend/src/apis/part-review.api.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,3 +342,25 @@ export const setPartReviewSampleImage = async (file: File) => {
342342
formData.append('partReviewSampleImage', file);
343343
return axios.post(apiUrls.setPartReviewSampleImage(), formData, {});
344344
};
345+
346+
/**
347+
* Sends a notification to the assignee of a part
348+
* @param partId id of the part
349+
* @param assigneeId id of the assignee
350+
*/
351+
export const sendPartAssignmentNotification = (payload: { partId: string; assigneeId: string }) => {
352+
return axios.post<{ message: string }>(apiUrls.notifyPartAssignee(), {
353+
...payload
354+
});
355+
};
356+
357+
/**
358+
* Sends a notification to the reviewer of a part
359+
* @param partId id of the part
360+
* @param reviewerId id of the reviewer
361+
*/
362+
export const sendPartReviewRequestNotification = (payload: { partId: string; reviewerId: string }) => {
363+
return axios.post<{ message: string }>(apiUrls.notifyPartReviewer(), {
364+
...payload
365+
});
366+
};

src/frontend/src/hooks/part-review.hooks.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ import {
3434
updateReviewPopup,
3535
deleteReviewPopup,
3636
uploadFile,
37+
sendPartAssignmentNotification,
38+
sendPartReviewRequestNotification,
3739
setPartReviewSampleImage,
3840
getPartReviewSampleImage,
3941
createCommonMistake,
@@ -649,3 +651,45 @@ export const usePartReviewSampleImageId = () => {
649651
return fileId;
650652
});
651653
};
654+
655+
/**
656+
* Custom React Hook to notify the assignee of a part
657+
*
658+
* @returns a success message
659+
*/
660+
export const useNotifyPartAssignee = () => {
661+
const queryClient = useQueryClient();
662+
return useMutation<{ message: string }, Error, { partId: string; assigneeId: string }>(
663+
['parts', 'notifyAssignee'],
664+
async (notification) => {
665+
const { data } = await sendPartAssignmentNotification(notification);
666+
return data;
667+
},
668+
{
669+
onSuccess: () => {
670+
queryClient.invalidateQueries(['pop-ups', 'current-user']);
671+
}
672+
}
673+
);
674+
};
675+
676+
/**
677+
* Custom React Hook to notify the reviewer of a part
678+
*
679+
* @returns a success message
680+
*/
681+
export const useNotifyPartReviewer = () => {
682+
const queryClient = useQueryClient();
683+
return useMutation<{ message: string }, Error, { partId: string; reviewerId: string }>(
684+
['parts', 'notifyReviewer'],
685+
async (notification) => {
686+
const { data } = await sendPartReviewRequestNotification(notification);
687+
return data;
688+
},
689+
{
690+
onSuccess: () => {
691+
queryClient.invalidateQueries(['pop-ups', 'current-user']);
692+
}
693+
}
694+
);
695+
};

0 commit comments

Comments
 (0)