Skip to content
Open
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
3ff416c
#4048 wp query args and project transformer edits to start fetching w…
getheobald Mar 23, 2026
a230f8e
#4048 finish backend transformer and query arg updates
getheobald Mar 23, 2026
48f6399
#4048 task card, column, and taskformmodal updates - still getting wb…
getheobald Mar 24, 2026
300fb60
#4048 fixed create task wbs check, chip now showing, updated task mod…
getheobald Mar 25, 2026
eec2ab2
Merge remote-tracking branch 'origin/develop' into #4048-tasks-for-wo…
getheobald Mar 25, 2026
1c111ac
#4048 revert overview and preview task include
getheobald Mar 25, 2026
35ea413
#4048 added editTaskWbsElement across stack to handle edits and now i…
getheobald Mar 26, 2026
99f3d44
#4048 wp tasks page with one-line filter, pre-refactor
getheobald Mar 26, 2026
b8e54af
#4048 refactor task frontend to accept piecemeal props instead of pro…
getheobald Mar 26, 2026
524b3a0
#4048 revert filter change and call TLC directly from WPVC
getheobald Mar 26, 2026
5047191
#4048 conditionally render wp dropdown, truncate chip, omit chip on w…
getheobald Mar 26, 2026
5cc4512
#4048 remove dead teams code from task frontend
getheobald Mar 26, 2026
5ab8d0f
#4048 clickable chip
getheobald Mar 26, 2026
a324ab9
#4048 extraneous imports
getheobald Mar 26, 2026
b4832e7
#4048 unit tests
getheobald Mar 26, 2026
359e27d
#4048 lint
getheobald Mar 27, 2026
4f6f675
#4048 tsc check and prettier
getheobald Mar 27, 2026
723e5f1
#4048 freaking commas
getheobald Mar 27, 2026
f4232a0
#4048 omg tsc check everything needs a wbsName
getheobald Mar 27, 2026
094ef6b
#4048 remove wp-task association in types, transformers, and query args
getheobald Mar 28, 2026
af1cf4d
#4048 full getTasksByWbsElement endpoint
getheobald Mar 28, 2026
4ccc6ea
#4048 revert tasklist props
getheobald Mar 28, 2026
9a64299
#4048 changed tasklistcontent state to flat array but sadly need to r…
getheobald Mar 28, 2026
a76d9bb
4048 fixed tasklistcontent to use hook but maintain custom ordering
getheobald Mar 28, 2026
1c1de38
#4048 remove some props and imports
getheobald Mar 30, 2026
a69c2a6
#4048 get wps by project endpoint
getheobald Apr 6, 2026
06c02aa
#4048 correctly wired frontend finally hopefully, minus edit endpoint…
getheobald Apr 6, 2026
f4e60bd
#4048 remove editTaskWbsElement everywhere
getheobald Apr 6, 2026
7817252
#4048 task and wp tests
getheobald Apr 6, 2026
a1dda84
#4048 played prop whackamole until it worked
getheobald Apr 7, 2026
ad83cc4
#4048 select wp from gantt chart task add
getheobald Apr 7, 2026
c61dd31
#4048 revert stub changes since I removed tasks from wp type
getheobald Apr 7, 2026
0e47e7c
Merge remote-tracking branch 'origin/develop' into #4048-tasks-for-wo…
getheobald Apr 7, 2026
0e8cba0
#4048 use wbsNum everywhere instead of wbsElementId except it still d…
getheobald Apr 8, 2026
7435292
#4048 sorry this was yesterday I forgot what these changes are but go…
getheobald Apr 9, 2026
241f7e2
#4048 omfg it was mui causing the snapback but I fixed it!
getheobald Apr 10, 2026
21950c6
#4048 colors on wp chip
getheobald Apr 10, 2026
83696ee
Merge remote-tracking branch 'origin/develop' into #4048-tasks-for-wo…
getheobald Apr 10, 2026
0a4501b
lint, prettier, merge tests
getheobald Apr 10, 2026
93315f2
#4048 lint prettier tsc
getheobald Apr 10, 2026
031aefe
#4048 tsc again bc there are a million files to remove props from
getheobald Apr 10, 2026
1b29022
#4048 proper route validation formatting
getheobald Apr 11, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/backend/src/controllers/tasks.controllers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,19 @@ export default class TasksController {
}
}

