Skip to content

Commit 0452c14

Browse files
authored
Merge pull request #4098 from Northeastern-Electric-Racing/#4048-tasks-for-work-packages
#4048 tasks for work packages
2 parents b068e67 + 61ac3dc commit 0452c14

33 files changed

Lines changed: 865 additions & 147 deletions

File tree

src/backend/src/controllers/tasks.controllers.ts

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export default class TasksController {
2929

3030
static async editTask(req: Request, res: Response, next: NextFunction) {
3131
try {
32-
const { title, notes, priority, deadline, startDate } = req.body;
32+
const { title, notes, priority, deadline, startDate, wbsNum } = req.body;
3333
const { taskId } = req.params as Record<string, string>;
3434

3535
const updateTask = await TasksService.editTask(
@@ -40,7 +40,8 @@ export default class TasksController {
4040
notes,
4141
priority,
4242
startDate ? new Date(startDate) : undefined,
43-
deadline ? new Date(deadline) : undefined
43+
deadline ? new Date(deadline) : undefined,
44+
wbsNum
4445
);
4546

4647
res.status(200).json(updateTask);
@@ -122,4 +123,14 @@ export default class TasksController {
122123
next(error);
123124
}
124125
}
126+
127+
static async getTasksByWbsNum(req: Request, res: Response, next: NextFunction) {
128+
try {
129+
const wbsNum: WbsNumber = validateWBS(req.params.wbsNum as string);
130+
const tasks = await TasksService.getTasksByWbsNum(wbsNum, req.organization);
131+
res.status(200).json(tasks);
132+
} catch (error: unknown) {
133+
next(error);
134+
}
135+
}
125136
}

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,17 @@ export default class WorkPackagesController {
6262
}
6363
}
6464

65+
// fetch all work packages for the given project wbs number
66+
static async getWorkPackagesByProject(req: Request, res: Response, next: NextFunction) {
67+
try {
68+
const projectWbsNum: WbsNumber = validateWBS(req.params.wbsNum as string);
69+
const workPackages = await WorkPackagesService.getWorkPackagesByProject(projectWbsNum, req.organization);
70+
res.status(200).json(workPackages);
71+
} catch (error: unknown) {
72+
next(error);
73+
}
74+
}
75+
6576
// Create a work package with the given details
6677
static async createWorkPackage(req: Request, res: Response, next: NextFunction) {
6778
try {

src/backend/src/prisma-query-args/projects.query-args.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { getDescriptionBulletQueryArgs } from './description-bullets.query-args.
44
import { getTeamPreviewQueryArgs } from './teams.query-args.js';
55
import { getTaskQueryArgs } from './tasks.query-args.js';
66
import { getLinkQueryArgs } from './links.query-args.js';
7-
import { getWorkPackagePreviewQueryArgs, getWorkPackageQueryArgs } from './work-packages.query-args.js';
7+
import { getWorkPackageQueryArgs, getWorkPackagePreviewQueryArgs } from './work-packages.query-args.js';
88

99
export type ProjectQueryArgs = ReturnType<typeof getProjectQueryArgs>;
1010

src/backend/src/prisma-query-args/tasks.query-args.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ export const getCalendarTaskQueryArgs = (organizationId: string) =>
2727
organizationId: true,
2828
dateDeleted: true,
2929
leadId: true,
30-
managerId: true
30+
managerId: true,
31+
name: true
3132
}
3233
},
3334
createdBy: getUserQueryArgs(organizationId),

src/backend/src/prisma/seed.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2870,7 +2870,7 @@ const performSeed: () => Promise<void> = async () => {
28702870
"of the wheel and put pedal to the metal. Accelerating down straightaways and taking corners with finesse, it's " +
28712871
'easy to forget McCauley, in his blue racing jacket and jet black helmet, is racing laps around the roof of ' +
28722872
"Columbus Parking Garage on Northeastern's Boston campus. But that's the reality of Northeastern Electric " +
2873-
'Racing, a student club that has made due and found massive success in the world of electric racing despite its ' +
2873+
'Racing, a student club that has made do and found massive success in the world of electric racing despite its ' +
28742874
"relative rookie status. McCauley, NER's chief electrical engineer, has seen the club's car, Cinnamon, go from " +
28752875
'a 5-foot drive test to hitting 60 miles per hour in competitions. "It\'s a go-kart that has 110 kilowatts of ' +
28762876
'power, 109 kilowatts of power," says McCauley, a fourth-year electrical and computer engineering student. ' +

src/backend/src/routes/tasks.routes.ts

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import {
66
isTaskPriority,
77
isTaskStatus,
88
validateInputs,
9-
isOptionalDateOnly
9+
isOptionalDateOnly,
10+
intMinZero
1011
} from '../utils/validation.utils.js';
1112
import { isDate } from '../utils/validation.utils.js';
1213

@@ -45,20 +46,27 @@ tasksRouter.post(
4546
isOptionalDateOnly(body('deadline')),
4647
isOptionalDateOnly(body('startDate')),
4748
isTaskPriority(body('priority')),
49+
intMinZero(body('wbsNum.carNumber')),
50+
intMinZero(body('wbsNum.projectNumber')),
51+
intMinZero(body('wbsNum.workPackageNumber')),
52+
validateInputs,
4853
TasksController.editTask
4954
);
5055

51-
tasksRouter.post('/:taskId/edit-status', isTaskStatus(body('status')), TasksController.editTaskStatus);
56+
tasksRouter.post('/:taskId/edit-status', isTaskStatus(body('status')), validateInputs, TasksController.editTaskStatus);
5257

5358
tasksRouter.post(
5459
'/:taskId/edit-assignees',
5560
body('assignees').isArray(),
5661
nonEmptyString(body('assignees.*')),
62+
validateInputs,
5763
TasksController.editTaskAssignees
5864
);
5965

60-
tasksRouter.post('/:taskId/delete', TasksController.deleteTask);
66+
tasksRouter.post('/:taskId/delete', validateInputs, TasksController.deleteTask);
6167

6268
tasksRouter.get('/overdue-by-team-member/:userId', TasksController.getOverdueTasksByTeamLeadership);
6369

70+
tasksRouter.get('/by-wbs/:wbsNum', TasksController.getTasksByWbsNum);
71+
6472
export default tasksRouter;

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ workPackagesRouter.post(
3030
WorkPackagesController.getManyWorkPackages
3131
);
3232
workPackagesRouter.get('/:wbsNum', WorkPackagesController.getSingleWorkPackage);
33+
34+
workPackagesRouter.get('/by-project/:wbsNum', WorkPackagesController.getWorkPackagesByProject);
35+
3336
workPackagesRouter.post(
3437
'/create',
3538
nonEmptyString(body('crId').optional()),

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

Lines changed: 113 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -73,15 +73,25 @@ export default class TasksService {
7373
wbsElement: true,
7474
workPackages: { include: { wbsElement: true } }
7575
}
76+
},
77+
workPackage: {
78+
include: {
79+
project: {
80+
include: {
81+
teams: getTeamQueryArgs(organization.organizationId)
82+
}
83+
}
84+
}
7685
}
7786
}
7887
});
7988
if (!requestedWbsElement) throw new NotFoundException('WBS Element', wbsPipe(wbsNum));
8089
if (requestedWbsElement.dateDeleted) throw new DeletedException('WBS Element', wbsPipe(wbsNum));
81-
const { project } = requestedWbsElement;
82-
if (!project) throw new HttpException(400, "This task's wbs element is not linked to a project!");
8390

