Skip to content

Commit 4717358

Browse files
committed
Merge branch 'develop' into #1289-change-expense-type-to-autocomplete
2 parents abba3ab + 474531a commit 4717358

72 files changed

Lines changed: 1127 additions & 345 deletions

File tree

Some content is hidden

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

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ A project management web application built in Typescript, React, and Express.
88

99
All of our documentation is on [our Confluence page](https://nerdocs.atlassian.net/wiki/spaces/NER/pages/5603329/Software). Start there to learn what the project is all about.
1010

11-
All questions can be directed to `#software` in the [NER Slack](https://nu-electric-racing.slack.com) (backup contact: [`@anthonybernardi`](https://github.com/anthonybernardi)).
11+
All questions can be directed to `#software` in the [NER Slack](https://nu-electric-racing.slack.com) (backup contact: [`@RChandler234`](https://github.com/RChandler234)).
1212

1313
## Environment Setup
1414

src/backend/src/controllers/reimbursement-requests.controllers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ export default class ReimbursementRequestsController {
7272
static async reimburseUser(req: Request, res: Response, next: NextFunction) {
7373
try {
7474
const user = await getCurrentUser(res);
75-
const { amount } = req.body;
75+
const { amount, dateReceived } = req.body;
7676

77-
const reimbursement = await ReimbursementRequestService.reimburseUser(amount, user);
77+
const reimbursement = await ReimbursementRequestService.reimburseUser(amount, dateReceived, user);
7878
res.status(200).json(reimbursement);
7979
} catch (error: unknown) {
8080
next(error);

src/backend/src/controllers/teams.controllers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,4 +62,16 @@ export default class TeamsController {
6262
next(error);
6363
}
6464
}
65+
66+
static async setTeamLeads(req: Request, res: Response, next: NextFunction) {
67+
try {
68+
const { userIds } = req.body;
69+
const { teamId } = req.params;
70+
const submitter = await getCurrentUser(res);
71+
const team = await TeamsService.setTeamLeads(submitter, teamId, userIds);
72+
return res.status(200).json(team);
73+
} catch (error: unknown) {
74+
next(error);
75+
}
76+
}
6577
}

src/backend/src/prisma/seed.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
Task_Priority,
1313
Task_Status,
1414
Team,
15+
Vendor,
1516
WBS_Element_Status
1617
} from '@prisma/client';
1718
import { dbSeedAllUsers } from './seed-data/users.seed';
@@ -684,6 +685,10 @@ const performSeed: () => Promise<void> = async () => {
684685
);
685686

686687
const vendor = await ReimbursementRequestService.createVendor(thomasEmrax, 'Tesla');
688+
const vendor2 = await ReimbursementRequestService.createVendor(thomasEmrax, 'Amazon');
689+
const vendor3 = await ReimbursementRequestService.createVendor(thomasEmrax, 'Google');
690+
691+
const vendors: Vendor[] = [vendor, vendor2, vendor3];
687692

688693
const expenseType = await ReimbursementRequestService.createExpenseType(thomasEmrax, 'Equipment', 123, true);
689694

src/backend/src/routes/reimbursement-requests.routes.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ reimbursementRequestsRouter.post(
111111
reimbursementRequestsRouter.post(
112112
'/reimburse',
113113
intMinZero(body('amount')),
114+
isDate(body('dateReceived')),
114115
validateInputs,
115116
ReimbursementRequestController.reimburseUser
116117
);

src/backend/src/routes/teams.routes.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ teamsRouter.post(
1515
validateInputs,
1616
TeamsController.setTeamMembers
1717
);
18+
teamsRouter.post(
19+
'/:teamId/set-leads',
20+
body('userIds').isArray(),
21+
intMinZero(body('userIds.*')),
22+
validateInputs,
23+
TeamsController.setTeamLeads
24+
);
1825
teamsRouter.post(
1926
'/:teamId/edit-description',
2027
body('newDescription').isString(),

src/backend/src/services/reimbursement-requests.services.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ export default class ReimbursementRequestService {
130130

131131
if (!expenseType) throw new NotFoundException('Expense Type', expenseTypeId);
132132

133+
if (!expenseType.allowed) throw new HttpException(400, `The expense type ${expenseType.name} is not allowed!`);
134+
133135
const validatedReimbursementProudcts = await validateReimbursementProducts(reimbursementProducts);
134136

135137
const createdReimbursementRequest = await prisma.reimbursement_Request.create({
@@ -167,10 +169,11 @@ export default class ReimbursementRequestService {
167169
* Function to reimburse a user for their expenses.
168170
*
169171
* @param amount the amount to be reimbursed
172+
* @param dateReceived the date the amount was received
170173
* @param submitter the person performing the reimbursement
171174
* @returns the created reimbursement
172175
*/
173-
static async reimburseUser(amount: number, submitter: User): Promise<Reimbursement> {
176+
static async reimburseUser(amount: number, dateReceived: string, submitter: User): Promise<Reimbursement> {
174177
if (isGuest(submitter.role)) {
175178
throw new AccessDeniedException('Guests cannot reimburse a user for their expenses.');
176179
}
@@ -195,11 +198,16 @@ export default class ReimbursementRequestService {
195198
if (amount > totalOwed - totalReimbursed) {
196199
throw new HttpException(400, 'Reimbursement is greater than the total amount owed to the user');
197200
}
201+
202+
// make the date object but add 12 hours so that the time isn't 00:00 to avoid timezone problems
203+
const dateCreated = new Date(dateReceived.split('T')[0]);
204+
dateCreated.setTime(dateCreated.getTime() + 12 * 60 * 60 * 1000);
205+
198206
const newReimbursement = await prisma.reimbursement.create({
199207
data: {
200208
purchaserId: submitter.userId,
201209
amount,
202-
dateCreated: new Date(),
210+
dateCreated: dateReceived,
203211
userSubmittedId: submitter.userId
204212
},
205213
...reimbursementQueryArgs

src/backend/src/services/tasks.services.ts

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import teamQueryArgs from '../prisma-query-args/teams.query-args';
66
import prisma from '../prisma/prisma';
77
import taskTransformer from '../transformers/tasks.transformer';
88
import { NotFoundException, AccessDeniedException, HttpException, DeletedException } from '../utils/errors.utils';
9-
import { hasPermissionToEditTask } from '../utils/tasks.utils';
9+
import { hasPermissionToEditTask, sendSlackTaskAssignedNotificationToUsers } from '../utils/tasks.utils';
1010
import { areUsersPartOfTeams, isUserOnTeam } from '../utils/teams.utils';
1111
import { getUsers } from '../utils/users.utils';
1212
import { wbsNumOf } from '../utils/utils';
@@ -79,7 +79,11 @@ export default class TasksService {
7979
...taskQueryArgs
8080
});
8181

82-
return taskTransformer(createdTask);
82+
const newTask = taskTransformer(createdTask);
83+
84+
sendSlackTaskAssignedNotificationToUsers(newTask, assignees);
85+
86+
return newTask;
8387
}
8488

8589
/**
@@ -94,7 +98,10 @@ export default class TasksService {
9498
*/
9599
static async editTask(user: User, taskId: string, title: string, notes: string, priority: Task_Priority, deadline: Date) {
96100
const hasPermission = await hasPermissionToEditTask(user, taskId);
97-
if (!hasPermission) throw new AccessDeniedException();
101+
if (!hasPermission)
102+
throw new AccessDeniedException(
103+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
104+
);
98105

99106
const originalTask = await prisma.task.findUnique({ where: { taskId } });
100107
if (!originalTask) throw new NotFoundException('Task', taskId);
@@ -129,7 +136,7 @@ export default class TasksService {
129136
const hasPermission = await hasPermissionToEditTask(user, taskId);
130137
if (!hasPermission)
131138
throw new AccessDeniedException(
132-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
139+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
133140
);
134141

135142
const updatedTask = await prisma.task.update({ where: { taskId }, data: { status }, ...taskQueryArgs });
@@ -149,16 +156,20 @@ export default class TasksService {
149156
const originalTask = await prisma.task.findUnique({
150157
where: { taskId },
151158
include: {
152-
wbsElement: { include: { project: { ...projectQueryArgs } } }
159+
wbsElement: { include: { project: { ...projectQueryArgs } } },
160+
assignees: true
153161
}
154162
});
155163
if (!originalTask) throw new NotFoundException('Task', taskId);
156164
if (originalTask.dateDeleted) throw new DeletedException('Task', taskId);
157165

166+
const originalAssigneeIds = originalTask.assignees.map((assignee) => assignee.userId);
167+
const newAssigneeIds = assignees.filter((userId) => !originalAssigneeIds.includes(userId));
168+
158169
const hasPermission = await hasPermissionToEditTask(user, taskId);
159170
if (!hasPermission)
160171
throw new AccessDeniedException(
161-
'Only admins, app admins, task creators, project leads, project managers, or project assignees can edit a task'
172+
'Only admins, app admins, heads, task creators, project leads, project managers, or project assignees can edit a task'
162173
);
163174

164175
// this throws if any of the users aren't found
@@ -180,17 +191,21 @@ export default class TasksService {
180191
};
181192
});
182193

183-
const updatedTask = await prisma.task.update({
184-
where: { taskId },
185-
data: {
186-
assignees: {
187-
set: transformedAssigneeUsers
188-
}
189-
},
190-
...taskQueryArgs
191-
});
192-
193-
return taskTransformer(updatedTask);
194+
const updatedTask = taskTransformer(
195+
await prisma.task.update({
196+
where: { taskId },
197+
data: {
198+
assignees: {
199+
set: transformedAssigneeUsers
200+
}
201+
},
202+
...taskQueryArgs
203+
})
204+
);
205+
206+
await sendSlackTaskAssignedNotificationToUsers(updatedTask, newAssigneeIds);
207+
208+
return updatedTask;
194209
}
195210

196211
/**

src/backend/src/services/teams.services.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,4 +162,53 @@ export default class TeamsService {
162162

163163
return teamTransformer(updateTeam);
164164
}
165+
166+
/**
167+
* Update the given teamId's team's leads
168+
* @param submitter a user who's making this request
169+
* @param teamId a id of team to be updated
170+
* @param userIds a array of user Ids that replaces team's old leads
171+
* @returns an updated team
172+
* @throws if the team is not found, the submitter has no privilege, or any user from the given userIds does not exist
173+
*/
174+
static async setTeamLeads(submitter: User, teamId: string, userIds: number[]): Promise<Team> {
175+
const team = await prisma.team.findUnique({
176+
where: { teamId },
177+
...teamQueryArgs
178+
});
179+
180+
const newLeads = await getUsers(userIds);
181+
182+
if (!team) throw new NotFoundException('Team', teamId);
183+
184+
if (!isAdmin(submitter.role) && submitter.userId !== team.headId) {
185+
throw new AccessDeniedException('You must be an admin or the head to update the lead!');
186+
}
187+
188+
if (newLeads.map((lead) => lead.userId).includes(team.headId)) {
189+
throw new HttpException(400, 'A lead cannot be the head of the team!');
190+
}
191+
192+
if (team.members.map((member) => member.userId).some((memberId) => userIds.includes(memberId))) {
193+
throw new HttpException(400, 'A lead cannot be a member of the team!');
194+
}
195+
196+
const transformedLeads = newLeads.map((lead) => {
197+
return {
198+
userId: lead.userId
199+
};
200+
});
201+
202+
const updateTeam = await prisma.team.update({
203+
where: { teamId },
204+
data: {
205+
leads: {
206+
set: transformedLeads
207+
}
208+
},
209+
...teamQueryArgs
210+
});
211+
212+
return teamTransformer(updateTeam);
213+
}
165214
}

src/backend/src/services/work-packages.services.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,12 @@ export default class WorkPackagesService {
154154
throw new HttpException(400, 'A Work Package cannot have its own project as a blocker');
155155
}
156156

157+
blockedBy.forEach((dep: WbsNumber) => {
158+
if (dep.workPackageNumber === 0) {
159+
throw new HttpException(400, 'A Project cannot be a Blocker');
160+
}
161+
});
162+
157163
const wbsElem = await prisma.wBS_Element.findUnique({
158164
where: {
159165
wbsNumber: {
@@ -287,6 +293,12 @@ export default class WorkPackagesService {
287293
// verify user is allowed to edit work packages
288294
if (isGuest(user.role)) throw new AccessDeniedGuestException('edit work packages');
289295

296+
blockedBy.forEach((dep: WbsNumber) => {
297+
if (dep.workPackageNumber === 0) {
298+
throw new HttpException(400, 'A Project cannot be a Blocker');
299+
}
300+
});
301+
290302
const { userId } = user;
291303

292304
// get the original work package so we can compare things

0 commit comments

Comments
 (0)