From e1a3312ded93ac46501036decee76c44cf588693 Mon Sep 17 00:00:00 2001 From: Matt McCoy <59743922+MattCMcCoy@users.noreply.github.com> Date: Mon, 25 Mar 2024 11:09:37 -0400 Subject: [PATCH] Feature/connect-all-task-fe (#59) * feat(SingleTask.tsx): Added route functionality * feat(SingleTask.tsx): Implemented entirety of v4 of lofi with additional logic * style(Task.tsx): Updated svgs and got rid of TaskInfo type * test: fix backend go tests for tasks * feat(navigation-between-calendar-and-task-list-screen): navigation between calendar and task list screen * fix: some fixes towards calendar task list connection * refactor: move label call into render method for tasks * feat: task connenction and group role user routes * fix: fix delete test to actually utilize db connection * test: user tests * fix: separate assigned from task by task id * fix: general fixes to code and remove refetch interval bc ngrok limits to 120 rpm --------- Co-authored-by: narayansharma-21 <97.sharman@gmail.com> Co-authored-by: oliviaseds Co-authored-by: wyattchris Co-authored-by: Chris <125088905+wyattchris@users.noreply.github.com> --- backend/db/migrations/1.user.sql | 1 + backend/db/migrations/2.group.sql | 2 +- backend/db/migrations/3.task.sql | 5 +- backend/db/migrations/4.label.sql | 8 +- backend/docs/docs.go | 129 +++++++++++ backend/docs/swagger.json | 129 +++++++++++ backend/docs/swagger.yaml | 88 +++++++ .../schema/group-roles/group_roles_test.go | 33 +++ backend/schema/group-roles/routes.go | 121 +++++++++- backend/schema/group-roles/transactions.go | 30 +++ backend/schema/tasks/task_test.go | 45 +--- backend/schema/user/user_test.go | 214 ++++++++++++++++++ client/App.tsx | 4 +- client/assets/Date_today.svg | 8 + client/assets/Time.svg | 6 + client/assets/calendar.svg | 6 - client/assets/checkmark.svg | 4 + client/assets/reject.svg | 4 + client/components/BackButton.tsx | 22 ++ client/components/DropDownItem.tsx | 10 + client/components/QuickTaskCard.tsx | 11 +- client/components/TaskInfoCard.tsx | 60 +++++ client/components/profile/Header.tsx | 2 +- .../components/profile/ProfileTopHeader.tsx | 2 +- client/contexts/CareWalletContext.tsx | 19 +- client/contexts/api.ts | 9 - client/contexts/types.ts | 4 +- .../navigation/AppStackBottomTabNavigator.tsx | 38 +++- client/navigation/types.ts | 3 + client/package.json | 15 +- client/screens/Calendar.tsx | 58 ++--- client/screens/Profile/Profile.tsx | 15 +- client/screens/SingleTask.tsx | 104 +++++++++ client/screens/TaskList.tsx | 188 +++++++++++++++ client/screens/timelineEvents.tsx | 8 - client/services/group.ts | 12 +- client/services/medication.ts | 3 +- client/services/notifications.tsx | 3 - client/services/task.ts | 52 ++++- client/services/user.ts | 6 +- client/types/task.ts | 5 +- client/types/taskLabel.ts | 5 + client/types/type.ts | 16 +- 43 files changed, 1349 insertions(+), 158 deletions(-) create mode 100644 client/assets/Date_today.svg create mode 100644 client/assets/Time.svg delete mode 100644 client/assets/calendar.svg create mode 100644 client/assets/checkmark.svg create mode 100644 client/assets/reject.svg create mode 100644 client/components/BackButton.tsx create mode 100644 client/components/DropDownItem.tsx create mode 100644 client/components/TaskInfoCard.tsx delete mode 100644 client/contexts/api.ts create mode 100644 client/screens/SingleTask.tsx create mode 100644 client/screens/TaskList.tsx delete mode 100644 client/screens/timelineEvents.tsx create mode 100644 client/types/taskLabel.ts diff --git a/backend/db/migrations/1.user.sql b/backend/db/migrations/1.user.sql index 239164a..69da3b8 100644 --- a/backend/db/migrations/1.user.sql +++ b/backend/db/migrations/1.user.sql @@ -29,6 +29,7 @@ VALUES ('8Sy7xBkGiGQv4ZKphcQfY8PxAqw1', 'Narayan', 'Sharma', 'sharma.na@northeastern.edu', '', ''), ('iL7PnjS4axQffmlPceobjUUZ9DF2', 'Caitlin', 'Flynn', 'flynn.ca@northeastern.edu', '', ''), ('5JgN2PQxCRM9VoCiiFPlQPNqkL32', 'Linwood', 'Blaisdell', 'blaisdell.l@northeastern.edu', '', ''), + ('P03ggWcw63N0RSY7ltbkeBoR6bd2', 'Chris', 'Wyatt', 'wyatt.c@northeastern.edu', '', ''), ('9rIMSUo6qNf8ToTABkCfNqnByRv1', 'Haley', 'Martin', 'martin.hal@northeastern.edu', '', '') -- End Care-Wallet Team diff --git a/backend/db/migrations/2.group.sql b/backend/db/migrations/2.group.sql index 16116e9..e1c36e5 100644 --- a/backend/db/migrations/2.group.sql +++ b/backend/db/migrations/2.group.sql @@ -50,7 +50,7 @@ VALUES (5, 'onrQs8HVGBVMPNz4Fk1uE94bSxg1', 'SECONDARY'), (5, '8Sy7xBkGiGQv4ZKphcQfY8PxAqw1', 'SECONDARY'), (5, 'iL7PnjS4axQffmlPceobjUUZ9DF2', 'SECONDARY'), + (5, 'P03ggWcw63N0RSY7ltbkeBoR6bd2', 'SECONDARY'), (5, '9rIMSUo6qNf8ToTABkCfNqnByRv1', 'SECONDARY') - -- End Care-Wallet Team ; diff --git a/backend/db/migrations/3.task.sql b/backend/db/migrations/3.task.sql index 5fcd2c5..3e1f53b 100644 --- a/backend/db/migrations/3.task.sql +++ b/backend/db/migrations/3.task.sql @@ -46,12 +46,15 @@ VALUES ('task 2', 2, 'user3', '2024-02-20 23:59:59', '2024-02-10 14:30:00', NULL, 'Schedule doctor appointment', 'INCOMPLETE', 'other', FALSE), ('task 3', 3, 'user4', '2020-02-05 11:00:00', NULL, '2024-02-20 23:59:59', 'Submit insurance claim', 'PARTIAL', 'financial', FALSE), ('task 4', 4, 'user1', '2006-01-02 15:04:05', NULL, NULL, 'Refill water pitcher', 'COMPLETE', 'other', TRUE), + ('task 1 - NO LABEL', 1, 'user2', '2024-02-03 10:45:00', '2024-02-05 10:00:00', '2024-02-05 11:00:00', 'Pick up medication from pharmacy', 'INCOMPLETE', 'med_mgmt', FALSE), ('task 5', 5, 'user1', '2024-03-19 11:00:00', '2024-03-19 15:00:00', '2024-03-19 19:00:00', 'Get medications', 'INCOMPLETE', 'dr_appt', TRUE), ('task 6', 5, 'user2', '2024-03-19 11:00:00', '2024-03-19 11:00:00', '2024-03-19 13:00:00', 'File Papers', 'INCOMPLETE', 'med_mgmt', TRUE), ('task 7', 5, 'user3', '2024-03-19 11:00:00', '2024-03-19 07:00:00', '2024-03-19 09:00:00', 'Send check to Drs', 'INCOMPLETE', 'financial', TRUE), ('task 8', 5, 'user1', '2024-03-19 11:00:00', '2024-03-19 15:00:00', '2024-03-19 19:00:00', 'Get medications', 'INCOMPLETE', 'dr_appt', FALSE), ('task 9', 5, 'user2', '2024-03-19 11:00:00', '2024-03-19 11:00:00', '2024-03-19 13:00:00', 'File Papers', 'INCOMPLETE', 'med_mgmt', FALSE), - ('task 10', 5, 'user3', '2024-03-19 11:00:00', '2024-03-19 07:00:00', '2024-03-19 09:00:00', 'Send check to Drs', 'INCOMPLETE', 'financial', FALSE) + ('task 10', 5, 'user3', '2024-03-19 11:00:00', '2024-03-19 07:00:00', '2024-03-19 09:00:00', 'Send check to Drs', 'INCOMPLETE', 'financial', FALSE), + ('test tile', 5, 'P03ggWcw63N0RSY7ltbkeBoR6bd2', '2020-02-05 11:00:00', NULL, '2024-02-20 23:59:59', 'Submit insurance claim', 'PARTIAL', 'financial', FALSE), + ('test tile', 5, 'P03ggWcw63N0RSY7ltbkeBoR6bd2', '2024-02-20 23:59:59', '2024-02-10 14:30:00', NULL, 'Schedule doctor appointment', 'INCOMPLETE', 'med_mgmt', FALSE) ; INSERT INTO task_assignees (task_id, user_id, assignment_status, assigned_by, assigned_date) diff --git a/backend/db/migrations/4.label.sql b/backend/db/migrations/4.label.sql index 944719b..13e99b4 100644 --- a/backend/db/migrations/4.label.sql +++ b/backend/db/migrations/4.label.sql @@ -23,7 +23,9 @@ VALUES (2, 'Appointments', 'green'), (3, 'Financial', 'orange'), (4, 'Household', 'purple'), - (1, 'Household', 'purple') + (1, 'Household', 'purple'), + (5, 'Financial', 'orange'), + (5, 'Appointments', 'green') ; INSERT INTO task_labels (task_id, group_id, label_name) @@ -31,5 +33,7 @@ VALUES (1, 1, 'Medication'), (2, 2, 'Appointments'), (3, 3, 'Financial'), - (4, 4, 'Household') + (4, 4, 'Household'), + (6, 5, 'Financial'), + (7, 5, 'Appointments') ; diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 823c6de..df089af 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -380,6 +380,135 @@ const docTemplate = `{ } } }, + "/group/{groupId}/{uid}": { + "delete": { + "description": "removes a user from a group given a group id and user id", + "tags": [ + "group" + ], + "summary": "Remove a user from a group", + "parameters": [ + { + "type": "string", + "description": "groupId", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "userId", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GroupRole" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/group/{groupId}/{uid}/{role}": { + "put": { + "description": "add a user to a group given a user id and group id and role", + "tags": [ + "group" + ], + "summary": "Add a user to a group", + "parameters": [ + { + "type": "string", + "description": "groupId", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "userId", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Group\tRole", + "name": "role", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GroupRole" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "description": "Change a user group role based off of group id and user id and role", + "tags": [ + "group" + ], + "summary": "Change a user group role", + "parameters": [ + { + "type": "string", + "description": "groupId", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "userId", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "New User Group Role", + "name": "role", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, "/medications": { "get": { "description": "get all user medications", diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 412bdeb..cf5ea38 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -373,6 +373,135 @@ } } }, + "/group/{groupId}/{uid}": { + "delete": { + "description": "removes a user from a group given a group id and user id", + "tags": [ + "group" + ], + "summary": "Remove a user from a group", + "parameters": [ + { + "type": "string", + "description": "groupId", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "userId", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GroupRole" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/group/{groupId}/{uid}/{role}": { + "put": { + "description": "add a user to a group given a user id and group id and role", + "tags": [ + "group" + ], + "summary": "Add a user to a group", + "parameters": [ + { + "type": "string", + "description": "groupId", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "userId", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Group\tRole", + "name": "role", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.GroupRole" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "description": "Change a user group role based off of group id and user id and role", + "tags": [ + "group" + ], + "summary": "Change a user group role", + "parameters": [ + { + "type": "string", + "description": "groupId", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "userId", + "name": "uid", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "New User Group Role", + "name": "role", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, "/medications": { "get": { "description": "get all user medications", diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index fbd55f7..657f177 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -270,6 +270,94 @@ paths: summary: Get a group tags: - group + /group/{groupId}/{uid}: + delete: + description: removes a user from a group given a group id and user id + parameters: + - description: groupId + in: path + name: groupId + required: true + type: string + - description: userId + in: path + name: uid + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.GroupRole' + "400": + description: Bad Request + schema: + type: string + summary: Remove a user from a group + tags: + - group + /group/{groupId}/{uid}/{role}: + patch: + description: Change a user group role based off of group id and user id and + role + parameters: + - description: groupId + in: path + name: groupId + required: true + type: string + - description: userId + in: path + name: uid + required: true + type: string + - description: New User Group Role + in: path + name: role + required: true + type: string + responses: + "200": + description: OK + schema: + type: string + "400": + description: Bad Request + schema: + type: string + summary: Change a user group role + tags: + - group + put: + description: add a user to a group given a user id and group id and role + parameters: + - description: groupId + in: path + name: groupId + required: true + type: string + - description: userId + in: path + name: uid + required: true + type: string + - description: "Group\tRole" + in: path + name: role + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.GroupRole' + "400": + description: Bad Request + schema: + type: string + summary: Add a user to a group + tags: + - group /group/{groupId}/add: post: description: Adds a user to a care group given a userID, groupID, and role diff --git a/backend/schema/group-roles/group_roles_test.go b/backend/schema/group-roles/group_roles_test.go index b23783d..f4456c0 100644 --- a/backend/schema/group-roles/group_roles_test.go +++ b/backend/schema/group-roles/group_roles_test.go @@ -64,4 +64,37 @@ func TestGetGroupRoles(t *testing.T) { t.Errorf("Expected group ID: %+v, Actual group ID: %+v", expectedGroup, responseGroup) } }) + + t.Run("TestRemoveUserFromGroup", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/group/5/fIoFY26mJnYWH8sNdfuVoxpnVnr1", nil) + router.ServeHTTP(w, req) + + // Check for HTTP Status OK (200) + if http.StatusOK != w.Code { + t.Error("Failed to remove user from group.") + } + }) + + t.Run("TestAddUserToGroup", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/group/1/fIoFY26mJnYWH8sNdfuVoxpnVnr1/SECONDARY", nil) + router.ServeHTTP(w, req) + + // Check for HTTP Status OK (200) + if http.StatusOK != w.Code { + t.Error("Failed to remove user from group.") + } + }) + + t.Run("TestChangeUserGroupRole", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("PATCH", "/group/5/JamnX6TZf0dt6juozMRzNG5LMQd2/PATIENT", nil) + router.ServeHTTP(w, req) + + // Check for HTTP Status OK (200) + if http.StatusOK != w.Code { + t.Error("Failed to change user role in group.") + } + }) } diff --git a/backend/schema/group-roles/routes.go b/backend/schema/group-roles/routes.go index 37bebd6..28db025 100644 --- a/backend/schema/group-roles/routes.go +++ b/backend/schema/group-roles/routes.go @@ -12,17 +12,134 @@ type PgModel struct { Conn *pgxpool.Pool } -// groupRoles.go file func GroupRolesGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { groupRoles := v1.Group("") { groupRoles.GET("/:groupId/roles", c.GetGroupRoles) - groupRoles.GET("/member/:uid", c.GetGroupByUID) + + group := v1.Group(":groupId") + { + user := group.Group(":uid") + { + user.DELETE("", c.RemoveUserFromGroup) + user.PATCH(":role", c.ChangeUserGroupRole) + user.PUT(":role", c.AddUserToGroup) + } + } + + member := v1.Group("member") + { + user := member.Group(":uid") + { + user.GET("", c.GetGroupByUID) + } + } + } return groupRoles } +// ChangeUserGroupRole godoc +// +// @summary Change a user group role +// @description Change a user group role based off of group id and user id and role +// @tags group +// +// @param groupId path string true "groupId" +// @param uid path string true "userId" +// @param role path string true "New User Group Role" +// +// @success 200 {object} string +// @failure 400 {object} string +// @router /group/{groupId}/{uid}/{role} [patch] +func (pg *PgModel) ChangeUserGroupRole(c *gin.Context) { + gid := c.Param("groupId") + gidInt, err := strconv.Atoi(gid) + uid := c.Param("uid") + role := c.Param("role") + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + err = ChangeUserGroupRoleInDB(pg.Conn, gidInt, uid, role) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, "") +} + +// AddUserToGroup godoc +// +// @summary Add a user to a group +// @description add a user to a group given a user id and group id and role +// @tags group +// +// @param groupId path string true "groupId" +// @param uid path string true "userId" +// @param role path string true "Group Role" +// +// @success 200 {object} models.GroupRole +// @failure 400 {object} string +// @router /group/{groupId}/{uid}/{role} [put] +func (pg *PgModel) AddUserToGroup(c *gin.Context) { + gid := c.Param("groupId") + gidInt, err := strconv.Atoi(gid) + uid := c.Param("uid") + role := c.Param("role") + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + groupRole, err := AddUserToGroupInDB(pg.Conn, gidInt, uid, role) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, groupRole) +} + +// RemoveUserFromGroup godoc +// +// @summary Remove a user from a group +// @description removes a user from a group given a group id and user id +// @tags group +// +// @param groupId path string true "groupId" +// @param uid path string true "userId" +// +// @success 200 {object} models.GroupRole +// @failure 400 {object} string +// @router /group/{groupId}/{uid} [delete] +func (pg *PgModel) RemoveUserFromGroup(c *gin.Context) { + gid := c.Param("groupId") + gidInt, err := strconv.Atoi(gid) + uid := c.Param("uid") + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + err = RemoveUserFromGroupInDB(pg.Conn, gidInt, uid) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, "") +} + // GetGroupByUID godoc // // @summary Retrieve a group id given a user id diff --git a/backend/schema/group-roles/transactions.go b/backend/schema/group-roles/transactions.go index 25a9cb2..1626222 100644 --- a/backend/schema/group-roles/transactions.go +++ b/backend/schema/group-roles/transactions.go @@ -8,6 +8,36 @@ import ( "github.com/jackc/pgx/v5/pgxpool" ) +func ChangeUserGroupRoleInDB(pool *pgxpool.Pool, gid int, uid string, role string) error { + fmt.Println(role) + _, err := pool.Exec(context.Background(), "UPDATE group_roles SET role = $1 WHERE group_id = $2 AND user_id = $3", role, gid, uid) + + return err +} + +func AddUserToGroupInDB(pool *pgxpool.Pool, gid int, uid string, role string) (models.GroupRole, error) { + var groupMember models.GroupRole + err := pool.QueryRow(context.Background(), "INSERT into group_roles (group_id, user_id, role) VALUES ($1, $2, $3) RETURNING *", gid, uid, role).Scan(&groupMember.GroupID, &groupMember.UserID, &groupMember.Role) + + if err != nil { + fmt.Printf("Error getting group_id from user_id: %v", err) + return groupMember, err + } + + return groupMember, nil +} + +func RemoveUserFromGroupInDB(pool *pgxpool.Pool, gid int, uid string) error { + _, err := pool.Exec(context.Background(), "DELETE from group_roles WHERE group_id = $1 AND user_id = $2", gid, uid) + + if err != nil { + fmt.Printf("Error getting group_id from user_id: %v", err) + return err + } + + return nil +} + // GetGroupIDByUIDFromDB returns the groupID of a user given their UID func GetGroupMemberByUIDFromDB(pool *pgxpool.Pool, uid string) (models.GroupRole, error) { var groupMember models.GroupRole diff --git a/backend/schema/tasks/task_test.go b/backend/schema/tasks/task_test.go index 63f2b98..e32f535 100644 --- a/backend/schema/tasks/task_test.go +++ b/backend/schema/tasks/task_test.go @@ -12,7 +12,6 @@ import ( "net/url" "os" "reflect" - "strconv" "testing" "time" @@ -119,7 +118,7 @@ func TestTaskGroup(t *testing.T) { } if !reflect.DeepEqual(expectedTasks, responseTasks) { - t.Error("Result was not correct") + t.Error("Result was not correct", responseTasks, "Expected", expectedTasks) return } }) @@ -349,48 +348,8 @@ func TestTaskGroup(t *testing.T) { }) t.Run("TestDeleteTask", func(t *testing.T) { - getTaskByIDFunc := func(taskID int) (models.Task, error) { - return models.Task{ - TaskID: taskID, - // Add other necessary fields - }, nil - } - - // Mock the successful deletion of the task in the database - deleteTaskInDBFunc := func(taskID int) error { - return nil - } - - // Create a new Gin router - router := gin.Default() - - // Attach the DeleteTask route to the router - router.DELETE("/tasks/:tid", func(c *gin.Context) { - // Extract task ID from the path parameter - taskID, err := strconv.Atoi(c.Param("tid")) - if err != nil { - c.JSON(http.StatusBadRequest, err.Error()) - return - } - - // Check if the task exists before attempting to delete - if _, err := getTaskByIDFunc(taskID); err != nil { - c.JSON(http.StatusBadRequest, err.Error()) - return - } - - // Delete the task from the database - if err := deleteTaskInDBFunc(taskID); err != nil { - fmt.Println("error deleting task from the database:", err) - c.JSON(http.StatusBadRequest, err.Error()) - return - } - - c.Status(http.StatusNoContent) - }) - // Perform a DELETE request to the /tasks/:tid endpoint - req, err := http.NewRequest("DELETE", "/tasks/1", nil) + req, err := http.NewRequest("DELETE", "/tasks/5", nil) if err != nil { t.Fatal("Failed to create HTTP request:", err) } diff --git a/backend/schema/user/user_test.go b/backend/schema/user/user_test.go index a00006b..0301957 100644 --- a/backend/schema/user/user_test.go +++ b/backend/schema/user/user_test.go @@ -1 +1,215 @@ package user + +import ( + "bytes" + "carewallet/configuration" + "carewallet/db" + "carewallet/models" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "os" + "reflect" + "testing" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func TestUser(t *testing.T) { + config, err := configuration.GetConfiguration() + + if err != nil { + fmt.Fprintf(os.Stderr, "Unable to retreive configuration file: %v\n", err) + os.Exit(1) + } + + conn := db.ConnectPosgresDatabase(config) + + defer conn.Close() + + controller := PgModel{Conn: conn} + + router := gin.Default() + + router.Use(cors.Default()) + + v1 := router.Group("") + { + UserGroup(v1, &controller) + } + + t.Run("TestGetUser", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/user/user1", nil) + router.ServeHTTP(w, req) + + // Check for HTTP Status OK (200) + if http.StatusOK != w.Code { + t.Error("Failed to retrieve user.") + return + } + + var responseUser models.User + + // Unmarshal the response JSON + if err := json.Unmarshal(w.Body.Bytes(), &responseUser); err != nil { + t.Errorf("Failed to unmarshal JSON: %v", err) + return + } + + // Define the expected user + expectedUser := models.User{ + UserID: "user1", + Phone: "123-456-7890", + Address: "123 Main St", + FirstName: "John", + LastName: "Smith", + Email: "john.smith@example.com", + } + + if expectedUser != responseUser { + t.Errorf("Expected user: %+v, Response User: %+v", expectedUser, responseUser) + } + + }) + + t.Run("TestGetUsers", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/user?userIDs=user1,user2", nil) + router.ServeHTTP(w, req) + + // Check for HTTP Status OK (200) + if http.StatusOK != w.Code { + t.Error("Failed to retrieve user.") + return + } + + var responseUser []models.User + + // Unmarshal the response JSON + if err := json.Unmarshal(w.Body.Bytes(), &responseUser); err != nil { + t.Errorf("Failed to unmarshal JSON: %v", err) + return + } + + // Define the expected user + expectedUser := []models.User{ + { + UserID: "user1", + Phone: "123-456-7890", + Address: "123 Main St", + FirstName: "John", + LastName: "Smith", + Email: "john.smith@example.com", + }, + { + UserID: "user2", + Phone: "987-654-3210", + FirstName: "Jane", + LastName: "Doe", + Email: "jane.doe@example.com", + Address: "456 Elm St", + }, + } + + if !reflect.DeepEqual(expectedUser, responseUser) { + t.Errorf("Expected user: %+v, Response User: %+v", expectedUser, responseUser) + } + }) + + t.Run("TestUpdateUser", func(t *testing.T) { + var updateRequest = UserInfoBody{ + FirstName: "Matt", + LastName: "Matt", + Email: "Email", + Phone: "Phone", + Address: "Address", + } + + requestJSON, err := json.Marshal(updateRequest) + if err != nil { + t.Error("Failed to marshal remove request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PUT", "/user/user1", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + // Check for HTTP Status OK (200) + if http.StatusOK != w.Code { + t.Error("Failed to retrieve user.") + return + } + + var responseUser models.User + + // Unmarshal the response JSON + if err := json.Unmarshal(w.Body.Bytes(), &responseUser); err != nil { + t.Errorf("Failed to unmarshal JSON: %v", err) + return + } + + // Define the expected user + expectedUser := models.User{ + UserID: "user1", + FirstName: "Matt", + LastName: "Matt", + Email: "Email", + Phone: "Phone", + Address: "Address", + } + + if !reflect.DeepEqual(expectedUser, responseUser) { + t.Errorf("Expected user: %+v, Response User: %+v", expectedUser, responseUser) + } + }) + + t.Run("TestCreateUser", func(t *testing.T) { + var createRequest = UserInfoBody{ + FirstName: "Matt", + LastName: "Matt", + Email: "Email", + Phone: "Phone", + Address: "Address", + } + + requestJSON, err := json.Marshal(createRequest) + if err != nil { + t.Error("Failed to marshal remove request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/user/user20", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + // Check for HTTP Status OK (200) + if http.StatusOK != w.Code { + t.Error("Failed to retrieve user.") + return + } + + var responseUser models.User + + // Unmarshal the response JSON + if err := json.Unmarshal(w.Body.Bytes(), &responseUser); err != nil { + t.Errorf("Failed to unmarshal JSON: %v", err) + return + } + + // Define the expected user + expectedUser := models.User{ + UserID: "user20", + FirstName: "Matt", + LastName: "Matt", + Email: "Email", + Phone: "Phone", + Address: "Address", + } + + if !reflect.DeepEqual(expectedUser, responseUser) { + t.Errorf("Expected user: %+v, Response User: %+v", expectedUser, responseUser) + } + }) +} diff --git a/client/App.tsx b/client/App.tsx index 4ace8fe..911c7ba 100644 --- a/client/App.tsx +++ b/client/App.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import { SafeAreaView } from 'react-native'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { PaperProvider } from 'react-native-paper'; -import { SafeAreaView } from 'react-native-safe-area-context'; import { CareWalletProvider } from './contexts/CareWalletContext'; import { Router } from './navigation/Router'; @@ -13,7 +13,7 @@ export default function App() { return ( - + diff --git a/client/assets/Date_today.svg b/client/assets/Date_today.svg new file mode 100644 index 0000000..dc72ee1 --- /dev/null +++ b/client/assets/Date_today.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/client/assets/Time.svg b/client/assets/Time.svg new file mode 100644 index 0000000..b90522b --- /dev/null +++ b/client/assets/Time.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/assets/calendar.svg b/client/assets/calendar.svg deleted file mode 100644 index 51b365f..0000000 --- a/client/assets/calendar.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/client/assets/checkmark.svg b/client/assets/checkmark.svg new file mode 100644 index 0000000..1c8e62f --- /dev/null +++ b/client/assets/checkmark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/assets/reject.svg b/client/assets/reject.svg new file mode 100644 index 0000000..e83910c --- /dev/null +++ b/client/assets/reject.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/components/BackButton.tsx b/client/components/BackButton.tsx new file mode 100644 index 0000000..af6b5c1 --- /dev/null +++ b/client/components/BackButton.tsx @@ -0,0 +1,22 @@ +import React from 'react'; + +import { useNavigation } from '@react-navigation/native'; +import { Button } from 'react-native-paper'; + +import { AppStackNavigation } from '../navigation/types'; + +export function BackButton() { + const navigation = useNavigation(); + + return ( + + ); +} diff --git a/client/components/DropDownItem.tsx b/client/components/DropDownItem.tsx new file mode 100644 index 0000000..9699464 --- /dev/null +++ b/client/components/DropDownItem.tsx @@ -0,0 +1,10 @@ +import React from 'react'; +import { Text, View } from 'react-native'; + +export function DropdownItem({ label }: { label: string }) { + return ( + + {label} + + ); +} diff --git a/client/components/QuickTaskCard.tsx b/client/components/QuickTaskCard.tsx index 302a985..9a499ad 100644 --- a/client/components/QuickTaskCard.tsx +++ b/client/components/QuickTaskCard.tsx @@ -1,12 +1,17 @@ import React from 'react'; import { Text, View } from 'react-native'; +import { TaskTypeDescriptions } from '../types/type'; + interface QuickTaskCardProps { name: string; label: string; } -function QuickTaskCard({ name, label }: QuickTaskCardProps): JSX.Element { +export function QuickTaskCard({ + name, + label +}: QuickTaskCardProps): JSX.Element { return ( @@ -16,11 +21,9 @@ function QuickTaskCard({ name, label }: QuickTaskCardProps): JSX.Element { - {label} + {TaskTypeDescriptions[label]} ); } - -export { QuickTaskCard }; diff --git a/client/components/TaskInfoCard.tsx b/client/components/TaskInfoCard.tsx new file mode 100644 index 0000000..fe13294 --- /dev/null +++ b/client/components/TaskInfoCard.tsx @@ -0,0 +1,60 @@ +import React from 'react'; +import { Text, View } from 'react-native'; + +import moment from 'moment'; + +import Calendar from '../assets/Date_today.svg'; +import Time from '../assets/Time.svg'; +import { useTaskById } from '../services/task'; +import { TaskTypeDescriptions } from '../types/type'; + +export function TaskInfoComponent({ + name, + id, + category, + status, + date +}: { + id: number; + name: string; + category: string; + status: string; + date: Date; +}) { + const { taskLabels } = useTaskById(id.toString()); + + return ( + + + {name} + + + + {moment(date).format('MMMM DD')} + + + + + + + + + {TaskTypeDescriptions[category]} + + + + {`${status?.charAt(0)}${status?.slice(1).toLowerCase()}`} + + + {taskLabels?.map((label) => ( + + {label.label_name} + + ))} + + + + ); +} diff --git a/client/components/profile/Header.tsx b/client/components/profile/Header.tsx index 9817d01..6ae221a 100644 --- a/client/components/profile/Header.tsx +++ b/client/components/profile/Header.tsx @@ -8,7 +8,7 @@ import ArrowLeft from '../../assets/arrow-left.svg'; import Edit from '../../assets/profile/edit.svg'; import Ellipse from '../../assets/profile/ellipse.svg'; import { useCareWalletContext } from '../../contexts/CareWalletContext'; -import { AppStackNavigation } from '../../navigation/AppNavigation'; +import { AppStackNavigation } from '../../navigation/types'; import { GroupRole, Role } from '../../types/group'; import { User } from '../../types/user'; import { ProfileTopHeader } from './ProfileTopHeader'; diff --git a/client/components/profile/ProfileTopHeader.tsx b/client/components/profile/ProfileTopHeader.tsx index 74c66a4..01add7e 100644 --- a/client/components/profile/ProfileTopHeader.tsx +++ b/client/components/profile/ProfileTopHeader.tsx @@ -4,7 +4,7 @@ import { Text, View } from 'react-native'; import { useNavigation } from '@react-navigation/native'; import { IconButton } from 'react-native-paper'; -import { AppStackNavigation } from '../../navigation/AppNavigation'; +import { AppStackNavigation } from '../../navigation/types'; import { User } from '../../types/user'; interface ProfileTopHeaderProps { diff --git a/client/contexts/CareWalletContext.tsx b/client/contexts/CareWalletContext.tsx index 448a71c..228b206 100644 --- a/client/contexts/CareWalletContext.tsx +++ b/client/contexts/CareWalletContext.tsx @@ -2,7 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { getAuth, onAuthStateChanged } from 'firebase/auth'; -import { getUserGroup } from './api'; +import { useUserGroup } from '../services/group'; import { Group, User } from './types'; type CareWalletContextData = { @@ -21,6 +21,8 @@ export function CareWalletProvider({ const [group, setGroup] = useState({} as Group); const auth = getAuth(); + const { userGroupRole, userGroupRoleIsLoading } = useUserGroup(user.userID); + useEffect(() => { onAuthStateChanged(auth, (user) => { const signedInUser: User = { @@ -29,16 +31,17 @@ export function CareWalletProvider({ }; setUser(signedInUser); - - // TODO: What should happen if a user isnt apart of a group? - if (user) { - getUserGroup(user.uid).then((grouprole) => { - setGroup({ role: grouprole.role, groupID: grouprole.group_id }); - }); - } }); }, []); + useEffect(() => { + if (userGroupRole && !userGroupRoleIsLoading) + setGroup({ + groupID: userGroupRole.group_id, + role: userGroupRole.role + }); + }, [userGroupRole]); + const CareWalletContextStore: CareWalletContextData = { user: user, group: group diff --git a/client/contexts/api.ts b/client/contexts/api.ts deleted file mode 100644 index a1fcb55..0000000 --- a/client/contexts/api.ts +++ /dev/null @@ -1,9 +0,0 @@ -import axios from 'axios'; - -import { api_url } from '../services/api-links'; -import { GroupRole } from '../types/group'; - -export const getUserGroup = async (userId: string): Promise => { - const { data } = await axios.get(`${api_url}/group/member/${userId}`); - return data; -}; diff --git a/client/contexts/types.ts b/client/contexts/types.ts index 81c0d92..ba7789a 100644 --- a/client/contexts/types.ts +++ b/client/contexts/types.ts @@ -1,3 +1,5 @@ +import { Role } from '../types/group'; + export interface User { userID: string; userEmail: string; @@ -5,5 +7,5 @@ export interface User { export interface Group { groupID: number; - role: string; // TODO: update to enum + role: Role; } diff --git a/client/navigation/AppStackBottomTabNavigator.tsx b/client/navigation/AppStackBottomTabNavigator.tsx index 107c485..48e1a01 100644 --- a/client/navigation/AppStackBottomTabNavigator.tsx +++ b/client/navigation/AppStackBottomTabNavigator.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { Text } from 'react-native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { createMaterialTopTabNavigator } from '@react-navigation/material-top-tabs'; import Bell from '../assets/bottom-nav/bell.svg'; import Calendar from '../assets/bottom-nav/calendar.svg'; @@ -11,9 +12,12 @@ import TimelineCalendarScreen from '../screens/Calendar'; import MedicationList from '../screens/MedicationList'; import PatientView from '../screens/Profile/PatientView'; import Profile from '../screens/Profile/Profile'; +import SingleTaskScreen from '../screens/SingleTask'; +import TaskList from '../screens/TaskList'; import { AppStack } from './types'; const AppStackBottomTab = createBottomTabNavigator(); +const TopTab = createMaterialTopTabNavigator(); export function AppStackBottomTabNavigator() { return ( @@ -26,20 +30,20 @@ export function AppStackBottomTabNavigator() { , tabBarLabel: () => }} component={MedicationList} /> , tabBarLabel: () => }} - component={TimelineCalendarScreen} + component={CalendarNavigationContainer} /> ); } + +function CalendarNavigationContainer() { + return ( + + + + + ); +} + +function CalendarTopNav() { + return ( + + + + + ); +} diff --git a/client/navigation/types.ts b/client/navigation/types.ts index 72acce9..f5b6033 100644 --- a/client/navigation/types.ts +++ b/client/navigation/types.ts @@ -12,6 +12,9 @@ export type AppStackParamList = { Calendar: undefined; Notifications: undefined; TaskType: undefined; + TaskDisplay: { id: number }; + TaskList: undefined; + CalendarTopNav: undefined; }; export type AppStackNavigation = NavigationProp; diff --git a/client/package.json b/client/package.json index 7829f97..59ae6d4 100644 --- a/client/package.json +++ b/client/package.json @@ -18,26 +18,26 @@ "@gorhom/bottom-sheet": "^4.6.1", "@react-native-async-storage/async-storage": "1.21.0", "@react-navigation/bottom-tabs": "^6.5.11", + "@react-navigation/material-top-tabs": "^6.6.13", "@react-navigation/native": "^6.1.9", "@react-navigation/native-stack": "^6.9.17", "@tanstack/react-query": "^5.18.1", - "@types/react": "^18.2.55", "axios": "^1.6.4", "clsx": "^2.1.0", "date-fns": "^3.3.1", "moment": "^2.30.1", - "expo": "^50.0.11", - "expo-device": "~5.4.0", + "expo": "~50.0.14", + "expo-device": "~5.9.3", "expo-document-picker": "~11.10.1", "expo-file-system": "~16.0.6", - "expo-notifications": "~0.20.1", + "expo-notifications": "~0.27.6", "expo-status-bar": "~1.11.1", "firebase": "^10.7.2", "lodash": "^4.17.21", "nativewind": "^2.0.11", "react": "18.2.0", - "react-native": "0.73.4", - "react-native-calendars": "^1.1303.0", + "react-native": "0.73.6", + "react-native-calendars": "^1.1304.1", "react-native-dropdown-picker": "^5.4.6", "react-native-gesture-handler": "~2.14.0", "react-native-paper": "^5.12.3", @@ -49,7 +49,8 @@ "devDependencies": { "@babel/core": "^7.20.0", "@trivago/prettier-plugin-sort-imports": "^4.3.0", - "@types/lodash": "^4.14.202", + "@types/lodash": "^4.17.0", + "@types/react": "^18.2.55", "@types/react-native": "^0.73.0", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "6.18.0", diff --git a/client/screens/Calendar.tsx b/client/screens/Calendar.tsx index 5df0a1d..54ec378 100644 --- a/client/screens/Calendar.tsx +++ b/client/screens/Calendar.tsx @@ -5,10 +5,11 @@ import React, { useRef, useState } from 'react'; -import { ActivityIndicator, Text, View } from 'react-native'; +import { ActivityIndicator, Pressable, Text, View } from 'react-native'; import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; import { BottomSheetDefaultBackdropProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetBackdrop/types'; +import { useNavigation } from '@react-navigation/native'; import _, { Dictionary } from 'lodash'; import moment from 'moment'; import { @@ -25,13 +26,16 @@ import { FlatList, GestureHandlerRootView } from 'react-native-gesture-handler'; import { QuickTaskCard } from '../components/QuickTaskCard'; import { useCareWalletContext } from '../contexts/CareWalletContext'; +import { AppStackNavigation } from '../navigation/types'; import { useFilteredTasks } from '../services/task'; import { Task } from '../types/task'; -import { EVENT_COLOR, getDate } from './timelineEvents'; export default function TimelineCalendarScreen() { + const navigation = useNavigation(); const { group } = useCareWalletContext(); - const [currentDate, setCurrentDate] = useState(getDate()); + const [currentDate, setCurrentDate] = useState( + moment(new Date()).format('YYYY-MM-DD') + ); const [month, setCurrentMonth] = useState(); const { tasks, tasksIsLoading } = useFilteredTasks({ @@ -103,7 +107,7 @@ export default function TimelineCalendarScreen() { end: moment(task.end_date).format('YYYY-MM-DD hh:mm:ss'), title: task.task_title, summary: task.task_status, - color: EVENT_COLOR + color: '#e6add8' }; }), (e) => CalendarUtils.getCalendarDateString(e?.start) @@ -138,6 +142,8 @@ export default function TimelineCalendarScreen() { handleOpenPress(); return; } + + navigation.navigate('TaskDisplay', { id: parseInt(e.id ?? '-1') }); } const renderBackdrop = useCallback( @@ -154,14 +160,6 @@ export default function TimelineCalendarScreen() { const snapPoints = useMemo(() => ['70%'], []); const bottomSheetRef = useRef(null); - // Todo: Look into if there is a change for this - const taskTypeDescriptions: Record = { - med_mgmt: 'Medication Management', - dr_appt: 'Doctor Appointment', - financial: 'Financial Task', - other: 'Other Task' - }; - const timelineProps: Partial = { format24h: true, unavailableHours: [ @@ -195,19 +193,18 @@ export default function TimelineCalendarScreen() { showTodayButton disabledOpacity={0.6} > - - - )} - timelineProps={timelineProps} - showNowIndicator - scrollToFirst - initialTime={{ - hour: parseInt(moment(Date.now()).format('hh')), - minutes: parseInt(moment(Date.now()).format('mm')) - }} - /> - + + )} + timelineProps={timelineProps} + showNowIndicator + scrollToFirst + initialTime={{ + hour: parseInt(moment(Date.now()).format('hh')), + minutes: parseInt(moment(Date.now()).format('mm')) + }} + /> } keyExtractor={(item) => item.task_id.toString()} renderItem={({ item }) => ( - + + navigation.navigate('TaskDisplay', { id: item.task_id }) + } + > + + )} /> diff --git a/client/screens/Profile/Profile.tsx b/client/screens/Profile/Profile.tsx index 30f90c0..09643b2 100644 --- a/client/screens/Profile/Profile.tsx +++ b/client/screens/Profile/Profile.tsx @@ -10,6 +10,7 @@ import { useCareWalletContext } from '../../contexts/CareWalletContext'; import { AppStackNavigation } from '../../navigation/types'; import { useAuth } from '../../services/auth'; import { useGroup } from '../../services/group'; +import { useTaskByAssigned } from '../../services/task'; import { useUsers } from '../../services/user'; export default function Profile() { @@ -20,10 +21,11 @@ export default function Profile() { const { users, usersAreLoading } = useUsers( roles?.map((role) => role.user_id) ?? [] ); + const { taskByUser, taskByUserIsLoading } = useTaskByAssigned(activeUser); const { signOutMutation } = useAuth(); - if (rolesAreLoading || usersAreLoading) { + if (rolesAreLoading || usersAreLoading || taskByUserIsLoading) { return ( @@ -40,10 +42,6 @@ export default function Profile() { ); } - // TODO: Connext with task screen - // TODO: Get task number from backend - // TODO: Connext with patient information screen - // TODO: Add ability to change user view if I click on another user? return (
- + navigation.navigate('TaskList')} + > Your Tasks - 4 + {taskByUser?.length ?? 0} diff --git a/client/screens/SingleTask.tsx b/client/screens/SingleTask.tsx new file mode 100644 index 0000000..42362bb --- /dev/null +++ b/client/screens/SingleTask.tsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import { ActivityIndicator, Text, TextInput, View } from 'react-native'; + +import { RouteProp, useRoute } from '@react-navigation/native'; +import moment from 'moment'; +import DropDownPicker from 'react-native-dropdown-picker'; + +import CheckMark from '../assets/checkmark.svg'; +import Reject from '../assets/reject.svg'; +import { BackButton } from '../components/task-type/BackButton'; +import { useTaskById } from '../services/task'; +import { TaskTypeDescriptions, TypeToCategoryMap } from '../types/type'; + +type ParamList = { + mt: { + id: string; + }; +}; + +export default function SingleTaskScreen() { + const route = useRoute>(); + const { id } = route.params; + const [open, setOpen] = useState(false); + const { task, taskIsLoading, taskLabels, taskLabelsIsLoading } = + useTaskById(id); + + if (taskIsLoading || taskLabelsIsLoading) + return ( + + + Loading Task... + + ); + + if (!task || task == undefined) { + + Error Loading Task + ; + } + + return ( + + + + + ''} + placeholder="To-Do" + style={{ + backgroundColor: 'lightgray', + borderColor: 'gray', + position: 'relative', + zIndex: 10 + }} + /> + + + + + {task?.task_title} + + + {moment(task?.start_date).format('hh:mm A')} + {task?.end_date && `- ${moment(task?.end_date).format('hh:mm A')}`} + + {taskLabels?.map((label) => ( + + {label.label_name || ''} + + ))} + + {task && + `${TypeToCategoryMap[task.task_type]} | ${TaskTypeDescriptions[task.task_type]}`} + + + + + + + {task?.notes} + + Additional Notes + + + + + + + + + + + + + ); +} diff --git a/client/screens/TaskList.tsx b/client/screens/TaskList.tsx new file mode 100644 index 0000000..6d46727 --- /dev/null +++ b/client/screens/TaskList.tsx @@ -0,0 +1,188 @@ +import React, { useCallback, useMemo, useRef, useState } from 'react'; +import { Pressable, ScrollView, Text, TextInput, View } from 'react-native'; + +import BottomSheet, { BottomSheetBackdrop } from '@gorhom/bottom-sheet'; +import { BottomSheetDefaultBackdropProps } from '@gorhom/bottom-sheet/lib/typescript/components/bottomSheetBackdrop/types'; +import { useNavigation } from '@react-navigation/native'; +import DropDownPicker from 'react-native-dropdown-picker'; +import { GestureHandlerRootView } from 'react-native-gesture-handler'; +import { Button } from 'react-native-paper'; + +import { CloseButton } from '../components/task-type/CloseButton'; +import { TaskInfoComponent } from '../components/TaskInfoCard'; +import { useCareWalletContext } from '../contexts/CareWalletContext'; +import { AppStackNavigation } from '../navigation/types'; +import { useFilteredTasks } from '../services/task'; +import { Task } from '../types/task'; + +export default function TaskListScreen() { + const { group } = useCareWalletContext(); + const navigator = useNavigation(); + const [canPress, setCanPress] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const { tasks } = useFilteredTasks({ groupID: group.groupID }); + + const snapToIndex = (index: number) => + bottomSheetRef.current?.snapToIndex(index); + const [open, setOpen] = useState(false); + const [selectedLabel, setSelectedLabel] = useState('Test'); + const filters = Object.values('Temp' || {}).map((filter) => ({ + label: filter[0], + value: filter[0] + })); + const snapPoints = useMemo(() => ['60%'], []); + const bottomSheetRef = useRef(null); + const closeBottomSheet = () => { + if (bottomSheetRef.current) { + bottomSheetRef.current.close(); + } + }; + const renderBackdrop = useCallback( + (props: BottomSheetDefaultBackdropProps) => ( + + ), + [] + ); + + // Filter tasks based on search query in multiple fields and labels + const filteredTasks = tasks?.filter((task) => { + const taskFieldsMatch = [ + 'task_id', + 'task_status', + 'task_type', + 'notes' + ].some((field) => + task?.[field] + ?.toString() + .toLowerCase() + .includes(searchQuery.toLowerCase()) + ); + + return taskFieldsMatch; + }); + + // Filter tasks based on categories + const pastDueTasks = tasks?.filter( + (task) => task?.end_date || '' < String(new Date()) + ); + const inProgressTasks = tasks?.filter( + (task) => task?.task_status === 'PARTIAL' + ); + const inFutureTasks = tasks?.filter( + (task) => (task?.start_date || '') > String(new Date()) + ); + const completeTasks = tasks?.filter( + (task) => task?.task_status === 'COMPLETE' + ); + const incompleteTasks = tasks?.filter( + (task) => task?.task_status === 'INCOMPLETE' + ); + + // Abstraction to render each section + const renderSection = (tasks: Task[], title: string) => { + // Don't render the section if there are no tasks + if (tasks.length === 0) { + return null; + } + return ( + + {title} + {tasks.map((task, index) => { + return ( + { + if (!canPress) return; + navigator.navigate('TaskDisplay', { id: task.task_id }); + }} + > + + + ); + })} + + ); + }; + + return ( + + setCanPress(false)} + onScrollEndDrag={() => setCanPress(true)} + > + + { + setSearchQuery(text); + }} + /> + + + + + + Task List (all tasks of all time) + + {filteredTasks && renderSection(filteredTasks, 'All Tasks')} + {pastDueTasks && renderSection(pastDueTasks, 'Past Due')} + {inProgressTasks && renderSection(inProgressTasks, 'In Progress')} + {inFutureTasks && renderSection(inFutureTasks, 'Future')} + {completeTasks && renderSection(completeTasks, 'Done')} + {incompleteTasks && + renderSection(incompleteTasks, 'Marked as Incomplete')} + + + + + Filter + + + + + + + + ); +} diff --git a/client/screens/timelineEvents.tsx b/client/screens/timelineEvents.tsx deleted file mode 100644 index 3705fdb..0000000 --- a/client/screens/timelineEvents.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { CalendarUtils } from 'react-native-calendars'; - -export const EVENT_COLOR = '#e6add8'; -const today = new Date(); -export const getDate = (offset = 0) => - CalendarUtils.getCalendarDateString( - new Date().setDate(today.getDate() + offset) - ); diff --git a/client/services/group.ts b/client/services/group.ts index 4660a0c..6e26e13 100644 --- a/client/services/group.ts +++ b/client/services/group.ts @@ -4,7 +4,7 @@ import axios from 'axios'; import { GroupRole } from '../types/group'; import { api_url } from './api-links'; -const getUserGroup = async (userId: string): Promise => { +const getUserGroup = async (userId: string): Promise => { const { data } = await axios.get(`${api_url}/group/member/${userId}`); return data; }; @@ -15,20 +15,18 @@ const getGroupRoles = async (groupId: number): Promise => { }; export const useUserGroup = (userId: string) => { - const { data: groupId, isLoading: groupIdIsLoading } = useQuery({ + const { data: userGroupRole, isLoading: userGroupRoleIsLoading } = useQuery({ queryKey: ['groupId', userId], - queryFn: () => getUserGroup(userId), - refetchInterval: 10000 + queryFn: () => getUserGroup(userId) }); - return { groupId, groupIdIsLoading }; + return { userGroupRole, userGroupRoleIsLoading }; }; export const useGroup = (groupId: number) => { const { data: roles, isLoading: rolesAreLoading } = useQuery({ queryKey: ['roles', groupId], - queryFn: () => getGroupRoles(groupId), - refetchInterval: 10000 + queryFn: () => getGroupRoles(groupId) }); return { roles, rolesAreLoading }; diff --git a/client/services/medication.ts b/client/services/medication.ts index 82097b4..fbfb334 100644 --- a/client/services/medication.ts +++ b/client/services/medication.ts @@ -26,8 +26,7 @@ export const useMedication = () => { Medication[] >({ queryKey: ['medList'], // if querying with a value add values here ex. ['medList', {id}] - queryFn: getAllMedications, - refetchInterval: 10000 + queryFn: getAllMedications }); const { mutate: addMedicationMutation } = useMutation({ diff --git a/client/services/notifications.tsx b/client/services/notifications.tsx index 9cc6ee8..e3da6c6 100644 --- a/client/services/notifications.tsx +++ b/client/services/notifications.tsx @@ -14,9 +14,6 @@ import { export async function registerForPushNotificationsAsync() { // checks that this is a physical device if (!isDevice) { - alert( - 'Must use physical device for Push Notifications. Must be ios or android.' - ); return null; } diff --git a/client/services/task.ts b/client/services/task.ts index 289000b..68744b3 100644 --- a/client/services/task.ts +++ b/client/services/task.ts @@ -16,18 +16,31 @@ type TaskQueryParams = { quickTask?: boolean; }; +const getTask = async (taskID: string): Promise => { + const { data } = await axios.get(`${api_url}/tasks/${taskID}`); + return data; +}; + +const getTaskByAssigned = async (userId: string): Promise => { + const { data } = await axios.get( + `${api_url}/tasks/assigned?userIDs=${userId}` + ); + + return data; +}; + const getFilteredTasks = async ( queryParams: TaskQueryParams ): Promise => { + if (!queryParams.groupID) []; const { data } = await axios.get(`${api_url}/tasks/filtered`, { params: queryParams }); - console.log('filtered tasks', data); return data; }; -export const getTaskLabels = async (taskID: string): Promise => { +const getTaskLabels = async (taskID: string): Promise => { const { data } = await axios.get(`${api_url}/tasks/${taskID}/labels`); return data; }; @@ -35,11 +48,42 @@ export const getTaskLabels = async (taskID: string): Promise => { export const useFilteredTasks = (queryParams: TaskQueryParams) => { const { data: tasks, isLoading: tasksIsLoading } = useQuery({ queryKey: ['filteredTaskList', queryParams], - queryFn: () => getFilteredTasks(queryParams), - refetchInterval: 10000 + queryFn: () => getFilteredTasks(queryParams) }); return { tasks, tasksIsLoading }; }; + +export const useTaskByAssigned = (userId: string) => { + const { data: taskByUser, isLoading: taskByUserIsLoading } = useQuery( + { + queryKey: ['tasks', userId], + queryFn: () => getTaskByAssigned(userId) + } + ); + + return { taskByUser, taskByUserIsLoading }; +}; + +export const useTaskById = (taskId: string) => { + const { data: task, isLoading: taskIsLoading } = useQuery({ + queryKey: ['task', taskId], + queryFn: () => getTask(taskId) + }); + + const { data: taskLabels, isLoading: taskLabelsIsLoading } = useQuery< + TaskLabel[] + >({ + queryKey: ['taskLabels', taskId], + queryFn: () => getTaskLabels(taskId) + }); + + return { + task, + taskIsLoading, + taskLabels, + taskLabelsIsLoading + }; +}; diff --git a/client/services/user.ts b/client/services/user.ts index 72f7329..c03490d 100644 --- a/client/services/user.ts +++ b/client/services/user.ts @@ -32,8 +32,7 @@ export const useUser = (userId: string) => { const { data: user, isLoading: userIsLoading } = useQuery({ queryKey: ['user', userId], - queryFn: () => getUser(userId), - refetchInterval: 10000 + queryFn: () => getUser(userId) }); const { mutate: updateUserMutation } = useMutation({ @@ -57,8 +56,7 @@ export const useUsers = (userIds: string[]) => { const { data: users, isLoading: usersAreLoading } = useQuery({ queryKey: ['users', userIds], queryFn: () => getUsers(userIds), - enabled: userIds.length > 0, - refetchInterval: 10000 + enabled: userIds.length > 0 }); return { users, usersAreLoading }; diff --git a/client/types/task.ts b/client/types/task.ts index 1c74528..502c526 100644 --- a/client/types/task.ts +++ b/client/types/task.ts @@ -1,3 +1,5 @@ +import { TypeOfTask } from './type'; + export interface Task { task_id: number; task_title: string; @@ -12,6 +14,7 @@ export interface Task { repeating_end_date?: string | null; quick_task: boolean; task_status: string; - task_type: string; + task_type: TypeOfTask; task_info?: string | null; + [key: string]: string | number | boolean | null | undefined; // Index signature for string indexing } diff --git a/client/types/taskLabel.ts b/client/types/taskLabel.ts new file mode 100644 index 0000000..3a3e99c --- /dev/null +++ b/client/types/taskLabel.ts @@ -0,0 +1,5 @@ +export interface TaskLabel { + task_id: number; + group_id: number; + label_name: string; +} diff --git a/client/types/type.ts b/client/types/type.ts index 985283d..a2c8a3c 100644 --- a/client/types/type.ts +++ b/client/types/type.ts @@ -29,7 +29,14 @@ export enum Category { OTHER = 'Other' } -export const categoryToTypeMap: Record = { +export const TypeToCategoryMap: Record = { + med_mgmt: Category.HEALTH, + dr_appt: Category.HEALTH, + financial: Category.FINANCIAL, + other: Category.OTHER +}; + +export const CategoryToTypeMap: Record = { [Category.ALL]: [], [Category.HEALTH]: [ TypeOfTask.MEDICATION, @@ -64,3 +71,10 @@ export const categoryToTypeMap: Record = { ], [Category.OTHER]: [TypeOfTask.OTHER] }; + +export const TaskTypeDescriptions: Record = { + med_mgmt: 'Medication Management', + dr_appt: 'Doctor Appointment', + financial: 'Financial Task', + other: 'Other Task' +};