Skip to content

Commit 327d850

Browse files
authored
Merge pull request #161 from uwblueprint/artyom/task-template-api
Artyom/task template api
2 parents 7ec34f6 + daab6d3 commit 327d850

File tree

8 files changed

+114
-84
lines changed

8 files changed

+114
-84
lines changed

backend/typescript/services/implementations/authService.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ class AuthService implements IAuthService {
3434
const user = await this.userService.getUserByEmail(email);
3535
return { ...token, ...user };
3636
} catch (error) {
37-
Logger.error(`Failed to generate token for user with email ${email}`);
37+
Logger.error(
38+
`Failed to generate token for user with email ${email}, error ${error}`,
39+
);
3840
throw error;
3941
}
4042
}

frontend/src/features/task-management/components/TaskDetailsModal.tsx

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,11 @@ import {
1313
useToast,
1414
} from "@chakra-ui/react";
1515
import React, { useEffect, useState } from "react";
16+
import { useHistory } from "react-router-dom";
1617
import TaskTemplateAPIClient from "../../../APIClients/TaskTemplateAPIClient";
1718
import Button from "../../../components/common/Button";
1819
import TaskCategoryBadge from "../../../components/common/TaskCategoryBadge";
20+
import { EDIT_TASK_TEMPLATE_PAGE } from "../../../constants/Routes";
1921
import { Task } from "../../../types/TaskTypes";
2022