84-
const { teams } = project;
91+
if (!requestedWbsElement.project && !requestedWbsElement.workPackage)
92+
throw new HttpException(400, "This task's wbs element is not linked to a project or work package!");
93+
94+
const teams = requestedWbsElement.project?.teams ?? requestedWbsElement.workPackage?.project?.teams;
8595
if (!teams || teams.length === 0)
8696
throw new HttpException(400, 'This project needs to be assigned to a team to create a task!');
8797

@@ -136,7 +146,9 @@ export default class TasksService {
136146
* @param title the new title for the task
137147
* @param notes the new notes for the task
138148
* @param priority the new priority for the task
149+
* @param startDate the new start date for the task
139150
* @param deadline the new deadline for the task
151+
* @param wbsNum the new wbs element for the task
140152
* @returns the sucessfully edited task
141153
*/
142154
static async editTask(
@@ -147,24 +159,56 @@ export default class TasksService {
147159
notes: string,
148160
priority: Task_Priority,
149161
startDate?: Date,
150-
deadline?: Date
162+
deadline?: Date,
163+
wbsNum?: WbsNumber
151164
) {
152165
const hasPermission = await userHasPermission(user.userId, organizationId, notGuest);
153166
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');
154167

155168
const originalTask = await prisma.task.findUnique({ where: { taskId }, include: { wbsElement: true } });
156169

170+
// error if there's a problem with the task
157171
if (!originalTask) throw new NotFoundException('Task', taskId);
158172
if (originalTask.wbsElement.organizationId !== organizationId) throw new InvalidOrganizationException('Task');
159173
if (originalTask.dateDeleted) throw new DeletedException('Task', taskId);
160174

161175
if (!isUnderWordCount(title, 15)) throw new HttpException(400, 'Title must be less than 15 words');
162-
163176
if (!isUnderWordCount(notes, 250)) throw new HttpException(400, 'Notes must be less than 250 words');
164177

178+
// if wbsNum passed, error if there's a problem with the wbs element
179+
if (wbsNum) {
180+
const newWbsElement = await prisma.wBS_Element.findUnique({
181+
where: {
182+
wbsNumber: {
183+
...wbsNum,
184+
organizationId
185+
}
186+
}
187+
});
188+
if (!newWbsElement) throw new NotFoundException('WBS Element', wbsPipe(wbsNum));
189+
if (newWbsElement.dateDeleted) throw new DeletedException('WBS Element', wbsPipe(wbsNum));
190+
}
191+
165192
const updatedTask = await prisma.task.update({
166193
where: { taskId },
167-
data: { title, notes, priority, startDate, deadline },
194+
data: {
195+
title,
196+
notes,
197+
priority,
198+
startDate,
199+
deadline,
200+
// if wbsNum passed, update prisma relation to connect task with wbs element
201+
...(wbsNum && {
202+
wbsElement: {
203+
connect: {
204+
wbsNumber: {
205+
...wbsNum,
206+
organizationId
207+
}
208+
}
209+
}
210+
})
211+
},
168212
...getTaskQueryArgs(originalTask.wbsElement.organizationId)
169213
});
170214
return taskTransformer(updatedTask);
@@ -173,13 +217,16 @@ export default class TasksService {
173217
/**
174218
* Edits the status of a task in the database
175219
* @param user the user editing the task
176-
* @param organizationId the organizqtion Id
220+
* @param organizationId the organization Id
177221
* @param taskId the id of the task
178222
* @param status the new status
179223
* @returns the updated task
180224
* @throws if the task does not exist, the task is already deleted, or if the user does not have permissions
181225
*/
182226
static async editTaskStatus(user: User, organizationId: string, taskId: string, status: Task_Status) {
227+
const hasPermission = await userHasPermission(user.userId, organizationId, notGuest);
228+
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');
229+
183230
// Get the original task and check if it exists
184231
const originalTask = await prisma.task.findUnique({ where: { taskId }, include: { assignees: true, wbsElement: true } });
185232
if (!originalTask) throw new NotFoundException('Task', taskId);
@@ -190,9 +237,6 @@ export default class TasksService {
190237
throw new HttpException(400, 'A task in progress must have a deadline and assignees!');
191238
}
192239

193-
const hasPermission = await userHasPermission(user.userId, organizationId, notGuest);
194-
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');
195-
196240
const updatedTask = await prisma.task.update({
197241
where: { taskId },
198242
data: { status },
@@ -216,6 +260,9 @@ export default class TasksService {
216260
assignees: string[],
217261
organization: Organization
218262
): Promise<Task> {
263+
const hasPermission = await userHasPermission(user.userId, organization.organizationId, notGuest);
264+
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');
265+
219266
// Get the original task and check if it exists
220267
const originalTask = await prisma.task.findUnique({
221268
where: { taskId },
@@ -230,9 +277,6 @@ export default class TasksService {
230277
const originalAssigneeIds = originalTask.assignees.map((assignee) => assignee.userId);
231278
const newAssigneeIds = assignees.filter((userId) => !originalAssigneeIds.includes(userId));
232279

233-
const hasPermission = await userHasPermission(user.userId, organization.organizationId, notGuest);
234-
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');
235-
236280
// this throws if any of the users aren't found
237281
const assigneeUsers = await getUsers(assignees);
238282

@@ -391,4 +435,60 @@ export default class TasksService {
391435

392436
return tasks.map(taskCardPreviewTransformer);
393437
}
438+
439+
/**
440+
* Gets all tasks associated with a wbs element
441+
* If the wbs number is a project (workPackageNumber === 0), returns the project's
442+
* own tasks merged with all of its work packages' tasks
443+
* If the wbs number is a work package, returns just that WP's tasks
444+
* @param wbsNum the wbs number to fetch tasks for
445+
* @param organization the organization that the user is currently in
446+
* @returns array of tasks
447+
*/
448+
static async getTasksByWbsNum(wbsNum: WbsNumber, organization: Organization): Promise<Task[]> {
449+
const wbsElement = await prisma.wBS_Element.findUnique({
450+
where: {
451+
wbsNumber: {
452+
...wbsNum,
453+
organizationId: organization.organizationId
454+
}
455+
}
456+
});
457+
458+
if (!wbsElement) throw new NotFoundException('WBS Element', wbsPipe(wbsNum));
459+
if (wbsElement.dateDeleted) throw new DeletedException('WBS Element', wbsPipe(wbsNum));
460+
461+
// project case, so return project's own tasks and all its wp's tasks
462+
if (wbsNum.workPackageNumber === 0) {
463+
const project = await prisma.project.findUnique({
464+
where: { wbsElementId: wbsElement.wbsElementId },
465+
include: { workPackages: { include: { wbsElement: true } } }
466+
});
467+
468+
if (!project) throw new NotFoundException('Project', wbsPipe(wbsNum));
469+
470+
const wpWbsElementIds = project.workPackages.map((wp) => wp.wbsElementId);
471+
472+
const tasks = await prisma.task.findMany({
473+
where: {
474+
dateDeleted: null,
475+
wbsElementId: { in: [wbsElement.wbsElementId, ...wpWbsElementIds] }
476+
},
477+
...getTaskQueryArgs(organization.organizationId)
478+
});
479+
480+
return tasks.map(taskTransformer);
481+
}
482+
483+
// work package case, so return just that wp's tasks
484+
const tasks = await prisma.task.findMany({
485+
where: {
486+
dateDeleted: null,
487+
wbsElementId: wbsElement.wbsElementId
488+
},
489+
...getTaskQueryArgs(organization.organizationId)
490+
});
491+
492+
return tasks.map(taskTransformer);
493+
}
394494
}

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,39 @@ export default class WorkPackagesService {
183183
return workPackages.map(workPackageTransformer);
184184
}
185185

186+
/**
187+
* Retrieve all work packages for a given project
188+
* @param projectWbsNum the wbs number of the project
189+
* @param organization the organization that the user is currently in
190+
* @returns the work packages for the given project
191+
* @throws if the project does not exist or is deleted
192+
*/
193+
static async getWorkPackagesByProject(projectWbsNum: WbsNumber, organization: Organization): Promise<WorkPackage[]> {
194+
const project = await prisma.project.findFirst({
195+
where: {
196+
wbsElement: {
197+
carNumber: projectWbsNum.carNumber,
198+
projectNumber: projectWbsNum.projectNumber,
199+
workPackageNumber: 0,
200+
organizationId: organization.organizationId,
201+
dateDeleted: null
202+
}
203+
}
204+
});
205+
206+
if (!project) throw new NotFoundException('Project', wbsPipe(projectWbsNum));
207+
208+
const workPackages = await prisma.work_Package.findMany({
209+
where: {
210+
projectId: project.projectId,
211+
wbsElement: { dateDeleted: null }
212+
},
213+
...getWorkPackageQueryArgs(organization.organizationId)
214+
});
215+
216+
return workPackages.map(workPackageTransformer);
217+
}
218+
186219
/**
187220
* Creates a Work_Package in the database
188221
* @param user the user creating the work package

src/backend/src/transformers/tasks.transformer.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,12 @@ import { convertTaskPriority, convertTaskStatus } from '../utils/tasks.utils.js'
55
import { userTransformer } from './user.transformer.js';
66
import { CalendarTaskQueryArgs, TaskQueryArgs, TaskPreviewQueryArgs } from '../prisma-query-args/tasks.query-args.js';
77

8-
const taskTransformer = (task: Prisma.TaskGetPayload<TaskQueryArgs>): Task => {
8+
export const taskTransformer = (task: Prisma.TaskGetPayload<TaskQueryArgs>): Task => {
99
const wbsNum = wbsNumOf(task.wbsElement);
1010
return {
1111
taskId: task.taskId,
1212
wbsNum,
13+
wbsName: task.wbsElement.name,
1314
title: task.title,
1415
notes: task.notes,
1516
deadline: task.deadline ?? undefined,
@@ -45,6 +46,7 @@ export const calendarTaskTransformer = (task: Prisma.TaskGetPayload<CalendarTask
4546
return {
4647
taskId: task.taskId,
4748
wbsNum,
49+
wbsName: task.wbsElement.name,
4850
title: task.title,
4951
notes: task.notes,
5052
deadline: task.deadline ?? undefined,

0 commit comments

Comments
 (0)