Skip to content

Commit a364099

Browse files
authored
Merge pull request #3988 from Northeastern-Electric-Racing/calendar-tasks
tasks on calendar
2 parents 4cc9863 + 71b2264 commit a364099

20 files changed

Lines changed: 1290 additions & 126 deletions

File tree

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ export default class TasksController {
9292
}
9393
}
9494

95+
static async getFilteredTasks(req: Request, res: Response, next: NextFunction) {
96+
try {
97+
const { memberIds, teamIds, startPeriod, endPeriod } = req.body;
98+
99+
const tasks = await TasksService.getFilteredTasks(
100+
{
101+
memberIds,
102+
teamIds,
103+
startPeriod: new Date(startPeriod),
104+
endPeriod: new Date(endPeriod)
105+
},
106+
req.organization
107+
);
108+
109+
res.status(200).json(tasks);
110+
} catch (error: unknown) {
111+
next(error);
112+
}
113+
}
114+
95115
static async getOverdueTasksByTeamLeadership(req: Request, res: Response, next: NextFunction) {
96116
try {
97117
const { userId } = req.params as Record<string, string>;

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { getUserQueryArgs } from './user.query-args.js';
33

44
export type TaskQueryArgs = ReturnType<typeof getTaskQueryArgs>;
55
export type TaskPreviewQueryArgs = ReturnType<typeof getTaskPreviewQueryArgs>;
6+
export type CalendarTaskQueryArgs = ReturnType<typeof getCalendarTaskQueryArgs>;
67

78
export const getTaskQueryArgs = (organizationId: string) =>
89
Prisma.validator<Prisma.TaskDefaultArgs>()({
@@ -14,6 +15,27 @@ export const getTaskQueryArgs = (organizationId: string) =>
1415
}
1516
});
1617

18+
export const getCalendarTaskQueryArgs = (organizationId: string) =>
19+
Prisma.validator<Prisma.TaskDefaultArgs>()({
20+
include: {
21+
wbsElement: {
22+
select: {
23+
wbsElementId: true,
24+
carNumber: true,
25+
projectNumber: true,
26+
workPackageNumber: true,
27+
organizationId: true,
28+
dateDeleted: true,
29+
leadId: true,
30+
managerId: true
31+
}
32+
},
33+
createdBy: getUserQueryArgs(organizationId),
34+
deletedBy: getUserQueryArgs(organizationId),
35+
assignees: getUserQueryArgs(organizationId)
36+
}
37+
});
38+
1739
export const getTaskPreviewQueryArgs = (organizationId: string) =>
1840
Prisma.validator<Prisma.TaskDefaultArgs>()({
1941
include: {

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,22 @@ import express from 'express';
22
import { body } from 'express-validator';
33
import TasksController from '../controllers/tasks.controllers.js';
44
import { nonEmptyString, isTaskPriority, isTaskStatus, validateInputs, isOptionalDate } from '../utils/validation.utils.js';
5+
import { isDate } from '../utils/validation.utils.js';
56

67
const tasksRouter = express.Router();
78

9+
tasksRouter.post(
10+
'/filter',
11+
isDate(body('startPeriod')),
12+
isDate(body('endPeriod')),
13+
body('memberIds').optional().isArray(),
14+
body('memberIds.*').optional().isString(),
15+
body('teamIds').optional().isArray(),
16+
body('teamIds.*').optional().isString(),
17+
validateInputs,
18+
TasksController.getFilteredTasks
19+
);
20+
821
tasksRouter.post(
922
'/:wbsNum',
1023
nonEmptyString(body('title')),

src/backend/src/services/projects.services.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export default class ProjectsService {
6666
static async getAllProjects(organization: Organization): Promise<ProjectPreview[]> {
6767
const projects = await prisma.project.findMany({
6868
where: { wbsElement: { dateDeleted: null, organizationId: organization.organizationId } },
69+
orderBy: { wbsElement: { dateCreated: 'desc' } },
6970
...getProjectPreviewQueryArgs(organization.organizationId)
7071
});
7172

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

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
import { Task_Priority, Task_Status, Organization } from '@prisma/client';
2-
import { isAdmin, isUnderWordCount, notGuest, Task, TaskCardPreview, WbsNumber, wbsPipe, User } from 'shared';
2+
import {
3+
CalendarTask,
4+
FilterTaskArgs,
5+
isAdmin,
6+
isUnderWordCount,
7+
notGuest,
8+
Task,
9+
TaskCardPreview,
10+
WbsNumber,
11+
wbsPipe,
12+
User
13+
} from 'shared';
314
import prisma from '../prisma/prisma.js';
4-
import taskTransformer, { taskCardPreviewTransformer } from '../transformers/tasks.transformer.js';
15+
import taskTransformer, { calendarTaskTransformer, taskCardPreviewTransformer } from '../transformers/tasks.transformer.js';
516
import {
617
NotFoundException,
718
AccessDeniedException,
@@ -13,7 +24,11 @@ import { sendSlackTaskAssignedNotificationToUsers } from '../utils/tasks.utils.j
1324
import { getUsers, userHasPermission } from '../utils/users.utils.js';
1425
import { wbsNumOf } from '../utils/utils.js';
1526
import { getTeamQueryArgs } from '../prisma-query-args/teams.query-args.js';
16-
import { getTaskPreviewQueryArgs, getTaskQueryArgs } from '../prisma-query-args/tasks.query-args.js';
27+
import {
28+
getCalendarTaskQueryArgs,
29+
getTaskPreviewQueryArgs,
30+
getTaskQueryArgs
31+
} from '../prisma-query-args/tasks.query-args.js';
1732
import { getProjectQueryArgs } from '../prisma-query-args/projects.query-args.js';
1833

1934
export default class TasksService {
@@ -281,6 +296,66 @@ export default class TasksService {
281296
return deletedTask.taskId;
282297
}
283298

299+
static async getFilteredTasks(filters: FilterTaskArgs, organization: Organization): Promise<CalendarTask[]> {
300+
const { memberIds, teamIds, startPeriod, endPeriod } = filters;
301+
302+
// Validate memberIds if provided
303+
if (memberIds && memberIds.length > 0) {
304+
const users = await prisma.user.findMany({
305+
where: { userId: { in: memberIds }, organizations: { some: { organizationId: organization.organizationId } } }
306+
});
307+
if (users.length !== memberIds.length) {
308+
throw new NotFoundException('User', 'one or more member IDs');
309+
}
310+
}
311+
312+
// Validate teamIds if provided
313+
if (teamIds && teamIds.length > 0) {
314+
const teams = await prisma.team.findMany({
315+
where: {
316+
teamId: { in: teamIds },
317+
organizationId: organization.organizationId
318+
}
319+
});
320+
if (teams.length !== teamIds.length) {
321+
throw new NotFoundException('Team', 'one or more team IDs');
322+
}
323+
}
324+
325+
const orFilters: any[] = [];
326+
if (memberIds && memberIds.length > 0) {
327+
orFilters.push({ assignees: { some: { userId: { in: memberIds } } } });
328+
orFilters.push({ createdByUserId: { in: memberIds } });
329+
}
330+
if (teamIds && teamIds.length > 0) {
331+
orFilters.push({
332+
wbsElement: {
333+
project: {
334+
teams: { some: { teamId: { in: teamIds } } }
335+
}
336+
}
337+
});
338+
}
339+
340+
const tasks = await prisma.task.findMany({
341+
where: {
342+
dateDeleted: null,
343+
deadline: {
344+
gte: startPeriod,
345+
lte: endPeriod
346+
},
347+
wbsElement: {
348+
organizationId: organization.organizationId,
349+
dateDeleted: null
350+
},
351+
...(orFilters.length > 0 ? { OR: orFilters } : {})
352+
},
353+
...getCalendarTaskQueryArgs(organization.organizationId)
354+
});
355+
356+
return tasks.map(calendarTaskTransformer);
357+
}
358+
284359
static async getOverdueTasksByTeamLeadership(userId: string, organization: Organization): Promise<TaskCardPreview[]> {
285360
const teams = await prisma.team.findMany({
286361
where: {

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

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { Prisma } from '@prisma/client';
2-
import { Task, TaskCardPreview } from 'shared';
2+
import { CalendarTask, Task, TaskCardPreview } from 'shared';
33
import { wbsNumOf } from '../utils/utils.js';
44
import { convertTaskPriority, convertTaskStatus } from '../utils/tasks.utils.js';
55
import { userTransformer } from './user.transformer.js';
6-
import { TaskQueryArgs, TaskPreviewQueryArgs } from '../prisma-query-args/tasks.query-args.js';
6+
import { CalendarTaskQueryArgs, TaskQueryArgs, TaskPreviewQueryArgs } from '../prisma-query-args/tasks.query-args.js';
77

88
const taskTransformer = (task: Prisma.TaskGetPayload<TaskQueryArgs>): Task => {
99
const wbsNum = wbsNumOf(task.wbsElement);
@@ -40,4 +40,25 @@ export const taskCardPreviewTransformer = (task: Prisma.TaskGetPayload<TaskPrevi
4040
};
4141
};
4242

43+
export const calendarTaskTransformer = (task: Prisma.TaskGetPayload<CalendarTaskQueryArgs>): CalendarTask => {
44+
const wbsNum = wbsNumOf(task.wbsElement);
45+
return {
46+
taskId: task.taskId,
47+
wbsNum,
48+
title: task.title,
49+
notes: task.notes,
50+
deadline: task.deadline ?? undefined,
51+
startDate: task.startDate ?? undefined,
52+
priority: convertTaskPriority(task.priority),
53+
status: convertTaskStatus(task.status),
54+
createdBy: userTransformer(task.createdBy),
55+
assignees: task.assignees.map(userTransformer),
56+
dateDeleted: task.dateDeleted ?? undefined,
57+
dateCreated: task.dateCreated,
58+
deletedBy: task.deletedBy ? userTransformer(task.deletedBy) : undefined,
59+
projectLeadId: task.wbsElement.leadId ?? undefined,
60+
projectManagerId: task.wbsElement.managerId ?? undefined
61+
};
62+
};
63+
4364
export default taskTransformer;

src/frontend/src/apis/tasks.api.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* See the LICENSE file in the repository root folder for details.
44
*/
55

6-
import { Task, TaskCardPreview, TaskPriority, TaskStatus, WbsNumber, wbsPipe } from 'shared';
6+
import { CalendarTask, FilterTaskArgs, Task, TaskCardPreview, TaskPriority, TaskStatus, WbsNumber, wbsPipe } from 'shared';
77
import axios from '../utils/axios';
88
import { apiUrls } from '../utils/urls';
99
import { taskTransformer } from './transformers/tasks.transformers';
@@ -113,6 +113,17 @@ export const deleteSingleTask = (taskId: string) => {
113113
return axios.post<{ message: string }>(apiUrls.deleteTask(taskId), {});
114114
};
115115

116+
/**
117+
* Gets all tasks that match the filter criteria.
118+
* @param payload the filter criteria
119+
* @returns an array of tasks that match the filter criteria
120+
*/
121+
export const getFilterTasks = (payload: FilterTaskArgs) => {
122+
return axios.post<CalendarTask[]>(apiUrls.tasksFilter(), payload, {
123+
transformResponse: (data) => JSON.parse(data).map(taskTransformer)
124+
});
125+
};
126+
116127
export const getOverdueTasksByTeamLeader = (userId: string) => {
117128
return axios.get<TaskCardPreview[]>(apiUrls.overdueTasksByTeamLeadership(userId), {
118129
transformResponse: (data) => JSON.parse(data).map(taskTransformer)

src/frontend/src/components/NERFormModal.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ interface NERFormModalProps<T extends FieldValues> extends NERModalProps {
99
formId: string;
1010
children?: ReactNode;
1111
paperProps?: any;
12+
titleChildren?: ReactNode;
13+
actionsLeftChildren?: ReactNode;
1214
}
1315

1416
const NERFormModal = ({
@@ -25,7 +27,9 @@ const NERFormModal = ({
2527
children,
2628
showCloseButton,
2729
hideBackDrop = false,
28-
paperProps
30+
paperProps,
31+
titleChildren,
32+
actionsLeftChildren
2933
}: NERFormModalProps<any>) => {
3034
/**
3135
* Wrapper function for onSubmit so that form data is reset after submit
@@ -56,6 +60,8 @@ const NERFormModal = ({
5660
showCloseButton={showCloseButton}
5761
hideBackDrop={hideBackDrop}
5862
paperProps={paperProps}
63+
titleChildren={titleChildren}
64+
actionsLeftChildren={actionsLeftChildren}
5965
>
6066
<form id={formId} onSubmit={handleFormSubmit} noValidate>
6167
{children}

src/frontend/src/components/NERModal.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export interface NERModalProps {
1515
onHide: () => void;
1616
children?: ReactNode;
1717
titleChildren?: ReactNode;
18+
actionsLeftChildren?: ReactNode;
1819
cancelText?: CancelText;
1920
submitText?: SubmitText;
2021
disabled?: boolean;
@@ -40,7 +41,8 @@ const NERModal = ({
4041
hideBackDrop = false,
4142
icon,
4243
paperProps,
43-
titleChildren
44+
titleChildren,
45+
actionsLeftChildren
4446
}: NERModalProps) => {
4547
return (
4648
<Dialog
@@ -110,7 +112,8 @@ const NERModal = ({
110112
{children}
111113
</DialogContent>
112114
{!hideFormButtons && (
113-
<DialogActions>
115+
<DialogActions sx={{ justifyContent: actionsLeftChildren ? 'space-between' : 'flex-end' }}>
116+
{actionsLeftChildren && <Box sx={{ ml: 1 }}>{actionsLeftChildren}</Box>}
114117
<Box sx={{ display: 'flex', flexDirection: 'row', mb: 1 }}>
115118
<NERFailButton sx={{ mx: 1 }} form={formId} onClick={onHide}>
116119
{cancelText || 'Cancel'}

src/frontend/src/hooks/tasks.hooks.ts

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,15 @@
44
*/
55

66
import { useMutation, useQuery, useQueryClient } from 'react-query';
7-
import { WbsNumber, TaskPriority, TaskStatus, Task, TaskCardPreview } from 'shared';
7+
import { CalendarTask, FilterTaskArgs, WbsNumber, TaskPriority, TaskStatus, Task, TaskCardPreview } from 'shared';
88
import {
99
createSingleTask,
1010
deleteSingleTask,
1111
editSingleTaskStatus,
1212
editTask,
1313
editTaskAssignees,
14-
getOverdueTasksByTeamLeader
14+
getOverdueTasksByTeamLeader,
15+
getFilterTasks
1516
} from '../apis/tasks.api';
1617

1718
export interface CreateTaskPayload {
@@ -25,6 +26,24 @@ export interface CreateTaskPayload {
2526
assignees: string[];
2627
}
2728

29+
/**
30+
* Custom React Hook for filtering tasks based on various criteria
31+
* @returns the filtered tasks query
32+
*/
33+
export const useFilterTasks = (filterArgs: FilterTaskArgs | null) => {
34+
return useQuery<CalendarTask[], Error>(
35+
['filter-tasks', filterArgs],
36+
async () => {
37+
const { data } = await getFilterTasks(filterArgs!);
38+
return data;
39+
},
40+
{
41+
keepPreviousData: true,
42+
enabled: filterArgs !== null
43+
}
44+
);
45+
};
46+
2847
export const useCreateTask = () => {
2948
const queryClient = useQueryClient();
3049
return useMutation<Task, Error, CreateTaskPayload>(
@@ -45,6 +64,7 @@ export const useCreateTask = () => {
4564
{
4665
onSuccess: () => {
4766
queryClient.invalidateQueries(['projects']);
67+
queryClient.invalidateQueries(['filter-tasks']);
4868
}
4969
}
5070
);
@@ -82,6 +102,7 @@ export const useEditTask = () => {
82102
{
83103
onSuccess: () => {
84104
queryClient.invalidateQueries(['projects']);
105+
queryClient.invalidateQueries(['filter-tasks']);
85106
}
86107
}
87108
);
@@ -102,6 +123,7 @@ export const useEditTaskAssignees = () => {
102123
{
103124
onSuccess: () => {
104125
queryClient.invalidateQueries(['projects']);
126+
queryClient.invalidateQueries(['filter-tasks']);
105127
}
106128
}
107129
);
@@ -142,6 +164,7 @@ export const useDeleteTask = () => {
142164
{
143165
onSuccess: () => {
144166
queryClient.invalidateQueries(['projects']);
167+
queryClient.invalidateQueries(['filter-tasks']);
145168
}
146169
}
147170
);

0 commit comments

Comments
 (0)