2123
interface TaskDetailsModelsProps {
@@ -30,6 +32,7 @@ const TaskDetailsModal = ({
3032
onClose,
3133
}: TaskDetailsModelsProps): React.ReactElement => {
3234
const toast = useToast();
35+
const history = useHistory();
3336
const [taskTemplateData, setTaskTemplateData] = useState<Task | null>(null);
3437

3538
useEffect(() => {
@@ -118,13 +121,7 @@ const TaskDetailsModal = ({
118121
variant="blue-outline"
119122
width="100%"
120123
onClick={() => {
121-
toast({
122-
title: "Edit Task",
123-
description: "Edit task functionality not implemented yet",
124-
status: "info",
125-
duration: 3000,
126-
isClosable: true,
127-
});
124+
history.push(`${EDIT_TASK_TEMPLATE_PAGE}/${taskTemplateId}`);
128125
}}
129126
>
130127
Edit Task

frontend/src/features/task-management/components/TaskManagementTable.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@ import TaskManagementTableSection from "./TaskManagementTableSection";
77
interface TaskManagementTableProps {
88
tasks: Task[];
99
clearFilters: () => void;
10+
onTaskClick: (task: Task) => void;
1011
}
1112

1213
const TaskManagementTable = ({
1314
tasks,
1415
clearFilters,
16+
onTaskClick,
1517
}: TaskManagementTableProps): React.ReactElement => {
1618
return (
1719
<Flex width="100%" overflowX="auto">
@@ -48,7 +50,7 @@ const TaskManagementTable = ({
4850
</Tr>
4951
</Tbody>
5052
) : (
51-
<TaskManagementTableSection tasks={tasks} />
53+
<TaskManagementTableSection tasks={tasks} onTaskClick={onTaskClick} />
5254
)}
5355
</Table>
5456
</Flex>

frontend/src/features/task-management/components/TaskManagementTableSection.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,22 @@ import { Task } from "../../../types/TaskTypes";
55

66
interface TaskManagementTableSectionProps {
77
tasks: Task[];
8+
onTaskClick: (task: Task) => void;
89
}
910

1011
const TaskManagementTableSection = ({
1112
tasks,
13+
onTaskClick,
1214
}: TaskManagementTableSectionProps): React.ReactElement => {
1315
return (
1416
<Tbody>
1517
{tasks.map((task) => (
16-
<Tr key={task.id}>
18+
<Tr
19+
key={task.id}
20+
onClick={() => onTaskClick(task)}
21+
cursor="pointer"
22+
_hover={{ bg: "gray.100" }}
23+
>
1724
<Td width="20%" py="1rem" px="2.5rem">
1825
<Text textStyle="body" m={0} noOfLines={1}>
1926
{task.name}

frontend/src/features/task-management/pages/AddTaskTemplatePage.tsx

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -24,12 +24,8 @@ import { ReactComponent as GamesIcon } from "../../../assets/icons/games.svg";
2424
import { ReactComponent as TrainingIcon } from "../../../assets/icons/training.svg";
2525
import { ReactComponent as WalkIcon } from "../../../assets/icons/walk.svg";
2626
import { ReactComponent as MiscIcon } from "../../../assets/icons/misc.svg";
27-
28-
interface TaskTemplateForm {
29-
taskName: string;
30-
taskCategory: TaskCategory | null;
31-
taskInstructions: string;
32-
}
27+
import TaskTemplateAPIClient from "../../../APIClients/TaskTemplateAPIClient";
28+
import { type CreateTaskDTO } from "../../../types/TaskTypes";
3329

3430
interface FormErrors {
3531
taskName?: string;
@@ -39,10 +35,10 @@ interface FormErrors {
3935

4036
const AddTaskTemplatePage = (): React.ReactElement => {
4137
const history = useHistory();
42-
const [formData, setFormData] = useState<TaskTemplateForm>({
38+
const [formData, setFormData] = useState<CreateTaskDTO>({
4339
taskName: "",
44-
taskCategory: null,
45-
taskInstructions: "",
40+
category: TaskCategory.MISC,
41+
instructions: "",
4642
});
4743
const [errors, setErrors] = useState<FormErrors>({});
4844
const [isSubmitting, setIsSubmitting] = useState(false);
@@ -93,7 +89,7 @@ const AddTaskTemplatePage = (): React.ReactElement => {
9389
};
9490

9591
const handleCategoryChange = (category: TaskCategory) => {
96-
setFormData({ ...formData, taskCategory: category });
92+
setFormData({ ...formData, category });
9793
setHasChanges(true);
9894
if (errors.taskCategory) {
9995
setErrors({ ...errors, taskCategory: undefined });
@@ -103,7 +99,7 @@ const AddTaskTemplatePage = (): React.ReactElement => {
10399
const handleInstructionsChange = (
104100
event: React.ChangeEvent<HTMLTextAreaElement>,
105101
) => {
106-
setFormData({ ...formData, taskInstructions: event.target.value });
102+
setFormData({ ...formData, instructions: event.target.value });
107103
setHasChanges(true);
108104
if (errors.taskInstructions) {
109105
setErrors({ ...errors, taskInstructions: undefined });
@@ -117,11 +113,11 @@ const AddTaskTemplatePage = (): React.ReactElement => {
117113
newErrors.taskName = "Field is required.";
118114
}
119115

120-
if (!formData.taskCategory) {
116+
if (!formData.category) {
121117
newErrors.taskCategory = "Please select an option from the dropdown.";
122118
}
123119

124-
if (!formData.taskInstructions.trim()) {
120+
if (!formData.instructions?.trim()) {
125121
newErrors.taskInstructions = "Information must not exceed 10,000 words.";
126122
}
127123

@@ -144,12 +140,18 @@ const AddTaskTemplatePage = (): React.ReactElement => {
144140
/* eslint-disable-next-line no-console */
145141
console.log({
146142
taskName: formData.taskName,
147-
taskCategory: formData.taskCategory,
148-
taskInstructions: formData.taskInstructions,
143+
taskCategory: formData.category,
144+
taskInstructions: formData.instructions,
149145
});
150146

151-
// Simulate API call delay
152-
await new Promise((resolve) => setTimeout(resolve, 1000));
147+
// Call the API endpoint
148+
try {
149+
await TaskTemplateAPIClient.createTaskTemplate(formData);
150+
} catch (error) {
151+
// TODO: deprecate console use in frontend
152+
/* eslint-disable-next-line no-console */
153+
console.error("Could not post task template: ", error);
154+
}
153155

154156
// Navigate back to task management page
155157
history.push(TASK_MANAGEMENT_PAGE);
@@ -217,7 +219,7 @@ const AddTaskTemplatePage = (): React.ReactElement => {
217219
<SingleSelect
218220
label="Task Category"
219221
values={taskCategories}
220-
selected={formData.taskCategory}
222+
selected={formData.category}
221223
onSelect={handleCategoryChange}
222224
placeholder="Click for options"
223225
icons={taskCategoryIconsArray}
@@ -233,7 +235,7 @@ const AddTaskTemplatePage = (): React.ReactElement => {
233235

234236
<TextArea
235237
label="Instructions"
236-
value={formData.taskInstructions}
238+
value={formData.instructions || ""}
237239
onChange={handleInstructionsChange}
238240
placeholder="Write task instructions here"
239241
error={errors.taskInstructions}

frontend/src/features/task-management/pages/EditTaskTemplatePage.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ const EditTaskTemplatePage = (): React.ReactElement => {
7474
);
7575

7676
useEffect(() => {
77-
const fetchUser = async () => {
77+
const fetchTaskTemplate = async () => {
7878
if (!taskTemplateId) return;
7979

8080
try {
@@ -91,15 +91,15 @@ const EditTaskTemplatePage = (): React.ReactElement => {
9191
} catch (error) {
9292
toast({
9393
title: "Error",
94-
description: "Failed to fetch user data",
94+
description: "Failed to fetch task template data",
9595
status: "error",
9696
duration: 3000,
9797
isClosable: true,
9898
});
9999
}
100100
};
101101

102-
fetchUser();
102+
fetchTaskTemplate();
103103
}, [reset, taskTemplateId, toast]);
104104

105105
const handleBackClick = () => {
@@ -122,7 +122,7 @@ const EditTaskTemplatePage = (): React.ReactElement => {
122122
const handleDeleteTaskTemplate = () => {
123123
// TODO: Open delete task template modal and remove toast
124124
toast({
125-
title: "Delete User",
125+
title: "Delete Task Template",
126126
description: "Delete functionality not implemented yet",
127127
status: "info",
128128
duration: 3000,
@@ -275,7 +275,7 @@ const EditTaskTemplatePage = (): React.ReactElement => {
275275
onClick={handleDeleteTaskTemplate}
276276
type="button"
277277
>
278-
Delete User
278+
Delete Task Template
279279
</Button>
280280
<Button
281281
type="submit"

frontend/src/features/task-management/pages/TaskManagementPage.tsx

Lines changed: 70 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,29 @@
1-
import React, { useState, useMemo } from "react";
1+
import { useDisclosure } from "@chakra-ui/react";
2+
import React, { useEffect, useMemo, useState } from "react";
23
import { useHistory } from "react-router-dom";
34
import TaskManagementTable from "../components/TaskManagementTable";
5+
import TaskDetailsModal from "../components/TaskDetailsModal";
46
import { TableWrapper } from "../../../components/common/table";
5-
import { mockTasks } from "../../../types/TaskTypes";
7+
import { type Task } from "../../../types/TaskTypes";
68
import Button from "../../../components/common/Button";
79
import { ADD_TASK_TEMPLATE_PAGE } from "../../../constants/Routes";
10+
import TaskTemplateAPIClient from "../../../APIClients/TaskTemplateAPIClient";
11+
import Pagination from "../../../components/common/Pagination";
812

913
const TaskManagementPage = (): React.ReactElement => {
1014
const history = useHistory();
15+
const [tasks, setTasks] = useState<Task[]>([]);
1116
const [filters, setFilters] = useState<Record<string, string[]>>({});
1217
const [search, setSearch] = useState<string>("");
18+
const { isOpen, onOpen, onClose } = useDisclosure();
19+
const [selectedTask, setSelectedTask] = useState<Task | null>(null);
20+
const [page, setPage] = useState<number>(1);
21+
const numTasksPerPage = 10;
22+
23+
const handleTaskClick = (task: Task) => {
24+
setSelectedTask(task);
25+
onOpen();
26+
};
1327

1428
const handleClearFilters = () => {
1529
setFilters({});
@@ -29,20 +43,49 @@ const TaskManagementPage = (): React.ReactElement => {
2943
};
3044

3145
const filteredTasks = useMemo(() => {
32-
return mockTasks
33-
.filter((task) => {
46+
const hasActiveFilters = Object.values(filters).some(
47+
(vals) => vals && vals.length > 0,
48+
);
49+
const hasSearch = search.trim() !== "";
50+
51+
// If no filters and no search, just return everything
52+
if (!hasActiveFilters && !hasSearch) return tasks;
53+
54+
return tasks
55+
.filter((task: Task) => {
3456
return Object.keys(filters).every((key) => {
3557
const filterVals = filters[key];
3658
if (!filterVals || filterVals.length === 0) return true;
3759
return filterVals.includes(task[key as keyof typeof task] as string);
3860
});
3961
})
4062
.filter(
41-
(task) =>
63+
(task: Task) =>
4264
task.name.toLowerCase().includes(search.toLowerCase()) ||
43-
task.instructions.toLowerCase().includes(search.toLowerCase()),
65+
task.instructions?.toLowerCase().includes(search.toLowerCase()),
4466
);
45-
}, [filters, search]);
67+
}, [filters, search, tasks]);
68+
69+
const filteredTasksLength = filteredTasks.length;
70+
71+
const getTasks = async () => {
72+
try {
73+
const fetchedTasks = await TaskTemplateAPIClient.getAllTaskTemplates();
74+
75+
if (fetchedTasks != null) {
76+
setTasks(fetchedTasks);
77+
}
78+
} catch (error) {
79+
setTasks([]);
80+
// TODO: deprecate console use in frontend
81+
/* eslint-disable-next-line no-console */
82+
console.error("Could not fetch tasks");
83+
}
84+
};
85+
86+
useEffect(() => {
87+
getTasks();
88+
}, []);
4689

4790
return (
4891
<TableWrapper
@@ -63,11 +106,30 @@ const TaskManagementPage = (): React.ReactElement => {
63106
</Button>
64107
),
65108
}}
109+
bottomContent={
110+
<Pagination
111+
value={page}
112+
onChange={(newPage) => setPage(newPage)}
113+
numberOfItems={filteredTasksLength}
114+
itemsPerPage={numTasksPerPage}
115+
/>
116+
}
66117
>
67118
<TaskManagementTable
68-
tasks={filteredTasks}
119+
tasks={filteredTasks.slice(
120+
(page - 1) * numTasksPerPage,
121+
page * numTasksPerPage,
122+
)}
69123
clearFilters={handleClearFilters}
124+
onTaskClick={handleTaskClick}
70125
/>
126+
{selectedTask && (
127+
<TaskDetailsModal
128+
isOpen={isOpen}
129+
onClose={onClose}
130+
taskTemplateId={selectedTask.id}
131+
/>
132+
)}
71133
</TableWrapper>
72134
);
73135
};

0 commit comments

Comments
 (0)