diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0b2b7b3..cf5575b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -28,5 +28,3 @@ repos: rev: v3.13.0 hooks: - id: commitizen - - id: commitizen-branch - stages: [push] diff --git a/backend/db/migrations/init.sql b/backend/db/migrations/init.sql index 3f65c2f..b7d4f46 100644 --- a/backend/db/migrations/init.sql +++ b/backend/db/migrations/init.sql @@ -8,7 +8,6 @@ DROP TABLE IF EXISTS label; DROP TABLE IF EXISTS task_labels; DROP TABLE IF EXISTS files; - CREATE TYPE role AS ENUM ('PATIENT', 'PRIMARY', 'SECONDARY'); CREATE TYPE task_assignment_status AS ENUM ('ACCEPTED', 'DECLINED', 'NOTIFIED'); CREATE TYPE task_status AS ENUM ('INCOMPLETE', 'COMPLETE', 'PARTIAL'); @@ -58,7 +57,7 @@ CREATE TABLE IF NOT EXISTS task ( start_date timestamp, end_date timestamp, notes varchar, - repeating BOOLEAN, + repeating BOOLEAN DEFAULT FALSE, repeating_interval varchar, repeating_end_date timestamp, task_status task_status NOT NULL, @@ -95,8 +94,8 @@ CREATE TABLE IF NOT EXISTS task_assignees ( group_id integer NOT NULL, label_name varchar NOT NULL, PRIMARY KEY (task_id, label_name), - FOREIGN KEY (task_id) REFERENCES task (task_id), - FOREIGN KEY (group_id, label_name) REFERENCES label (group_id, label_name) -- NOTE: unsure about label/task_labels table constraints, uncommenting this line is err + FOREIGN KEY (task_id) REFERENCES task (task_id) ON UPDATE CASCADE, + FOREIGN KEY (group_id, label_name) REFERENCES label (group_id, label_name) ON UPDATE CASCADE -- NOTE: unsure about label/task_labels table constraints, uncommenting this line is err ); CREATE TABLE IF NOT EXISTS files ( @@ -113,6 +112,7 @@ CREATE TABLE IF NOT EXISTS files ( FOREIGN KEY (task_id) REFERENCES task (task_id) ); +----------------- SAMPLE DATA :) ----------------------- -- Insert sample data into "medication" table INSERT INTO medication (medication_id, medication_name) @@ -121,19 +121,75 @@ VALUES (2, 'Medication B'), (3, 'Medication C'), (4, 'Medication D'), - (5, 'Medication E'); + (5, 'Medication E') +; --- Insert sample data into "users" table -INSERT INTO users (user_id, first_name, last_name, email, phone, address, pfp_s3_url, device_id, push_notification_enabled) VALUES -('user123', 'John', 'Doe', 'john.doe@example.com', '123-456-7890', '123 Main St, Anytown, USA', 'https://example.com/pfp/user123.jpg', 'device123', TRUE), -('user456', 'Jane', 'Smith', 'jane.smith@example.com', '987-654-3210', '456 Elm St, Anytown, USA', 'https://example.com/pfp/user456.jpg', 'device456', TRUE); - --- Insert sample data into "care_group" table INSERT INTO care_group (group_name, date_created) -VALUES ('Sample Care Group', CURRENT_TIMESTAMP), -('Care-Wallet Group', NOW()); +VALUES + ('Smith Family', NOW()), + ('Johnson Support Network', NOW()), + ('Williams Care Team', NOW()), + ('Brown Medical Group', NOW()), + ('Care-Wallet Group', NOW()) +; + +INSERT INTO users (user_id, first_name, last_name, email, phone, address) +VALUES + ('user1', 'John', 'Smith', 'john.smith@example.com', '123-456-7890', '123 Main St'), + ('user2', 'Jane', 'Doe', 'jane.doe@example.com', '987-654-3210', '456 Elm St'), + ('user3', 'Bob', 'Johnson', 'bob.johnson@example.com', NULL, NULL), + ('user4', 'Emily', 'Garcia', 'emily.garcia@example.com', '555-1212', '789 Oak Ave'), + ('fIoFY26mJnYWH8sNdfuVoxpnVnr1', 'Matt', 'McCoy', '', '', '') +; + +INSERT INTO group_roles (group_id, user_id, role) +VALUES + (1, 'user1', 'PATIENT'), + (1, 'user2', 'PRIMARY'), + (2, 'user3', 'PRIMARY'), + (2, 'user4', 'SECONDARY'), + (3, 'user4', 'PATIENT'), + (4, 'user1', 'SECONDARY'), + (4, 'user3', 'SECONDARY'), + (5, 'fIoFY26mJnYWH8sNdfuVoxpnVnr1', 'PRIMARY') +; + +INSERT INTO task (group_id, created_by, created_date, start_date, end_date, notes, task_status, task_type) +VALUES + (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'), + (2, 'user3', '2024-02-20 23:59:59', '2024-02-10 14:30:00', NULL, 'Schedule doctor appointment', 'INCOMPLETE', 'other'), + (3, 'user4', '2020-02-05 11:00:00', NULL, '2024-02-20 23:59:59', 'Submit insurance claim', 'PARTIAL', 'financial'), + (4, 'user1', '2006-01-02 15:04:05', NULL, NULL, 'Refill water pitcher', 'COMPLETE', 'other') +; --- Insert sample data into "group_roles" table -INSERT INTO group_roles (group_id, user_id, role) VALUES -(1, 'user123', 'PATIENT'), -(1, 'user456', 'PRIMARY'); +INSERT INTO task_assignees (task_id, user_id, assignment_status, assigned_by, assigned_date) +VALUES + (1, 'user1', 'ACCEPTED', 'user2', NOW()), + (2, 'user3', 'NOTIFIED', 'user3', NOW()), + (3, 'user4', 'DECLINED', 'user4', NOW()), + (4, 'user2', 'DECLINED', 'user1', NOW()) +; + +INSERT INTO label (group_id, label_name, label_color) +VALUES + (1, 'Medication', 'blue'), + (2, 'Appointments', 'green'), + (3, 'Financial', 'orange'), + (4, 'Household', 'purple'), + (1, 'Household', 'purple') +; + +INSERT INTO task_labels (task_id, group_id, label_name) +VALUES + (1, 1, 'Medication'), + (2, 2, 'Appointments'), + (3, 3, 'Financial'), + (4, 4, 'Household') +; + +INSERT INTO files (file_id, file_name, group_id, upload_by, upload_date, file_size, task_id) +VALUES + (1, 'Medication list.pdf', 1, 'user2', NOW(), 123456, 1), + (2, 'Insurance form.docx', 3, 'user4', NOW(), 456789, 3), + (3, 'Water pitcher instructions.txt', 4, 'user1', NOW(), 1234, 4) +; diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 3ad2355..6da6f32 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -192,6 +192,165 @@ const docTemplate = `{ } } }, + "/group/{groupId}/labels": { + "get": { + "description": "get all labels for a group given their group id", + "tags": [ + "labels" + ], + "summary": "get labels for a group", + "parameters": [ + { + "type": "integer", + "description": "the group id to get labels for", + "name": "groupId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Label" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "create a new label for a group", + "tags": [ + "labels" + ], + "summary": "Create A New Label", + "parameters": [ + { + "type": "string", + "description": "Group to create label for", + "name": "groupId", + "in": "path", + "required": true + }, + { + "description": "Label creation data", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Label" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/group/{groupId}/labels/{lname}": { + "delete": { + "description": "delete a label", + "tags": [ + "labels" + ], + "summary": "Delete A Label", + "parameters": [ + { + "type": "string", + "description": "Group to delete label from", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of label to delete", + "name": "lname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "description": "edit a label", + "tags": [ + "labels" + ], + "summary": "Edit A Label", + "parameters": [ + { + "type": "string", + "description": "Group of label to edit", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of label to edit", + "name": "lname", + "in": "path", + "required": true + }, + { + "description": "Label edit data", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Label" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, "/group/{groupId}/roles": { "get": { "description": "get all group members for a group given group id from the db", @@ -272,6 +431,307 @@ const docTemplate = `{ } } } + }, + "/tasks/assigned": { + "get": { + "description": "get tasks assigned to given users", + "tags": [ + "tasks" + ], + "summary": "Get Tasks Assigned To Given Users", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "userIDs", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/filtered": { + "get": { + "description": "get filtered tasks", + "tags": [ + "tasks" + ], + "summary": "Get Filtered Tasks", + "parameters": [ + { + "type": "string", + "name": "createdBy", + "in": "query" + }, + { + "type": "string", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "name": "groupID", + "in": "query" + }, + { + "type": "string", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "name": "taskID", + "in": "query" + }, + { + "type": "string", + "name": "taskStatus", + "in": "query" + }, + { + "type": "string", + "name": "taskType", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/{tid}/assign": { + "post": { + "description": "assign users to task", + "tags": [ + "tasks" + ], + "summary": "Assign Users To Task", + "parameters": [ + { + "type": "string", + "description": "Task ID to assign users to", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "Users to assign to task and assignee", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.Assignment" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskUser" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/{tid}/labels": { + "get": { + "description": "get a tasks labels given the task id", + "tags": [ + "task labels" + ], + "summary": "get a tasks labels", + "parameters": [ + { + "type": "string", + "description": "the task id to get labels for", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Task_Label" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "add a label to a task given the task id, group id, and label name", + "tags": [ + "task labels" + ], + "summary": "add a label to a task", + "parameters": [ + { + "type": "integer", + "description": "the task id to add the label to", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "The label data to add to the task", + "name": "requestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/task_labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Task_Label" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "remove a label from a task given the task id, group id, and label name", + "tags": [ + "task labels" + ], + "summary": "remove a label from a task", + "parameters": [ + { + "type": "integer", + "description": "the task id to get labels for", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "The label data to remove from the task", + "name": "requestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/task_labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/{tid}/remove": { + "delete": { + "description": "remove users from task", + "tags": [ + "tasks" + ], + "summary": "Remove Users From Task", + "parameters": [ + { + "type": "string", + "description": "Task ID to remove users from", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "Users to remove from task", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.Removal" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskUser" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { @@ -286,6 +746,17 @@ const docTemplate = `{ } } }, + "labels.LabelData": { + "type": "object", + "properties": { + "label_color": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, "models.CareGroup": { "type": "object", "properties": { @@ -340,6 +811,20 @@ const docTemplate = `{ } } }, + "models.Label": { + "type": "object", + "properties": { + "group_id": { + "type": "integer" + }, + "label_color": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, "models.Medication": { "type": "object", "properties": { @@ -363,6 +848,112 @@ const docTemplate = `{ "RolePrimary", "RoleSecondary" ] + }, + "models.Task": { + "type": "object", + "properties": { + "created_by": { + "description": "User ID", + "type": "string" + }, + "created_date": { + "type": "string" + }, + "end_date": { + "type": "string" + }, + "group_id": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "repeating": { + "type": "boolean" + }, + "repeating_end_date": { + "type": "string" + }, + "repeating_interval": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "task_id": { + "type": "integer" + }, + "task_info": { + "type": "string" + }, + "task_status": { + "type": "string" + }, + "task_type": { + "type": "string" + } + } + }, + "models.TaskUser": { + "type": "object", + "properties": { + "taskID": { + "type": "integer" + }, + "userID": { + "type": "string" + } + } + }, + "models.Task_Label": { + "type": "object", + "properties": { + "group_id": { + "type": "integer" + }, + "label_name": { + "type": "string" + }, + "task_id": { + "type": "integer" + } + } + }, + "task_labels.LabelData": { + "type": "object", + "properties": { + "group_id": { + "type": "integer" + }, + "label_name": { + "type": "string" + } + } + }, + "tasks.Assignment": { + "type": "object", + "properties": { + "assigner": { + "type": "string" + }, + "userIDs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "tasks.Removal": { + "type": "object", + "properties": { + "userIDs": { + "type": "array", + "items": { + "type": "string" + } + } + } } } }` diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 5e21fd8..40f2229 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -185,6 +185,165 @@ } } }, + "/group/{groupId}/labels": { + "get": { + "description": "get all labels for a group given their group id", + "tags": [ + "labels" + ], + "summary": "get labels for a group", + "parameters": [ + { + "type": "integer", + "description": "the group id to get labels for", + "name": "groupId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Label" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "create a new label for a group", + "tags": [ + "labels" + ], + "summary": "Create A New Label", + "parameters": [ + { + "type": "string", + "description": "Group to create label for", + "name": "groupId", + "in": "path", + "required": true + }, + { + "description": "Label creation data", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Label" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/group/{groupId}/labels/{lname}": { + "delete": { + "description": "delete a label", + "tags": [ + "labels" + ], + "summary": "Delete A Label", + "parameters": [ + { + "type": "string", + "description": "Group to delete label from", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of label to delete", + "name": "lname", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "patch": { + "description": "edit a label", + "tags": [ + "labels" + ], + "summary": "Edit A Label", + "parameters": [ + { + "type": "string", + "description": "Group of label to edit", + "name": "groupId", + "in": "path", + "required": true + }, + { + "type": "string", + "description": "Name of label to edit", + "name": "lname", + "in": "path", + "required": true + }, + { + "description": "Label edit data", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Label" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, "/group/{groupId}/roles": { "get": { "description": "get all group members for a group given group id from the db", @@ -265,6 +424,307 @@ } } } + }, + "/tasks/assigned": { + "get": { + "description": "get tasks assigned to given users", + "tags": [ + "tasks" + ], + "summary": "Get Tasks Assigned To Given Users", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "name": "userIDs", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/filtered": { + "get": { + "description": "get filtered tasks", + "tags": [ + "tasks" + ], + "summary": "Get Filtered Tasks", + "parameters": [ + { + "type": "string", + "name": "createdBy", + "in": "query" + }, + { + "type": "string", + "name": "endDate", + "in": "query" + }, + { + "type": "string", + "name": "groupID", + "in": "query" + }, + { + "type": "string", + "name": "startDate", + "in": "query" + }, + { + "type": "string", + "name": "taskID", + "in": "query" + }, + { + "type": "string", + "name": "taskStatus", + "in": "query" + }, + { + "type": "string", + "name": "taskType", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Task" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/{tid}/assign": { + "post": { + "description": "assign users to task", + "tags": [ + "tasks" + ], + "summary": "Assign Users To Task", + "parameters": [ + { + "type": "string", + "description": "Task ID to assign users to", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "Users to assign to task and assignee", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.Assignment" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskUser" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/{tid}/labels": { + "get": { + "description": "get a tasks labels given the task id", + "tags": [ + "task labels" + ], + "summary": "get a tasks labels", + "parameters": [ + { + "type": "string", + "description": "the task id to get labels for", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.Task_Label" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "add a label to a task given the task id, group id, and label name", + "tags": [ + "task labels" + ], + "summary": "add a label to a task", + "parameters": [ + { + "type": "integer", + "description": "the task id to add the label to", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "The label data to add to the task", + "name": "requestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/task_labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Task_Label" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "delete": { + "description": "remove a label from a task given the task id, group id, and label name", + "tags": [ + "task labels" + ], + "summary": "remove a label from a task", + "parameters": [ + { + "type": "integer", + "description": "the task id to get labels for", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "The label data to remove from the task", + "name": "requestBody", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/task_labels.LabelData" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "string" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/tasks/{tid}/remove": { + "delete": { + "description": "remove users from task", + "tags": [ + "tasks" + ], + "summary": "Remove Users From Task", + "parameters": [ + { + "type": "string", + "description": "Task ID to remove users from", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "Users to remove from task", + "name": "_", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.Removal" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.TaskUser" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { @@ -279,6 +739,17 @@ } } }, + "labels.LabelData": { + "type": "object", + "properties": { + "label_color": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, "models.CareGroup": { "type": "object", "properties": { @@ -333,6 +804,20 @@ } } }, + "models.Label": { + "type": "object", + "properties": { + "group_id": { + "type": "integer" + }, + "label_color": { + "type": "string" + }, + "label_name": { + "type": "string" + } + } + }, "models.Medication": { "type": "object", "properties": { @@ -356,6 +841,112 @@ "RolePrimary", "RoleSecondary" ] + }, + "models.Task": { + "type": "object", + "properties": { + "created_by": { + "description": "User ID", + "type": "string" + }, + "created_date": { + "type": "string" + }, + "end_date": { + "type": "string" + }, + "group_id": { + "type": "integer" + }, + "notes": { + "type": "string" + }, + "repeating": { + "type": "boolean" + }, + "repeating_end_date": { + "type": "string" + }, + "repeating_interval": { + "type": "string" + }, + "start_date": { + "type": "string" + }, + "task_id": { + "type": "integer" + }, + "task_info": { + "type": "string" + }, + "task_status": { + "type": "string" + }, + "task_type": { + "type": "string" + } + } + }, + "models.TaskUser": { + "type": "object", + "properties": { + "taskID": { + "type": "integer" + }, + "userID": { + "type": "string" + } + } + }, + "models.Task_Label": { + "type": "object", + "properties": { + "group_id": { + "type": "integer" + }, + "label_name": { + "type": "string" + }, + "task_id": { + "type": "integer" + } + } + }, + "task_labels.LabelData": { + "type": "object", + "properties": { + "group_id": { + "type": "integer" + }, + "label_name": { + "type": "string" + } + } + }, + "tasks.Assignment": { + "type": "object", + "properties": { + "assigner": { + "type": "string" + }, + "userIDs": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "tasks.Removal": { + "type": "object", + "properties": { + "userIDs": { + "type": "array", + "items": { + "type": "string" + } + } + } } } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index f999257..2617a4b 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -7,6 +7,13 @@ definitions: user_id: type: string type: object + labels.LabelData: + properties: + label_color: + type: string + label_name: + type: string + type: object models.CareGroup: properties: date_created: @@ -42,6 +49,15 @@ definitions: user_id: type: string type: object + models.Label: + properties: + group_id: + type: integer + label_color: + type: string + label_name: + type: string + type: object models.Medication: properties: medication_id: @@ -59,6 +75,75 @@ definitions: - RolePatient - RolePrimary - RoleSecondary + models.Task: + properties: + created_by: + description: User ID + type: string + created_date: + type: string + end_date: + type: string + group_id: + type: integer + notes: + type: string + repeating: + type: boolean + repeating_end_date: + type: string + repeating_interval: + type: string + start_date: + type: string + task_id: + type: integer + task_info: + type: string + task_status: + type: string + task_type: + type: string + type: object + models.Task_Label: + properties: + group_id: + type: integer + label_name: + type: string + task_id: + type: integer + type: object + models.TaskUser: + properties: + taskID: + type: integer + userID: + type: string + type: object + task_labels.LabelData: + properties: + group_id: + type: integer + label_name: + type: string + type: object + tasks.Assignment: + properties: + assigner: + type: string + userIDs: + items: + type: string + type: array + type: object + tasks.Removal: + properties: + userIDs: + items: + type: string + type: array + type: object info: contact: {} description: This is an API for the Care-Wallet App. @@ -144,6 +229,112 @@ paths: summary: Adds a user to a care group tags: - group + /group/{groupId}/labels: + get: + description: get all labels for a group given their group id + parameters: + - description: the group id to get labels for + in: path + name: groupId + required: true + type: integer + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Label' + type: array + "400": + description: Bad Request + schema: + type: string + summary: get labels for a group + tags: + - labels + post: + description: create a new label for a group + parameters: + - description: Group to create label for + in: path + name: groupId + required: true + type: string + - description: Label creation data + in: body + name: _ + required: true + schema: + $ref: '#/definitions/labels.LabelData' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Label' + "400": + description: Bad Request + schema: + type: string + summary: Create A New Label + tags: + - labels + /group/{groupId}/labels/{lname}: + delete: + description: delete a label + parameters: + - description: Group to delete label from + in: path + name: groupId + required: true + type: string + - description: Name of label to delete + in: path + name: lname + required: true + type: string + responses: + "200": + description: OK + schema: + type: string + "400": + description: Bad Request + schema: + type: string + summary: Delete A Label + tags: + - labels + patch: + description: edit a label + parameters: + - description: Group of label to edit + in: path + name: groupId + required: true + type: string + - description: Name of label to edit + in: path + name: lname + required: true + type: string + - description: Label edit data + in: body + name: _ + required: true + schema: + $ref: '#/definitions/labels.LabelData' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Label' + "400": + description: Bad Request + schema: + type: string + summary: Edit A Label + tags: + - labels /group/{groupId}/roles: get: description: get all group members for a group given group id from the db @@ -235,4 +426,201 @@ paths: summary: add a medication tags: - medications + /tasks/{tid}/assign: + post: + description: assign users to task + parameters: + - description: Task ID to assign users to + in: path + name: tid + required: true + type: string + - description: Users to assign to task and assignee + in: body + name: _ + required: true + schema: + $ref: '#/definitions/tasks.Assignment' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.TaskUser' + type: array + "400": + description: Bad Request + schema: + type: string + summary: Assign Users To Task + tags: + - tasks + /tasks/{tid}/labels: + delete: + description: remove a label from a task given the task id, group id, and label + name + parameters: + - description: the task id to get labels for + in: path + name: tid + required: true + type: integer + - description: The label data to remove from the task + in: body + name: requestBody + required: true + schema: + $ref: '#/definitions/task_labels.LabelData' + responses: + "200": + description: OK + schema: + type: string + "400": + description: Bad Request + schema: + type: string + summary: remove a label from a task + tags: + - task labels + get: + description: get a tasks labels given the task id + parameters: + - description: the task id to get labels for + in: path + name: tid + required: true + type: string + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Task_Label' + type: array + "400": + description: Bad Request + schema: + type: string + summary: get a tasks labels + tags: + - task labels + post: + description: add a label to a task given the task id, group id, and label name + parameters: + - description: the task id to add the label to + in: path + name: tid + required: true + type: integer + - description: The label data to add to the task + in: body + name: requestBody + required: true + schema: + $ref: '#/definitions/task_labels.LabelData' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Task_Label' + "400": + description: Bad Request + schema: + type: string + summary: add a label to a task + tags: + - task labels + /tasks/{tid}/remove: + delete: + description: remove users from task + parameters: + - description: Task ID to remove users from + in: path + name: tid + required: true + type: string + - description: Users to remove from task + in: body + name: _ + required: true + schema: + $ref: '#/definitions/tasks.Removal' + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.TaskUser' + type: array + "400": + description: Bad Request + schema: + type: string + summary: Remove Users From Task + tags: + - tasks + /tasks/assigned: + get: + description: get tasks assigned to given users + parameters: + - collectionFormat: csv + in: query + items: + type: string + name: userIDs + type: array + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Task' + type: array + "400": + description: Bad Request + schema: + type: string + summary: Get Tasks Assigned To Given Users + tags: + - tasks + /tasks/filtered: + get: + description: get filtered tasks + parameters: + - in: query + name: createdBy + type: string + - in: query + name: endDate + type: string + - in: query + name: groupID + type: string + - in: query + name: startDate + type: string + - in: query + name: taskID + type: string + - in: query + name: taskStatus + type: string + - in: query + name: taskType + type: string + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.Task' + type: array + "400": + description: Bad Request + schema: + type: string + summary: Get Filtered Tasks + tags: + - tasks swagger: "2.0" diff --git a/backend/main.go b/backend/main.go index 8327913..4b25ed5 100644 --- a/backend/main.go +++ b/backend/main.go @@ -7,7 +7,10 @@ import ( "carewallet/schema/files" groupRoles "carewallet/schema/group-roles" "carewallet/schema/groups" + "carewallet/schema/labels" "carewallet/schema/medication" + "carewallet/schema/task_labels" + "carewallet/schema/tasks" "fmt" "os" @@ -40,11 +43,20 @@ func main() { v1 := r.Group("/") { medication.GetMedicationGroup(v1, &medication.PgModel{Conn: conn}) - files.GetFileGroup(v1, &files.PgModel{Conn: conn}) + + files.FileGroup(v1, &files.PgModel{Conn: conn}) + group := v1.Group("group") { - groups.GetCareGroups(group, &groups.PgModel{Conn: conn}) - groupRoles.GetGroupRolesGroup(group, &groupRoles.PgModel{Conn: conn}) + groups.CareGroups(group, &groups.PgModel{Conn: conn}) + groupRoles.GroupRolesGroup(group, &groupRoles.PgModel{Conn: conn}) + labels.LabelGroup(group, &labels.PgModel{Conn: conn}) + } + + task := v1.Group("tasks") + { + tasks.TaskGroup(task, &tasks.PgModel{Conn: conn}) + task_labels.TaskGroup(task, &task_labels.PgModel{Conn: conn}) } } diff --git a/backend/models/label.go b/backend/models/label.go new file mode 100644 index 0000000..464f0f6 --- /dev/null +++ b/backend/models/label.go @@ -0,0 +1,7 @@ +package models + +type Label struct { + GroupID int `json:"group_id"` + LabelName string `json:"label_name"` + LabelColor string `json:"label_color"` +} diff --git a/backend/models/task.go b/backend/models/task.go new file mode 100644 index 0000000..4f700c2 --- /dev/null +++ b/backend/models/task.go @@ -0,0 +1,21 @@ +package models + +import ( + "time" +) + +type Task struct { + TaskID int `json:"task_id"` + GroupID int `json:"group_id"` + CreatedBy string `json:"created_by"` // User ID + CreatedDate time.Time `json:"created_date"` + StartDate *time.Time `json:"start_date"` + EndDate *time.Time `json:"end_date"` + Notes *string `json:"notes"` + Repeating bool `json:"repeating"` + RepeatingInterval *string `json:"repeating_interval"` + RepeatingEndDate *time.Time `json:"repeating_end_date"` + TaskStatus string `json:"task_status"` + TaskType string `json:"task_type"` + TaskInfo *string `json:"task_info"` +} diff --git a/backend/models/task_label.go b/backend/models/task_label.go new file mode 100644 index 0000000..9d40f18 --- /dev/null +++ b/backend/models/task_label.go @@ -0,0 +1,7 @@ +package models + +type Task_Label struct { + TaskId int `json:"task_id"` + GroupId int `json:"group_id"` + LabelName string `json:"label_name"` +} diff --git a/backend/models/task_user.go b/backend/models/task_user.go new file mode 100644 index 0000000..105a996 --- /dev/null +++ b/backend/models/task_user.go @@ -0,0 +1,6 @@ +package models + +type TaskUser struct { + TaskID int + UserID string +} diff --git a/backend/schema/files/routes.go b/backend/schema/files/routes.go index 0aaafb4..fe98610 100644 --- a/backend/schema/files/routes.go +++ b/backend/schema/files/routes.go @@ -13,7 +13,7 @@ type PgModel struct { Conn *pgx.Conn } -func GetFileGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { +func FileGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { files := v1.Group("files") { diff --git a/backend/schema/group-roles/group_roles_test.go b/backend/schema/group-roles/group_roles_test.go index f3c8bbe..b23783d 100644 --- a/backend/schema/group-roles/group_roles_test.go +++ b/backend/schema/group-roles/group_roles_test.go @@ -37,12 +37,12 @@ func TestGetGroupRoles(t *testing.T) { v1 := router.Group("/group") { - GetGroupRolesGroup(v1, &controller) + GroupRolesGroup(v1, &controller) } t.Run("TestGetGroupRoles", func(t *testing.T) { w := httptest.NewRecorder() - req, _ := http.NewRequest("GET", "/group/member/user123", nil) + req, _ := http.NewRequest("GET", "/group/member/user1", nil) router.ServeHTTP(w, req) // Check for HTTP Status OK (200) @@ -58,7 +58,7 @@ func TestGetGroupRoles(t *testing.T) { } // Define the expected group - expectedGroup := models.GroupRole{GroupID: 1, Role: "PATIENT", UserID: "user123"} + expectedGroup := models.GroupRole{GroupID: 1, Role: "PATIENT", UserID: "user1"} if expectedGroup != responseGroup { t.Errorf("Expected group ID: %+v, Actual group ID: %+v", expectedGroup, responseGroup) diff --git a/backend/schema/group-roles/routes.go b/backend/schema/group-roles/routes.go index 3b2178a..3138d22 100644 --- a/backend/schema/group-roles/routes.go +++ b/backend/schema/group-roles/routes.go @@ -13,7 +13,7 @@ type PgModel struct { } // groupRoles.go file -func GetGroupRolesGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { +func GroupRolesGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { groupRoles := v1.Group("") { groupRoles.GET("/:groupId/roles", c.GetGroupRoles) diff --git a/backend/schema/groups/groups_test.go b/backend/schema/groups/groups_test.go index bbce5c9..75bc565 100644 --- a/backend/schema/groups/groups_test.go +++ b/backend/schema/groups/groups_test.go @@ -39,7 +39,7 @@ func TestGroupRoutes(t *testing.T) { v1 := router.Group("/group") { - GetCareGroups(v1, &controller) + CareGroups(v1, &controller) } // test to get group members @@ -89,7 +89,7 @@ func TestGroupRoutes(t *testing.T) { } // Define the expected users - expectedGroupID := 3 + expectedGroupID := 6 if expectedGroupID != responseGroupID { t.Error("Result was not correct") @@ -100,7 +100,7 @@ func TestGroupRoutes(t *testing.T) { // test to add a user to a group t.Run("TestAddUser", func(t *testing.T) { postRequest := GroupMember{ - UserId: "user123", + UserId: "user3", Role: "PATIENT", } diff --git a/backend/schema/groups/routes.go b/backend/schema/groups/routes.go index 63303b4..0659c15 100644 --- a/backend/schema/groups/routes.go +++ b/backend/schema/groups/routes.go @@ -13,7 +13,7 @@ type PgModel struct { Conn *pgx.Conn } -func GetCareGroups(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { +func CareGroups(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { careGroups := v1.Group("") { careGroups.POST("/create/:groupName", c.CreateCareGroups) diff --git a/backend/schema/labels/label_test.go b/backend/schema/labels/label_test.go new file mode 100644 index 0000000..72cf287 --- /dev/null +++ b/backend/schema/labels/label_test.go @@ -0,0 +1,159 @@ +package labels + +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 TestLabelGroup(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("/group") + { + LabelGroup(v1, &controller) + } + + t.Run("TestGetLabelsByGroup", func(t *testing.T) { + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/group/1/labels", nil) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to get labels by group.") + } + + var getResponse []models.Label + err = json.Unmarshal(w.Body.Bytes(), &getResponse) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + expectedResponse := []models.Label{ + { + GroupID: 1, + LabelName: "Medication", + LabelColor: "blue", + }, + { + GroupID: 1, + LabelName: "Household", + LabelColor: "purple", + }, + } + + if !reflect.DeepEqual(expectedResponse, getResponse) { + t.Error("Result was not correct") + } + + }) + + t.Run("TestCreateNewLabel", func(t *testing.T) { + postRequest := LabelData{ + LabelName: "Office", + LabelColor: "Orange", + } + + requestJSON, err := json.Marshal(postRequest) + if err != nil { + t.Error("Failed to marshal remove request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/group/2/labels", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to create new label.") + } + + var postResponse models.Label + err = json.Unmarshal(w.Body.Bytes(), &postResponse) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + expectedResponse := models.Label{ + GroupID: 2, + LabelName: "Office", + LabelColor: "Orange", + } + + if !reflect.DeepEqual(expectedResponse, postResponse) { + t.Error("Result was not correct") + } + }) + + t.Run("TestDeleteLabel", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/group/2/labels/Appointment", nil) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to delete label.") + } + }) + + t.Run("TestEditLabel", func(t *testing.T) { + postRequest := LabelData{ + LabelName: "Family", + LabelColor: "Yellow", + } + + requestJSON, err := json.Marshal(postRequest) + if err != nil { + t.Error("Failed to marshal remove request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("PATCH", "/group/4/labels/Household", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to edit label.") + } + + var postResponse models.Label + err = json.Unmarshal(w.Body.Bytes(), &postResponse) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + expectedResponse := models.Label{ + GroupID: 4, + LabelName: "Family", + LabelColor: "Yellow", + } + + if !reflect.DeepEqual(expectedResponse, postResponse) { + t.Error("Result was not correct") + } + }) +} diff --git a/backend/schema/labels/routes.go b/backend/schema/labels/routes.go new file mode 100644 index 0000000..1ff93df --- /dev/null +++ b/backend/schema/labels/routes.go @@ -0,0 +1,146 @@ +package labels + +import ( + "fmt" + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx" +) + +type PgModel struct { + Conn *pgx.Conn +} + +func LabelGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { + + labels := v1.Group(":groupId/labels") + { + labels.POST("", c.CreateNewLabel) + labels.GET("", c.GetLabelsByGroup) + labels.DELETE(":lname", c.DeleteLabel) + labels.PATCH(":lname", c.EditLabel) + } + + return labels +} + +// GetLabelsByGroup godoc +// +// @summary get labels for a group +// @description get all labels for a group given their group id +// @tags labels +// +// @param groupId path int true "the group id to get labels for" +// +// @success 200 {array} models.Label +// @failure 400 {object} string +// @router /group/{groupId}/labels [GET] +func (pg *PgModel) GetLabelsByGroup(c *gin.Context) { + group_id := c.Param("groupId") + + labels, err := GetLabelsByGroupFromDB(pg.Conn, group_id) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, labels) +} + +type LabelData struct { + LabelName string `json:"label_name"` + LabelColor string `json:"label_color"` +} + +// CreateNewLabel godoc +// +// @summary Create A New Label +// @description create a new label for a group +// @tags labels +// +// @param groupId path string true "Group to create label for" +// @param _ body LabelData true "Label creation data" +// +// @success 200 {object} models.Label +// @failure 400 {object} string +// @router /group/{groupId}/labels [POST] +func (pg *PgModel) CreateNewLabel(c *gin.Context) { + var requestBody LabelData + group_id := c.Param("groupId") + + if err := c.BindJSON(&requestBody); err != nil { + fmt.Println("Error binding JSON: ", err.Error()) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + id, _ := strconv.Atoi(group_id) + + label, err := CreateNewLabelInDB(pg.Conn, id, requestBody) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, label) +} + +// DeleteLabel godoc +// +// @summary Delete A Label +// @description delete a label +// @tags labels +// +// @param groupId path string true "Group to delete label from" +// @param lname path string true "Name of label to delete" +// +// @success 200 {object} string +// @failure 400 {object} string +// @router /group/{groupId}/labels/{lname} [DELETE] +func (pg *PgModel) DeleteLabel(c *gin.Context) { + group_id := c.Param("groupId") + label_name := c.Param("lname") + + err := DeleteLabelFromDB(pg.Conn, group_id, label_name) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, nil) +} + +// EditLabel godoc +// +// @summary Edit A Label +// @description edit a label +// @tags labels +// +// @param groupId path string true "Group of label to edit" +// @param lname path string true "Name of label to edit" +// @param _ body LabelData true "Label edit data" +// +// @success 200 {object} models.Label +// @failure 400 {object} string +// @router /group/{groupId}/labels/{lname} [PATCH] +func (pg *PgModel) EditLabel(c *gin.Context) { + group_id := c.Param("groupId") + label_name := c.Param("lname") + + var requestBody LabelData + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + label, err := EditLabelInDB(pg.Conn, group_id, label_name, requestBody) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, label) +} diff --git a/backend/schema/labels/transactions.go b/backend/schema/labels/transactions.go new file mode 100644 index 0000000..7648c0c --- /dev/null +++ b/backend/schema/labels/transactions.go @@ -0,0 +1,101 @@ +package labels + +import ( + "carewallet/models" + "strconv" + + "github.com/jackc/pgx" +) + +func GetLabelsByGroupFromDB(pool *pgx.Conn, groupID string) ([]models.Label, error) { + groupIDInt, err := strconv.Atoi(groupID) + if err != nil { + return nil, err + } + + rows, err := pool.Query("SELECT label_name, label_color FROM label WHERE group_id = $1", groupIDInt) + if err != nil { + return nil, err + } + + defer rows.Close() + + var results []models.Label + + for rows.Next() { + label := models.Label{} + err := rows.Scan(&label.LabelName, &label.LabelColor) + if err != nil { + return nil, err + } + label.GroupID = groupIDInt + results = append(results, label) + } + + return results, nil + +} + +func CreateNewLabelInDB(pool *pgx.Conn, groupID int, requestBody LabelData) (models.Label, error) { + labelName := requestBody.LabelName + labelColor := requestBody.LabelColor + + _, err := pool.Exec("INSERT INTO label (group_id, label_name, label_color) VALUES ($1, $2, $3)", groupID, labelName, labelColor) + + if err != nil { + print(err.Error()) + return models.Label{}, err + } + + label := models.Label{ + GroupID: groupID, + LabelName: labelName, + LabelColor: labelColor, + } + return label, nil +} + +func DeleteLabelFromDB(pool *pgx.Conn, groupID string, labelName string) error { + groupIDInt, err := strconv.Atoi(groupID) + if err != nil { + return err + } + + _, err = pool.Exec("DELETE FROM label WHERE group_id = $1 AND label_name = $2", groupIDInt, labelName) + if err != nil { + return err + } + + return nil +} + +func EditLabelInDB(pool *pgx.Conn, groupID string, labelName string, data LabelData) (models.Label, error) { + groupIDInt, err := strconv.Atoi(groupID) + if err != nil { + return models.Label{}, err + } + + _, err = pool.Exec("UPDATE label SET label_color = $1, label_name = $2 WHERE group_id = $3 AND label_name = $4", data.LabelColor, data.LabelName, groupIDInt, labelName) + if err != nil { + print(err.Error()) + return models.Label{}, err + } + + // Is there a better way to do this when we don't know which fields are being edited? + editedName := data.LabelName + if editedName == "" { + editedName = labelName + } + + var label = models.Label{ + GroupID: groupIDInt, + LabelName: editedName, + } + + err = pool.QueryRow("SELECT label_color FROM label WHERE group_id = $1 AND label_name = $2", groupIDInt, editedName).Scan(&label.LabelColor) + if err != nil { + return models.Label{}, err + } + + return label, nil +} diff --git a/backend/schema/task_labels/routes.go b/backend/schema/task_labels/routes.go new file mode 100644 index 0000000..79e7b17 --- /dev/null +++ b/backend/schema/task_labels/routes.go @@ -0,0 +1,111 @@ +package task_labels + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx" +) + +type PgModel struct { + Conn *pgx.Conn +} + +func TaskGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { + + tasks := v1.Group(":tid/labels") + { + tasks.POST("", c.AddLabelToTask) + tasks.DELETE("", c.RemoveLabelFromTask) + tasks.GET("", c.GetLabelsByTask) + } + + return tasks +} + +// GetLabelsByTask godoc +// +// @summary get a tasks labels +// @description get a tasks labels given the task id +// @tags task labels +// +// @param tid path string true "the task id to get labels for" +// +// @success 200 {array} models.Task_Label +// @failure 400 {object} string +// @router /tasks/{tid}/labels [GET] +func (pg *PgModel) GetLabelsByTask(c *gin.Context) { + taskLabels, err := GetLabelsByTaskInDB(pg.Conn, c.Param("tid")) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, taskLabels) +} + +type LabelData struct { + GroupID int `json:"group_id"` + LabelName string `json:"label_name"` +} + +// AddLabelToTask godoc +// +// @summary add a label to a task +// @description add a label to a task given the task id, group id, and label name +// @tags task labels +// +// @param tid path int true "the task id to add the label to" +// @param requestBody body LabelData true "The label data to add to the task" +// +// @success 200 {object} models.Task_Label +// @failure 400 {object} string +// @router /tasks/{tid}/labels [POST] +func (pg *PgModel) AddLabelToTask(c *gin.Context) { + var requestBody LabelData + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + updatedTaskLabel, err := AddLabelToTaskInDB(pg.Conn, requestBody, c.Param("tid")) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, updatedTaskLabel) +} + +// RemoveLabelFromTask godoc +// +// @summary remove a label from a task +// @description remove a label from a task given the task id, group id, and label name +// @tags task labels +// +// @param tid path int true "the task id to get labels for" +// @param requestBody body LabelData true "The label data to remove from the task" +// +// @success 200 {object} string +// @failure 400 {object} string +// @router /tasks/{tid}/labels [DELETE] +func (pg *PgModel) RemoveLabelFromTask(c *gin.Context) { + var requestBody LabelData + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + err := RemoveLabelFromTaskInDB(pg.Conn, requestBody, c.Param("tid")) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, "") +} diff --git a/backend/schema/task_labels/task_labels_test.go b/backend/schema/task_labels/task_labels_test.go new file mode 100644 index 0000000..27bcdae --- /dev/null +++ b/backend/schema/task_labels/task_labels_test.go @@ -0,0 +1,124 @@ +package task_labels + +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 TestTaskLabelsGroup(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("/tasks") + { + TaskGroup(v1, &controller) + } + + t.Run("TestGetTaskLabels", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/tasks/1/labels", nil) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to get labels by task.") + } + + var getResponse []LabelData + err = json.Unmarshal(w.Body.Bytes(), &getResponse) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + expectedResponse := []LabelData{ + { + GroupID: 1, + LabelName: "Medication", + }, + } + + if !reflect.DeepEqual(expectedResponse, getResponse) { + t.Error("Failed to get the expected response") + } + }) + + t.Run("TestAddTaskLabels", func(t *testing.T) { + postRequest := LabelData{ + GroupID: 1, + LabelName: "Household", + } + + requestJSON, err := json.Marshal(postRequest) + if err != nil { + t.Error("Failed to marshal remove request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/tasks/1/labels", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to assign new label.") + } + + var postResponse models.Task_Label + err = json.Unmarshal(w.Body.Bytes(), &postResponse) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + expectedResponse := models.Task_Label{ + GroupId: 1, + TaskId: 1, + LabelName: "Household", + } + + if !reflect.DeepEqual(expectedResponse, postResponse) { + t.Error("Result was not correct") + } + }) + + t.Run("TestRemoveTaskLabels", func(t *testing.T) { + postRequest := LabelData{ + GroupID: 1, + LabelName: "Medication", + } + + requestJSON, err := json.Marshal(postRequest) + if err != nil { + t.Error("Failed to marshal remove request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/tasks/1/labels", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to remove label.") + } + }) +} diff --git a/backend/schema/task_labels/transactions.go b/backend/schema/task_labels/transactions.go new file mode 100644 index 0000000..0864ed4 --- /dev/null +++ b/backend/schema/task_labels/transactions.go @@ -0,0 +1,58 @@ +package task_labels + +import ( + "carewallet/models" + + "github.com/jackc/pgx" +) + +func GetLabelsByTaskInDB(conn *pgx.Conn, taskId string) ([]models.Task_Label, error) { + rows, err := conn.Query("SELECT * FROM task_labels WHERE task_id = $1", taskId) + + if err != nil { + print(err, "error selecting tasks by query") + return nil, err + } + + defer rows.Close() + + var results []models.Task_Label + + for rows.Next() { + task := models.Task_Label{} + err := rows.Scan(&task.GroupId, &task.TaskId, &task.LabelName) + + if err != nil { + print(err, "error scanning tasks by query") + return nil, err + } + + results = append(results, task) + } + + return results, nil +} + +func AddLabelToTaskInDB(conn *pgx.Conn, requestBody LabelData, taskid string) (models.Task_Label, error) { + var task_label models.Task_Label + err := conn.QueryRow("INSERT INTO task_labels (task_id, group_id, label_name) VALUES ($1, $2, $3) RETURNING *;", + taskid, requestBody.GroupID, requestBody.LabelName).Scan(&task_label.TaskId, &task_label.GroupId, &task_label.LabelName) + + if err != nil { + print(err.Error()) + return models.Task_Label{}, err + } + + return task_label, nil +} + +func RemoveLabelFromTaskInDB(conn *pgx.Conn, requestBody LabelData, taskId string) error { + _, err := conn.Exec("DELETE FROM task_labels WHERE task_id = $1 AND group_id = $2 AND label_name = $3", taskId, requestBody.GroupID, requestBody.LabelName) + + if err != nil { + print(err.Error()) + return err + } + + return nil +} diff --git a/backend/schema/tasks/routes.go b/backend/schema/tasks/routes.go new file mode 100644 index 0000000..6fef37a --- /dev/null +++ b/backend/schema/tasks/routes.go @@ -0,0 +1,164 @@ +package tasks + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx" +) + +type PgModel struct { + Conn *pgx.Conn +} + +func TaskGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { + + tasks := v1.Group("") + { + tasks.GET("/filtered", c.GetFilteredTasks) + tasks.POST("/:tid/assign", c.AssignUsersToTask) + tasks.DELETE("/:tid/remove", c.RemoveUsersFromTask) + tasks.GET("/assigned", c.GetTasksByAssignedUsers) + } + + return tasks +} + +type TaskQuery struct { + TaskID string `form:"taskID"` + GroupID string `form:"groupID"` + CreatedBy string `form:"createdBy"` + TaskStatus string `form:"taskStatus"` + TaskType string `form:"taskType"` + StartDate string `form:"startDate"` + EndDate string `form:"endDate"` +} + +// GetFilteredTasks godoc +// +// @summary Get Filtered Tasks +// @description get filtered tasks +// @tags tasks +// +// @param _ query TaskQuery true "Filters for task query" +// +// @success 200 {array} models.Task +// @failure 400 {object} string +// @router /tasks/filtered [get] +func (pg *PgModel) GetFilteredTasks(c *gin.Context) { + var filterQuery TaskQuery + if err := c.ShouldBindQuery(&filterQuery); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + tasks, err := GetTasksByQueryFromDB(pg.Conn, filterQuery) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, tasks) +} + +type Assignment struct { + UserIDs []string `json:"userIDs"` + Assigner string `json:"assigner"` +} + +// AssignUsersToTask godoc +// +// @summary Assign Users To Task +// @description assign users to task +// @tags tasks +// +// @param tid path string true "Task ID to assign users to" +// @param _ body Assignment true "Users to assign to task and assignee" +// +// @success 200 {array} models.TaskUser +// @failure 400 {object} string +// @router /tasks/{tid}/assign [post] +func (pg *PgModel) AssignUsersToTask(c *gin.Context) { + var requestBody Assignment + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + assignedUsers, err := AssignUsersToTaskInDB(pg.Conn, requestBody.UserIDs, c.Param("tid"), requestBody.Assigner) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, assignedUsers) +} + +type Removal struct { + UserIDs []string `json:"userIDs"` +} + +// RemoveUsersFromTask godoc +// +// @summary Remove Users From Task +// @description remove users from task +// @tags tasks +// +// @param tid path string true "Task ID to remove users from" +// @param _ body Removal true "Users to remove from task" +// +// @success 200 {array} models.TaskUser +// @failure 400 {object} string +// @router /tasks/{tid}/remove [delete] +func (pg *PgModel) RemoveUsersFromTask(c *gin.Context) { + var requestBody Removal + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + removedUsers, err := RemoveUsersFromTaskInDB(pg.Conn, requestBody.UserIDs, c.Param("tid")) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, removedUsers) +} + +type AssignedQuery struct { + UserIDs []string `query:"userIDs"` +} + +// GetTasksByAssignedUsers godoc +// +// @summary Get Tasks Assigned To Given Users +// @description get tasks assigned to given users +// @tags tasks +// +// @param _ query AssignedQuery true "Users to return tasks for" +// +// @success 200 {array} models.Task +// @failure 400 {object} string +// @router /tasks/assigned [get] +func (pg *PgModel) GetTasksByAssignedUsers(c *gin.Context) { + userIDs := c.Query("userIDs") + assignedQuery := AssignedQuery{ + UserIDs: strings.Split(userIDs, ","), + } + + tasks, err := GetTasksByAssignedFromDB(pg.Conn, assignedQuery.UserIDs) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, tasks) +} diff --git a/backend/schema/tasks/task_test.go b/backend/schema/tasks/task_test.go new file mode 100644 index 0000000..c8785fe --- /dev/null +++ b/backend/schema/tasks/task_test.go @@ -0,0 +1,210 @@ +package tasks + +import ( + "bytes" + "carewallet/configuration" + "carewallet/db" + "carewallet/models" + "encoding/json" + "fmt" + "net/http" + "net/http/httptest" + "net/url" + "os" + "reflect" + "testing" + "time" + + "github.com/gin-contrib/cors" + "github.com/gin-gonic/gin" +) + +func TestTaskGroup(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("/tasks") + { + TaskGroup(v1, &controller) + } + + t.Run("TestGetFilteredTasks", func(t *testing.T) { + getRequest := TaskQuery{ + GroupID: "", + CreatedBy: "", + TaskStatus: "", + TaskType: "other", + StartDate: "", + EndDate: "", + } + + w := httptest.NewRecorder() + query := url.Values{} + query.Add("groupID", getRequest.GroupID) + query.Add("createdBy", getRequest.CreatedBy) + query.Add("taskStatus", getRequest.TaskStatus) + query.Add("taskType", getRequest.TaskType) + query.Add("startDate", getRequest.StartDate) + query.Add("endDate", getRequest.EndDate) + + req, _ := http.NewRequest("GET", "/tasks/filtered?"+query.Encode(), nil) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to retrieve tasks by filter query.") + } + + var responseTasks []models.Task + err = json.Unmarshal(w.Body.Bytes(), &responseTasks) + + if err != nil { + t.Error("Failed to unmarshal json") + } + start_date_1 := time.Date(2024, 2, 10, 14, 30, 0, 0, time.UTC) + expectedTasks := []models.Task{ + { + TaskID: 2, + GroupID: 2, + CreatedBy: "user3", + CreatedDate: time.Date(2024, 2, 20, 23, 59, 59, 0, time.UTC), + StartDate: &start_date_1, + TaskStatus: "INCOMPLETE", + TaskType: "other", + }, + { + TaskID: 4, + GroupID: 4, + CreatedBy: "user1", + CreatedDate: time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC), + TaskStatus: "COMPLETE", + TaskType: "other", + }, + } + + if !reflect.DeepEqual(expectedTasks, responseTasks) { + t.Error("Result was not correct") + } + }) + + t.Run("TestRemoveUsersFromTask", func(t *testing.T) { + var removeRequest = Removal{ + UserIDs: []string{"user1"}, + } + + requestJSON, err := json.Marshal(removeRequest) + if err != nil { + t.Error("Failed to marshal remove request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("DELETE", "/tasks/1/remove", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to remove users from task.") + } + + var removeResponse []models.TaskUser + err = json.Unmarshal(w.Body.Bytes(), &removeResponse) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + expectedTaskUsers := []models.TaskUser{ + { + TaskID: 1, + UserID: "user1", + }, + } + + if !reflect.DeepEqual(expectedTaskUsers, removeResponse) { + t.Error("Result was not correct") + } + }) + + t.Run("TestAssignUsersToTask", func(t *testing.T) { + assignRequest := Assignment{ + UserIDs: []string{"user4"}, + Assigner: "user1", + } + + requestJSON, err := json.Marshal(assignRequest) + if err != nil { + t.Error("Failed to marshal assign request to JSON") + } + + w := httptest.NewRecorder() + req, _ := http.NewRequest("POST", "/tasks/2/assign", bytes.NewBuffer(requestJSON)) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to assign users to task.") + } + + var assignResponse []models.TaskUser + err = json.Unmarshal(w.Body.Bytes(), &assignResponse) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + expectedTaskUsers := []models.TaskUser{ + { + TaskID: 2, + UserID: "user4", + }, + } + + if !reflect.DeepEqual(expectedTaskUsers, assignResponse) { + t.Error("Result was not correct") + } + }) + + t.Run("TestGetTasksByAssigned", func(t *testing.T) { + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/tasks/assigned?userIDs=user2", nil) + router.ServeHTTP(w, req) + + if http.StatusOK != w.Code { + t.Error("Failed to retrieve tasks by assigned user.") + } + + var responseTasks []models.Task + err = json.Unmarshal(w.Body.Bytes(), &responseTasks) + + if err != nil { + t.Error("Failed to unmarshal json") + } + + note := "Refill water pitcher" + expectedTasks := []models.Task{ + { + TaskID: 4, + GroupID: 4, + CreatedBy: "user1", + CreatedDate: time.Date(2006, 1, 2, 15, 4, 5, 0, time.UTC), + Notes: ¬e, + TaskStatus: "COMPLETE", + TaskType: "other", + }, + } + + fmt.Println("Expected: ", expectedTasks) + fmt.Println("Response: ", responseTasks) + if !reflect.DeepEqual(expectedTasks, responseTasks) { + t.Error("Result was not correct") + } + }) +} diff --git a/backend/schema/tasks/transactions.go b/backend/schema/tasks/transactions.go new file mode 100644 index 0000000..77f87b2 --- /dev/null +++ b/backend/schema/tasks/transactions.go @@ -0,0 +1,156 @@ +package tasks + +import ( + "carewallet/models" + "fmt" + "strconv" + "time" + + "github.com/jackc/pgx" +) + +func GetTasksByQueryFromDB(pool *pgx.Conn, filterQuery TaskQuery) ([]models.Task, error) { + query_fields := []string{ + filterQuery.TaskID, + filterQuery.GroupID, + filterQuery.CreatedBy, + filterQuery.TaskStatus, + filterQuery.TaskType, + filterQuery.StartDate, + filterQuery.EndDate} + + field_names := []string{"task_id =", "group_id =", "created_by =", "task_status =", "task_type =", "start_date >=", "end_date <="} + var query string + var args []interface{} + + for i, field := range query_fields { + if field != "" { + if query != "" { + query += " AND " + } + query += fmt.Sprintf("%s $%d", field_names[i], len(args)+1) + args = append(args, field) + } + } + + rows, err := pool.Query("SELECT task_id, group_id, created_by, created_date, start_date, end_date, task_status, task_type FROM task WHERE "+query, args...) + + if err != nil { + print(err, "error selecting tasks by query") + return nil, err + } + + defer rows.Close() + + var results []models.Task + + for rows.Next() { + task := models.Task{} + err := rows.Scan(&task.TaskID, &task.GroupID, &task.CreatedBy, &task.CreatedDate, &task.StartDate, &task.EndDate, &task.TaskStatus, &task.TaskType) + + if err != nil { + print(err, "error scanning tasks by query") + return nil, err + } + + results = append(results, task) + } + + return results, nil +} + +func AssignUsersToTaskInDB(pool *pgx.Conn, users []string, taskID string, assigner string) ([]models.TaskUser, error) { + task_id, err := strconv.Atoi(taskID) + if err != nil { + return nil, err + } + + var assignedUsers []models.TaskUser + + for _, user := range users { + _, err := pool.Exec("INSERT INTO task_assignees (task_id, user_id, assignment_status, assigned_by, assigned_date) VALUES ($1, $2, $3, $4, $5);", task_id, user, "NOTIFIED", assigner, time.Now()) + + if err != nil { + print(err.Error(), "error inserting users into task_assignees") + return nil, err + } + + assignedUsers = append(assignedUsers, models.TaskUser{TaskID: task_id, UserID: user}) + } + + return assignedUsers, nil +} + +func RemoveUsersFromTaskInDB(pool *pgx.Conn, users []string, taskID string) ([]models.TaskUser, error) { + task_id, err := strconv.Atoi(taskID) + if err != nil { + print(err, "error converting task ID to int") + return nil, err + } + + var removedUsers []models.TaskUser + + for _, user := range users { + var exists int + err := pool.QueryRow("SELECT 1 FROM task_assignees WHERE task_id = $1 AND user_id = $2 LIMIT 1;", task_id, user).Scan(&exists) + if err != nil { + if err == pgx.ErrNoRows { + return nil, fmt.Errorf("user not assigned to task") + } + print(err, "error checking if user and task exist in task_assignees") + return nil, err + } + + _, err = pool.Exec("DELETE FROM task_assignees WHERE task_id = $1 AND user_id = $2;", task_id, user) + if err != nil { + print(err, "error deleting users from task_assignees") + return nil, err + } + + removedUsers = append(removedUsers, models.TaskUser{TaskID: task_id, UserID: user}) + } + + return removedUsers, nil +} + +func GetTasksByAssignedFromDB(pool *pgx.Conn, userIDs []string) ([]models.Task, error) { + var task_ids []int + var tasks []models.Task + + // Get all task IDs assigned to the user + for _, userID := range userIDs { + fmt.Println(userID) + taskIDs, err := pool.Query("SELECT task_id FROM task_assignees WHERE user_id = $1;", userID) + if err != nil { + print(err, "error selecting task assignees") + return nil, err + } + defer taskIDs.Close() + + for taskIDs.Next() { + var task_id int + + err := taskIDs.Scan(&task_id) + if err != nil { + print(err, "error scanning task ID") + return nil, err + } + fmt.Println(task_id) + task_ids = append(task_ids, task_id) + } + } + + // Get all tasks by task ID + var task models.Task + for _, task_id := range task_ids { + err := pool.QueryRow("SELECT * FROM task WHERE task_id = $1;", task_id).Scan(&task.TaskID, &task.GroupID, &task.CreatedBy, &task.CreatedDate, &task.StartDate, &task.EndDate, &task.Notes, &task.Repeating, &task.RepeatingInterval, &task.RepeatingEndDate, &task.TaskStatus, &task.TaskType, &task.TaskInfo) + if err != nil { + print(err, "error querying task by ID") + return nil, err + } + + tasks = append(tasks, task) + } + + return tasks, nil +}