static async editTaskWbsElement(req: Request, res: Response, next: NextFunction) {
Comment thread
wavehassman marked this conversation as resolved.
Outdated
try {
const { wbsElementId } = req.body;
const { taskId } = req.params as Record<string, string>;

const updatedTask = await TasksService.editTaskWbsElement(req.currentUser, taskId, wbsElementId, req.organization);

res.status(200).json(updatedTask);
} catch (error: unknown) {
next(error);
}
}

static async deleteTask(req: Request, res: Response, next: NextFunction) {
try {
const { taskId } = req.params as Record<string, string>;
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/prisma-query-args/projects.query-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getDescriptionBulletQueryArgs } from './description-bullets.query-args.
import { getTeamPreviewQueryArgs } from './teams.query-args.js';
import { getTaskQueryArgs } from './tasks.query-args.js';
import { getLinkQueryArgs } from './links.query-args.js';
import { getWorkPackagePreviewQueryArgs, getWorkPackageQueryArgs } from './work-packages.query-args.js';
import { getWorkPackageQueryArgs, getWorkPackagePreviewQueryArgs } from './work-packages.query-args.js';

export type ProjectQueryArgs = ReturnType<typeof getProjectQueryArgs>;

Expand Down
3 changes: 2 additions & 1 deletion src/backend/src/prisma-query-args/tasks.query-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export const getCalendarTaskQueryArgs = (organizationId: string) =>
organizationId: true,
dateDeleted: true,
leadId: true,
managerId: true
managerId: true,
name: true
}
},
createdBy: getUserQueryArgs(organizationId),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { getUserPreviewQueryArgs, getUserQueryArgs } from './user.query-args.js'
import { getDescriptionBulletQueryArgs } from './description-bullets.query-args.js';
import { getLinkQueryArgs } from './links.query-args.js';
import { getEventQueryArgs } from './event.query-args.js';
import { getTaskQueryArgs } from './tasks.query-args.js';

export type WorkPackageQueryArgs = ReturnType<typeof getWorkPackageQueryArgs>;
export type WorkPackagePreviewQueryArgs = ReturnType<typeof getWorkPackagePreviewQueryArgs>;
Expand Down Expand Up @@ -30,7 +31,8 @@ export const getWorkPackageQueryArgs = (organizationId: string) =>
orderBy: { dateImplemented: 'asc' }
},
blocking: { where: { wbsElement: { dateDeleted: null } }, include: { wbsElement: true } },
descriptionBullets: { where: { dateDeleted: null }, ...getDescriptionBulletQueryArgs(organizationId) }
descriptionBullets: { where: { dateDeleted: null }, ...getDescriptionBulletQueryArgs(organizationId) },
tasks: { where: { dateDeleted: null }, ...getTaskQueryArgs(organizationId) }
Comment thread
wavehassman marked this conversation as resolved.
Outdated
}
},
blockedBy: { where: { dateDeleted: null } },
Expand Down
2 changes: 1 addition & 1 deletion src/backend/src/prisma/seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1677,7 +1677,7 @@ const performSeed: () => Promise<void> = async () => {
"of the wheel and put pedal to the metal. Accelerating down straightaways and taking corners with finesse, it's " +
'easy to forget McCauley, in his blue racing jacket and jet black helmet, is racing laps around the roof of ' +
"Columbus Parking Garage on Northeastern's Boston campus. But that's the reality of Northeastern Electric " +
'Racing, a student club that has made due and found massive success in the world of electric racing despite its ' +
'Racing, a student club that has made do and found massive success in the world of electric racing despite its ' +
Comment thread
wavehassman marked this conversation as resolved.
"relative rookie status. McCauley, NER's chief electrical engineer, has seen the club's car, Cinnamon, go from " +
'a 5-foot drive test to hitting 60 miles per hour in competitions. "It\'s a go-kart that has 110 kilowatts of ' +
'power, 109 kilowatts of power," says McCauley, a fourth-year electrical and computer engineering student. ' +
Expand Down
12 changes: 10 additions & 2 deletions src/backend/src/routes/tasks.routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,24 @@ tasksRouter.post(
TasksController.editTask
);

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

tasksRouter.post(
'/:taskId/edit-assignees',
body('assignees').isArray(),
nonEmptyString(body('assignees.*')),
validateInputs,
TasksController.editTaskAssignees
);

tasksRouter.post('/:taskId/delete', TasksController.deleteTask);
tasksRouter.post(
'/:taskId/edit-wbs-element',
nonEmptyString(body('wbsElementId')),
validateInputs,
TasksController.editTaskWbsElement
);

tasksRouter.post('/:taskId/delete', validateInputs, TasksController.deleteTask);

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

Expand Down
77 changes: 67 additions & 10 deletions src/backend/src/services/tasks.services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,15 +73,25 @@ export default class TasksService {
wbsElement: true,
workPackages: { include: { wbsElement: true } }
}
},
workPackage: {
include: {
project: {
Comment thread
wavehassman marked this conversation as resolved.
include: {
teams: getTeamQueryArgs(organization.organizationId)
}
}
}
}
}
});
if (!requestedWbsElement) throw new NotFoundException('WBS Element', wbsPipe(wbsNum));
if (requestedWbsElement.dateDeleted) throw new DeletedException('WBS Element', wbsPipe(wbsNum));
const { project } = requestedWbsElement;
if (!project) throw new HttpException(400, "This task's wbs element is not linked to a project!");

const { teams } = project;
if (!requestedWbsElement.project && !requestedWbsElement.workPackage)
throw new HttpException(400, "This task's wbs element is not linked to a project or work package!");

const teams = requestedWbsElement.project?.teams ?? requestedWbsElement.workPackage?.project?.teams;
if (!teams || teams.length === 0)
throw new HttpException(400, 'This project needs to be assigned to a team to create a task!');

Expand Down Expand Up @@ -173,13 +183,16 @@ export default class TasksService {
/**
* Edits the status of a task in the database
* @param user the user editing the task
* @param organizationId the organizqtion Id
* @param organizationId the organization Id
* @param taskId the id of the task
* @param status the new status
* @returns the updated task
* @throws if the task does not exist, the task is already deleted, or if the user does not have permissions
*/
static async editTaskStatus(user: User, organizationId: string, taskId: string, status: Task_Status) {
const hasPermission = await userHasPermission(user.userId, organizationId, notGuest);
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');

// Get the original task and check if it exists
const originalTask = await prisma.task.findUnique({ where: { taskId }, include: { assignees: true, wbsElement: true } });
if (!originalTask) throw new NotFoundException('Task', taskId);
Expand All @@ -190,9 +203,6 @@ export default class TasksService {
throw new HttpException(400, 'A task in progress must have a deadline and assignees!');
}

const hasPermission = await userHasPermission(user.userId, organizationId, notGuest);
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');

const updatedTask = await prisma.task.update({
where: { taskId },
data: { status },
Expand All @@ -216,6 +226,9 @@ export default class TasksService {
assignees: string[],
organization: Organization
): Promise<Task> {
const hasPermission = await userHasPermission(user.userId, organization.organizationId, notGuest);
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');

// Get the original task and check if it exists
const originalTask = await prisma.task.findUnique({
where: { taskId },
Expand All @@ -230,9 +243,6 @@ export default class TasksService {
const originalAssigneeIds = originalTask.assignees.map((assignee) => assignee.userId);
const newAssigneeIds = assignees.filter((userId) => !originalAssigneeIds.includes(userId));

const hasPermission = await userHasPermission(user.userId, organization.organizationId, notGuest);
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');

// this throws if any of the users aren't found
const assigneeUsers = await getUsers(assignees);

Expand Down Expand Up @@ -261,6 +271,53 @@ export default class TasksService {
return updatedTask;
}

/**
* Edits the wbs element of a task in the database
* @param user the user editing the task
* @param taskId the id of the task
* @param wbsElementId the id of the new wbs element
* @param organization the organization that the user is currently in
* @returns the updated task
* @throws if the task does not exist, the task is already deleted, the wbs element doesn't exist, or if the user does not have permissions
*/
static async editTaskWbsElement(
Comment thread
getheobald marked this conversation as resolved.
Outdated
user: User,
taskId: string,
wbsElementId: string,
organization: Organization
): Promise<Task> {
const hasPermission = await userHasPermission(user.userId, organization.organizationId, notGuest);
if (!hasPermission) throw new AccessDeniedException('Guests cannot edit tasks');

// get the original task and check if it exists
const originalTask = await prisma.task.findUnique({
where: { taskId },
include: { wbsElement: true }
});

// throw error if there are issues with the task
if (!originalTask) throw new NotFoundException('Task', taskId);
if (originalTask.dateDeleted) throw new DeletedException('Task', taskId);
if (originalTask.wbsElement.organizationId !== organization.organizationId)
throw new InvalidOrganizationException('Task');

const newWbsElement = await prisma.wBS_Element.findUnique({
where: { wbsElementId }
});

// throw error if there are issues with the wbs element
if (!newWbsElement) throw new NotFoundException('WBS Element', wbsElementId);
if (newWbsElement.dateDeleted) throw new DeletedException('WBS Element', wbsElementId);

const updatedTask = await prisma.task.update({
where: { taskId },
data: { wbsElement: { connect: { wbsElementId } } },
...getTaskQueryArgs(organization.organizationId)
});

return taskTransformer(updatedTask);
}

/**
* Delete task in the database
* @param taskId the id number of the given task
Expand Down
10 changes: 8 additions & 2 deletions src/backend/src/transformers/projects.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,10 @@ const projectTransformer = (project: Prisma.ProjectGetPayload<ProjectQueryArgs>)
startDate: calculateProjectStartDate(project.workPackages),
endDate: calculateProjectEndDate(project.workPackages),
descriptionBullets: wbsElement.descriptionBullets.map(descBulletConverter),
tasks: wbsElement.tasks.map(taskTransformer),
tasks: [
...wbsElement.tasks.map(taskTransformer), // this project's tasks
...project.workPackages.flatMap((wp) => wp.wbsElement.tasks.map(taskTransformer)) // all of this project's work packages' tasks, flattened to one array
],
workPackages: project.workPackages.map(workPackageTransformer),
abbreviation: project.abbreviation ?? undefined
};
Expand Down Expand Up @@ -91,7 +94,10 @@ export const projectGanttTransformer = (project: Prisma.ProjectGetPayload<Projec
})),
duration: calculateDuration(project.workPackages),
startDate: calculateProjectStartDate(project.workPackages),
tasks: project.wbsElement.tasks.map(taskTransformer),
tasks: [
...wbsElement.tasks.map(taskTransformer), // this project's tasks
...project.workPackages.flatMap((wp) => wp.wbsElement.tasks.map(taskTransformer)) // all of this project's work packages' tasks, flattened to one array
],
workPackages: project.workPackages.map(workPackageTransformer),
abbreviation: project.abbreviation ?? undefined
};
Expand Down
4 changes: 3 additions & 1 deletion src/backend/src/transformers/tasks.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,12 @@ import { convertTaskPriority, convertTaskStatus } from '../utils/tasks.utils.js'
import { userTransformer } from './user.transformer.js';
import { CalendarTaskQueryArgs, TaskQueryArgs, TaskPreviewQueryArgs } from '../prisma-query-args/tasks.query-args.js';

const taskTransformer = (task: Prisma.TaskGetPayload<TaskQueryArgs>): Task => {
export const taskTransformer = (task: Prisma.TaskGetPayload<TaskQueryArgs>): Task => {
const wbsNum = wbsNumOf(task.wbsElement);
return {
taskId: task.taskId,
wbsNum,
wbsName: task.wbsElement.name,
title: task.title,
notes: task.notes,
deadline: task.deadline ?? undefined,
Expand Down Expand Up @@ -45,6 +46,7 @@ export const calendarTaskTransformer = (task: Prisma.TaskGetPayload<CalendarTask
return {
taskId: task.taskId,
wbsNum,
wbsName: task.wbsElement.name,
title: task.title,
notes: task.notes,
deadline: task.deadline ?? undefined,
Expand Down
2 changes: 2 additions & 0 deletions src/backend/src/transformers/work-packages.transformer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { userTransformer } from './user.transformer.js';
import { WorkPackageQueryArgs, WorkPackagePreviewQueryArgs } from '../prisma-query-args/work-packages.query-args.js';
import { teamTypeTransformer } from './team-types.transformer.js';
import { eventPreviewTransformer } from './calendar.transformer.js';
import { taskTransformer } from './tasks.transformer.js';

const workPackageTransformer = (wpInput: Prisma.Work_PackageGetPayload<WorkPackageQueryArgs>): WorkPackage => {
const wbsNum = wbsNumOf(wpInput.wbsElement);
Expand Down Expand Up @@ -42,6 +43,7 @@ const workPackageTransformer = (wpInput: Prisma.Work_PackageGetPayload<WorkPacka
events: wpInput.events.map((event) =>
eventPreviewTransformer(event, `${wpInput.project.wbsElement.name} - ${wpInput.wbsElement.name}`)
),
tasks: wpInput.wbsElement.tasks.map(taskTransformer),
deleted: wpInput.wbsElement.dateDeleted !== null
};
};
Expand Down
1 change: 1 addition & 0 deletions src/backend/src/utils/validation.utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export const materialValidators = [
body('linkUrl').optional().isString(),
body('notes').isString().optional()
];

export const validateInputs = (req: Request, res: Response, next: Function): void => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
Expand Down
Loading
Loading