From 9066bb0599cb6ed5b7d767c1db7227fe76df92d3 Mon Sep 17 00:00:00 2001 From: Matt McCoy <59743922+MattCMcCoy@users.noreply.github.com> Date: Fri, 23 Feb 2024 12:52:55 -0500 Subject: [PATCH 1/3] refactor: split db tables (#36) * split db tables * add 90% of the carewallet peeps to a group --- .github/workflows/BackendCI.yml | 5 +- backend/db/migrations/1.user.sql | 32 +++++ backend/db/migrations/2.group.sql | 53 ++++++++ backend/db/migrations/3.task.sql | 55 ++++++++ backend/db/migrations/4.label.sql | 35 +++++ backend/db/migrations/5.files.sql | 15 +++ backend/db/migrations/init.sql | 179 -------------------------- client/contexts/CareWalletContext.tsx | 2 +- 8 files changed, 195 insertions(+), 181 deletions(-) create mode 100644 backend/db/migrations/1.user.sql create mode 100644 backend/db/migrations/2.group.sql create mode 100644 backend/db/migrations/3.task.sql create mode 100644 backend/db/migrations/4.label.sql create mode 100644 backend/db/migrations/5.files.sql diff --git a/.github/workflows/BackendCI.yml b/.github/workflows/BackendCI.yml index 66c3ee5..716a60a 100644 --- a/.github/workflows/BackendCI.yml +++ b/.github/workflows/BackendCI.yml @@ -78,7 +78,10 @@ jobs: uses: actions/checkout@v4 - name: Import DB seed data - run: psql -d postgresql://user:pwd@172.17.0.1:5432/carewallet -f init.sql + run: | + for file in $(ls -1 ./ | sort); do + psql -d postgresql://user:pwd@172.17.0.1:5432/carewallet -f "$file" + done working-directory: ./backend/db/migrations - name: Set up Go diff --git a/backend/db/migrations/1.user.sql b/backend/db/migrations/1.user.sql new file mode 100644 index 0000000..c0cec92 --- /dev/null +++ b/backend/db/migrations/1.user.sql @@ -0,0 +1,32 @@ +DROP TABLE IF EXISTS users; + +CREATE TABLE IF NOT EXISTS users ( + user_id varchar NOT NULL UNIQUE, + first_name varchar NOT NULL, + last_name varchar NOT NULL, + email varchar NOT NULL, + phone varchar, --potentially make phone/address required (NOT NULL) + address varchar, + pfp_s3_url varchar, --for profile picture if we implement that + device_id varchar, --expoPushToken for push notifications + push_notification_enabled BOOLEAN DEFAULT FALSE, + PRIMARY KEY (user_id) +); + +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'), + + -- Care-Wallet Team + ('fIoFY26mJnYWH8sNdfuVoxpnVnr1', 'Matt', 'McCoy', 'mattcmccoy01@gmail.com', '', ''), + ('BLq3MXk4rVg4RKuYiMd7aEmMhsz1', 'Ansh', 'Patel', 'anshrpatel22@gmail.com', '', ''), + ('mPeo3d3MiXfnpPJADWgFD9ZcB2M2', 'Olivia', 'Sedarski', 'olivia@gmail.com', '', ''), + ('onrQs8HVGBVMPNz4Fk1uE94bSxg1', 'Danny', 'Rollo', 'dannyrollo4@gmail.com', '', ''), + ('8Sy7xBkGiGQv4ZKphcQfY8PxAqw1', 'Narayan', 'Sharma', 'sharma.na@northeastern.edu', '', ''), + ('iL7PnjS4axQffmlPceobjUUZ9DF2', 'Caitlin', 'Flynn', 'flynn.ca@northeastern.edu', '', ''), + ('5JgN2PQxCRM9VoCiiFPlQPNqkL32', 'Linwood', 'Blaisdell', 'blaisdell.l@northeastern.edu', '', '') + -- End Care-Wallet Team +; diff --git a/backend/db/migrations/2.group.sql b/backend/db/migrations/2.group.sql new file mode 100644 index 0000000..a89b784 --- /dev/null +++ b/backend/db/migrations/2.group.sql @@ -0,0 +1,53 @@ +DROP TABLE IF EXISTS care_group; +DROP TABLE IF EXISTS group_roles; + +CREATE TYPE role AS ENUM ('PATIENT', 'PRIMARY', 'SECONDARY'); + +CREATE TABLE IF NOT EXISTS care_group ( + group_id serial NOT NULL UNIQUE, + group_name varchar NOT NULL, + date_created timestamp NOT NULL, --do we default current time? + PRIMARY KEY (group_id) +); + +CREATE TABLE IF NOT EXISTS group_roles ( + group_id integer NOT NULL, + user_id varchar NOT NULL, + role role NOT NULL, + PRIMARY KEY (group_id, user_id), + FOREIGN KEY (group_id) REFERENCES care_group (group_id), + FOREIGN KEY (user_id) REFERENCES users (user_id) +); + +INSERT INTO care_group (group_name, date_created) +VALUES + ('Smith Family', NOW()), + ('Johnson Support Network', NOW()), + ('Williams Care Team', NOW()), + ('Brown Medical Group', NOW()), + + -- Care-Wallet Team + ('We <3 Old People', NOW()) + -- End Care-Wallet Team +; + +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'), + + -- Care-Wallet Team + (5, 'fIoFY26mJnYWH8sNdfuVoxpnVnr1', 'PRIMARY'), + (5, '5JgN2PQxCRM9VoCiiFPlQPNqkL32', 'PATIENT'), + (5, 'BLq3MXk4rVg4RKuYiMd7aEmMhsz1', 'SECONDARY'), + (5, 'mPeo3d3MiXfnpPJADWgFD9ZcB2M2', 'SECONDARY'), + (5, 'onrQs8HVGBVMPNz4Fk1uE94bSxg1', 'SECONDARY'), + (5, '8Sy7xBkGiGQv4ZKphcQfY8PxAqw1', 'SECONDARY'), + (5, 'iL7PnjS4axQffmlPceobjUUZ9DF2', 'SECONDARY') + -- End Care-Wallet Team +; diff --git a/backend/db/migrations/3.task.sql b/backend/db/migrations/3.task.sql new file mode 100644 index 0000000..7da671b --- /dev/null +++ b/backend/db/migrations/3.task.sql @@ -0,0 +1,55 @@ +DROP TABLE IF EXISTS task; +DROP TABLE IF EXISTS task_assignees; +DROP TABLE IF EXISTS task_labels; + +CREATE TYPE task_assignment_status AS ENUM ('ACCEPTED', 'DECLINED', 'NOTIFIED'); +CREATE TYPE task_status AS ENUM ('INCOMPLETE', 'COMPLETE', 'PARTIAL'); +CREATE TYPE task_type AS ENUM ('med_mgmt', 'dr_appt', 'financial', 'other'); + +CREATE TABLE IF NOT EXISTS task ( + task_id serial NOT NULL, + group_id integer NOT NULL, + created_by varchar NOT NULL, + created_date timestamp NOT NULL, -- add default val with current timestamp? + start_date timestamp, + end_date timestamp, + notes varchar, + repeating BOOLEAN DEFAULT FALSE, + repeating_interval varchar, + repeating_end_date timestamp, + task_status task_status NOT NULL, + task_type task_type NOT NULL, -- (eg. medication management, dr appointment, etc.) + task_info json, + PRIMARY KEY (task_id), + FOREIGN KEY (group_id) REFERENCES care_group (group_id), + FOREIGN KEY (created_by) REFERENCES users (user_id) +); + +CREATE TABLE IF NOT EXISTS task_assignees ( + task_id integer NOT NULL, + user_id varchar NOT NULL, + assignment_status task_assignment_status NOT NULL, + assigned_by varchar NOT NULL, + assigned_date timestamp NOT NULL, -- add default val with current timestamp? + last_notified timestamp, + PRIMARY KEY (task_id, user_id), + FOREIGN KEY (task_id) REFERENCES task (task_id), + FOREIGN KEY (user_id) REFERENCES users (user_id), + FOREIGN KEY (assigned_by) REFERENCES users (user_id) +); + +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 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()) +; diff --git a/backend/db/migrations/4.label.sql b/backend/db/migrations/4.label.sql new file mode 100644 index 0000000..944719b --- /dev/null +++ b/backend/db/migrations/4.label.sql @@ -0,0 +1,35 @@ + DROP TABLE IF EXISTS label; + + CREATE TABLE If NOT EXISTS label ( + group_id integer NOT NULL, + label_name varchar NOT NULL, + label_color varchar NOT NULL, + PRIMARY KEY (group_id, label_name), + FOREIGN KEY (group_id) REFERENCES care_group (group_id) +); + + CREATE TABLE If NOT EXISTS task_labels ( + task_id integer NOT NULL, + group_id integer NOT NULL, + label_name varchar NOT NULL, + PRIMARY KEY (task_id, label_name), + 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 +); + +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') +; diff --git a/backend/db/migrations/5.files.sql b/backend/db/migrations/5.files.sql new file mode 100644 index 0000000..82b38cf --- /dev/null +++ b/backend/db/migrations/5.files.sql @@ -0,0 +1,15 @@ +DROP TABLE IF EXISTS files; + +CREATE TABLE IF NOT EXISTS files ( + file_id serial NOT NULL UNIQUE, + file_name varchar NOT NULL, + group_id integer NOT NULL, + upload_by varchar NOT NULL, + upload_date timestamp, + file_size integer NOT NULL, + task_id integer, + PRIMARY KEY (file_id), + FOREIGN KEY (group_id) REFERENCES care_group (group_id), + FOREIGN KEY (upload_by) REFERENCES users (user_id), + FOREIGN KEY (task_id) REFERENCES task (task_id) +); diff --git a/backend/db/migrations/init.sql b/backend/db/migrations/init.sql index b7d4f46..f664cfc 100644 --- a/backend/db/migrations/init.sql +++ b/backend/db/migrations/init.sql @@ -1,17 +1,4 @@ DROP TABLE IF EXISTS medication; -DROP TABLE IF EXISTS care_group; -DROP TABLE IF EXISTS users; -DROP TABLE IF EXISTS group_roles; -DROP TABLE IF EXISTS task; -DROP TABLE IF EXISTS task_assignees; -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'); -CREATE TYPE task_type AS ENUM ('med_mgmt', 'dr_appt', 'financial', 'other'); CREATE TABLE IF NOT EXISTS medication ( medication_id integer NOT NULL UNIQUE, @@ -19,102 +6,6 @@ CREATE TABLE IF NOT EXISTS medication ( PRIMARY KEY (medication_id) ); - -CREATE TABLE IF NOT EXISTS care_group ( - group_id serial NOT NULL UNIQUE, - group_name varchar NOT NULL, - date_created timestamp NOT NULL, --do we default current time? - PRIMARY KEY (group_id) -); - -CREATE TABLE IF NOT EXISTS users ( - user_id varchar NOT NULL UNIQUE, - first_name varchar NOT NULL, - last_name varchar NOT NULL, - email varchar NOT NULL, - phone varchar, --potentially make phone/address required (NOT NULL) - address varchar, - pfp_s3_url varchar, --for profile picture if we implement that - device_id varchar, --expoPushToken for push notifications - push_notification_enabled BOOLEAN DEFAULT FALSE, - PRIMARY KEY (user_id) -); - -CREATE TABLE IF NOT EXISTS group_roles ( - group_id integer NOT NULL, - user_id varchar NOT NULL, - role role NOT NULL, - PRIMARY KEY (group_id, user_id), - FOREIGN KEY (group_id) REFERENCES care_group (group_id), - FOREIGN KEY (user_id) REFERENCES users (user_id) -); - -CREATE TABLE IF NOT EXISTS task ( - task_id serial NOT NULL, - group_id integer NOT NULL, - created_by varchar NOT NULL, - created_date timestamp NOT NULL, -- add default val with current timestamp? - start_date timestamp, - end_date timestamp, - notes varchar, - repeating BOOLEAN DEFAULT FALSE, - repeating_interval varchar, - repeating_end_date timestamp, - task_status task_status NOT NULL, - task_type task_type NOT NULL, -- (eg. medication management, dr appointment, etc.) - task_info json, - PRIMARY KEY (task_id), - FOREIGN KEY (group_id) REFERENCES care_group (group_id), - FOREIGN KEY (created_by) REFERENCES users (user_id) -); - -CREATE TABLE IF NOT EXISTS task_assignees ( - task_id integer NOT NULL, - user_id varchar NOT NULL, - assignment_status task_assignment_status NOT NULL, - assigned_by varchar NOT NULL, - assigned_date timestamp NOT NULL, -- add default val with current timestamp? - last_notified timestamp, - PRIMARY KEY (task_id, user_id), - FOREIGN KEY (task_id) REFERENCES task (task_id), - FOREIGN KEY (user_id) REFERENCES users (user_id), - FOREIGN KEY (assigned_by) REFERENCES users (user_id) -); - - CREATE TABLE If NOT EXISTS label ( - group_id integer NOT NULL, - label_name varchar NOT NULL, - label_color varchar NOT NULL, -- TODO:figure out what form color should be ("rgba(12,2,1,0)", "#21292F", "red" etc.) or just - PRIMARY KEY (group_id, label_name), - FOREIGN KEY (group_id) REFERENCES care_group (group_id) -); - - CREATE TABLE If NOT EXISTS task_labels ( - task_id integer NOT NULL, - group_id integer NOT NULL, - label_name varchar NOT NULL, - PRIMARY KEY (task_id, label_name), - 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 ( - file_id serial NOT NULL UNIQUE, - file_name varchar NOT NULL, - group_id integer NOT NULL, - upload_by varchar NOT NULL, - upload_date timestamp, - file_size integer NOT NULL, - task_id integer, - PRIMARY KEY (file_id), - FOREIGN KEY (group_id) REFERENCES care_group (group_id), - FOREIGN KEY (upload_by) REFERENCES users (user_id), - FOREIGN KEY (task_id) REFERENCES task (task_id) -); - ------------------ SAMPLE DATA :) ----------------------- - --- Insert sample data into "medication" table INSERT INTO medication (medication_id, medication_name) VALUES (1, 'Medication A'), @@ -123,73 +14,3 @@ VALUES (4, 'Medication D'), (5, 'Medication E') ; - -INSERT INTO care_group (group_name, date_created) -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 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/client/contexts/CareWalletContext.tsx b/client/contexts/CareWalletContext.tsx index 9170200..e97ed1d 100644 --- a/client/contexts/CareWalletContext.tsx +++ b/client/contexts/CareWalletContext.tsx @@ -30,7 +30,7 @@ export function CareWalletProvider({ setUser(signedInUser); setGroup({ - groupID: 999, + groupID: 5, role: 'TEMP' }); }); From a02de6974f7be2f535ab58e95eba3027a10107af Mon Sep 17 00:00:00 2001 From: Matt McCoy <59743922+MattCMcCoy@users.noreply.github.com> Date: Fri, 23 Feb 2024 13:00:30 -0500 Subject: [PATCH 2/3] remaining task routes (get task, update task, create task) (#33) * haleys remaining task routes --- backend/docs/docs.go | 191 ++++++++++++++++++++ backend/docs/swagger.json | 191 ++++++++++++++++++++ backend/docs/swagger.yaml | 126 ++++++++++++++ backend/schema/tasks/routes.go | 227 ++++++++++++++++++++++-- backend/schema/tasks/task_test.go | 251 ++++++++++++++++++++++++++- backend/schema/tasks/transactions.go | 95 ++++++++++ 6 files changed, 1058 insertions(+), 23 deletions(-) diff --git a/backend/docs/docs.go b/backend/docs/docs.go index 6da6f32..f958fba 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -432,6 +432,34 @@ const docTemplate = `{ } } }, + "/tasks": { + "post": { + "description": "Create a new task", + "tags": [ + "tasks" + ], + "summary": "Create a New Task", + "parameters": [ + { + "description": "Create Task Request", + "name": "request_body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.TaskBody" + } + } + ], + "responses": { + "201": { + "description": "Created Task", + "schema": { + "$ref": "#/definitions/models.Task" + } + } + } + } + }, "/tasks/assigned": { "get": { "description": "get tasks assigned to given users", @@ -532,6 +560,92 @@ const docTemplate = `{ } } }, + "/tasks/{tid}": { + "get": { + "description": "get a task given its id", + "tags": [ + "tasks" + ], + "summary": "get task by id", + "parameters": [ + { + "type": "integer", + "description": "the id of the task", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Task" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "description": "Update the task_info field of a task by ID", + "tags": [ + "tasks" + ], + "summary": "Update Task Info", + "parameters": [ + { + "type": "integer", + "description": "Task ID", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "Update Task Info Request", + "name": "request_body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.TaskBody" + } + } + ], + "responses": { + "200": { + "description": "Updated Task", + "schema": { + "$ref": "#/definitions/models.Task" + } + } + } + }, + "delete": { + "description": "Delete a task by ID", + "tags": [ + "tasks" + ], + "summary": "Delete a Task", + "parameters": [ + { + "type": "integer", + "description": "Task ID", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/tasks/{tid}/assign": { "post": { "description": "assign users to task", @@ -576,6 +690,41 @@ const docTemplate = `{ } } }, + "/tasks/{tid}/assigned": { + "get": { + "description": "Get list of users assigned to a task by task ID", + "tags": [ + "tasks" + ], + "summary": "Get list of users assigned to a task", + "parameters": [ + { + "type": "integer", + "description": "Task ID", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of user IDs assigned to the task", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, "/tasks/{tid}/labels": { "get": { "description": "get a tasks labels given the task id", @@ -954,6 +1103,48 @@ const docTemplate = `{ } } } + }, + "tasks.TaskBody": { + "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_info": { + "type": "string" + }, + "task_status": { + "type": "string" + }, + "task_type": { + "type": "string" + } + } } } }` diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 40f2229..05ec608 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -425,6 +425,34 @@ } } }, + "/tasks": { + "post": { + "description": "Create a new task", + "tags": [ + "tasks" + ], + "summary": "Create a New Task", + "parameters": [ + { + "description": "Create Task Request", + "name": "request_body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.TaskBody" + } + } + ], + "responses": { + "201": { + "description": "Created Task", + "schema": { + "$ref": "#/definitions/models.Task" + } + } + } + } + }, "/tasks/assigned": { "get": { "description": "get tasks assigned to given users", @@ -525,6 +553,92 @@ } } }, + "/tasks/{tid}": { + "get": { + "description": "get a task given its id", + "tags": [ + "tasks" + ], + "summary": "get task by id", + "parameters": [ + { + "type": "integer", + "description": "the id of the task", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.Task" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "description": "Update the task_info field of a task by ID", + "tags": [ + "tasks" + ], + "summary": "Update Task Info", + "parameters": [ + { + "type": "integer", + "description": "Task ID", + "name": "tid", + "in": "path", + "required": true + }, + { + "description": "Update Task Info Request", + "name": "request_body", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/tasks.TaskBody" + } + } + ], + "responses": { + "200": { + "description": "Updated Task", + "schema": { + "$ref": "#/definitions/models.Task" + } + } + } + }, + "delete": { + "description": "Delete a task by ID", + "tags": [ + "tasks" + ], + "summary": "Delete a Task", + "parameters": [ + { + "type": "integer", + "description": "Task ID", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": "No Content" + } + } + } + }, "/tasks/{tid}/assign": { "post": { "description": "assign users to task", @@ -569,6 +683,41 @@ } } }, + "/tasks/{tid}/assigned": { + "get": { + "description": "Get list of users assigned to a task by task ID", + "tags": [ + "tasks" + ], + "summary": "Get list of users assigned to a task", + "parameters": [ + { + "type": "integer", + "description": "Task ID", + "name": "tid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "List of user IDs assigned to the task", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, "/tasks/{tid}/labels": { "get": { "description": "get a tasks labels given the task id", @@ -947,6 +1096,48 @@ } } } + }, + "tasks.TaskBody": { + "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_info": { + "type": "string" + }, + "task_status": { + "type": "string" + }, + "task_type": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index 2617a4b..f6b619e 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -144,6 +144,34 @@ definitions: type: string type: array type: object + tasks.TaskBody: + 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_info: + type: string + task_status: + type: string + task_type: + type: string + type: object info: contact: {} description: This is an API for the Care-Wallet App. @@ -426,6 +454,81 @@ paths: summary: add a medication tags: - medications + /tasks: + post: + description: Create a new task + parameters: + - description: Create Task Request + in: body + name: request_body + required: true + schema: + $ref: '#/definitions/tasks.TaskBody' + responses: + "201": + description: Created Task + schema: + $ref: '#/definitions/models.Task' + summary: Create a New Task + tags: + - tasks + /tasks/{tid}: + delete: + description: Delete a task by ID + parameters: + - description: Task ID + in: path + name: tid + required: true + type: integer + responses: + "204": + description: No Content + summary: Delete a Task + tags: + - tasks + get: + description: get a task given its id + parameters: + - description: the id of the task + in: path + name: tid + required: true + type: integer + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.Task' + "400": + description: Bad Request + schema: + type: string + summary: get task by id + tags: + - tasks + put: + description: Update the task_info field of a task by ID + parameters: + - description: Task ID + in: path + name: tid + required: true + type: integer + - description: Update Task Info Request + in: body + name: request_body + required: true + schema: + $ref: '#/definitions/tasks.TaskBody' + responses: + "200": + description: Updated Task + schema: + $ref: '#/definitions/models.Task' + summary: Update Task Info + tags: + - tasks /tasks/{tid}/assign: post: description: assign users to task @@ -455,6 +558,29 @@ paths: summary: Assign Users To Task tags: - tasks + /tasks/{tid}/assigned: + get: + description: Get list of users assigned to a task by task ID + parameters: + - description: Task ID + in: path + name: tid + required: true + type: integer + responses: + "200": + description: List of user IDs assigned to the task + schema: + items: + type: string + type: array + "400": + description: Bad Request + schema: + type: string + summary: Get list of users assigned to a task + tags: + - tasks /tasks/{tid}/labels: delete: description: remove a label from a task given the task id, group id, and label diff --git a/backend/schema/tasks/routes.go b/backend/schema/tasks/routes.go index 6fef37a..9452a46 100644 --- a/backend/schema/tasks/routes.go +++ b/backend/schema/tasks/routes.go @@ -1,8 +1,13 @@ package tasks import ( + "carewallet/models" + "encoding/json" + "fmt" "net/http" + "strconv" "strings" + "time" "github.com/gin-gonic/gin" "github.com/jackc/pgx" @@ -13,18 +18,50 @@ type PgModel struct { } 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) - } + tasks.POST("", c.CreateTask) + byId := tasks.Group("/:tid") + { + byId.GET("", c.GetTaskByID) + byId.DELETE("", c.DeleteTask) + byId.PUT("", c.UpdateTaskInfo) + byId.GET("/assigned", c.GetUsersAssignedToTask) + byId.POST("/assign", c.AssignUsersToTask) + byId.DELETE("/remove", c.RemoveUsersFromTask) + } + } return tasks } +// GetTaskByID godoc +// +// @summary get task by id +// @description get a task given its id +// @tags tasks +// +// @param tid path int true "the id of the task" +// +// @success 200 {object} models.Task +// @failure 400 {object} string +// @router /tasks/{tid} [GET] +func (pg *PgModel) GetTaskByID(c *gin.Context) { + taskID, err := strconv.Atoi(c.Param("tid")) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + task, err := GetTaskByID(pg.Conn, taskID) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + c.JSON(http.StatusOK, task) +} + type TaskQuery struct { TaskID string `form:"taskID"` GroupID string `form:"groupID"` @@ -52,14 +89,11 @@ func (pg *PgModel) GetFilteredTasks(c *gin.Context) { 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) } @@ -82,19 +116,15 @@ type Assignment struct { // @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) } @@ -116,19 +146,15 @@ type Removal struct { // @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) } @@ -152,13 +178,180 @@ func (pg *PgModel) GetTasksByAssignedUsers(c *gin.Context) { 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) +} +type TaskBody struct { + 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"` +} + +// CreateTask godoc +// +// @summary Create a New Task +// @description Create a new task +// @tags tasks +// +// @param request_body body TaskBody true "Create Task Request" +// +// @success 201 {object} models.Task "Created Task" +// @router /tasks [post] +func (pg *PgModel) CreateTask(c *gin.Context) { + // Bind the request body to the CreateTaskRequest struct + var requestBody models.Task + if err := c.BindJSON(&requestBody); err != nil { + fmt.Println("error binding to request body: ", err) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + // Create the new task in the database + newTaskID, err := CreateTaskInDB(pg.Conn, requestBody) if err != nil { + fmt.Println("error creating task in the database:", err) c.JSON(http.StatusBadRequest, err.Error()) return } - c.JSON(http.StatusOK, tasks) + // Fetch the created task from the database + createdTask, err := GetTaskByID(pg.Conn, newTaskID) + if err != nil { + fmt.Println("error fetching created task from the database:", err) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusCreated, createdTask) +} + +// DeleteTask godoc +// +// @summary Delete a Task +// @description Delete a task by ID +// @tags tasks +// @param tid path int true "Task ID" +// @success 204 "No Content" +// @router /tasks/{tid} [delete] +func (pg *PgModel) DeleteTask(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 := GetTaskByID(pg.Conn, taskID); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + // Delete the task from the database + if err := DeleteTaskInDB(pg.Conn, taskID); err != nil { + fmt.Println("error deleting task from the database:", err) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.Status(http.StatusNoContent) +} + +// UpdateTaskInfo godoc +// +// @summary Update Task Info +// @description Update the task_info field of a task by ID +// @tags tasks +// @param tid path int true "Task ID" +// @param request_body body TaskBody true "Update Task Info Request" +// @success 200 {object} models.Task "Updated Task" +// @router /tasks/{tid} [put] +func (pg *PgModel) UpdateTaskInfo(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 + } + + // Bind the request body to the UpdateTaskInfoRequest struct + var requestBody models.Task + + if err := c.BindJSON(&requestBody); err != nil { + fmt.Println("error binding to request body: ", err) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + // Convert TaskInfo to json.RawMessage + taskInfoRaw, err := json.Marshal(requestBody.TaskInfo) + if err != nil { + fmt.Println("error converting TaskInfo to json.RawMessage:", err) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + // Update the task_info field in the database + if err := UpdateTaskInfoInDB(pg.Conn, taskID, taskInfoRaw); err != nil { + fmt.Println("error updating task info in the database:", err) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + // Fetch the updated task from the database + updatedTask, err := GetTaskByID(pg.Conn, taskID) + if err != nil { + fmt.Println("error fetching updated task from the database:", err) + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, updatedTask) +} + +// TaskUser represents the user assigned to a task. +type TaskUser struct { + UserID string `json:"userID"` +} + +// GetUsersAssignedToTask godoc +// +// @summary Get list of users assigned to a task +// @description Get list of users assigned to a task by task ID +// @tags tasks +// @param tid path int true "Task ID" +// @success 200 {array} string "List of user IDs assigned to the task" +// @failure 400 {object} string +// @router /tasks/{tid}/assigned [get] +func (pg *PgModel) GetUsersAssignedToTask(c *gin.Context) { + // Extract task ID from the path parameter + taskIDStr := c.Param("tid") + taskID, err := strconv.Atoi(taskIDStr) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + // Get list of users assigned to the task + userIDs, err := GetUsersAssignedToTaskFromDB(pg.Conn, taskID) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, userIDs) } diff --git a/backend/schema/tasks/task_test.go b/backend/schema/tasks/task_test.go index c8785fe..12847d8 100644 --- a/backend/schema/tasks/task_test.go +++ b/backend/schema/tasks/task_test.go @@ -12,6 +12,7 @@ import ( "net/url" "os" "reflect" + "strconv" "testing" "time" @@ -51,12 +52,12 @@ func TestTaskGroup(t *testing.T) { 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) + query.Set("groupID", getRequest.GroupID) + query.Set("createdBy", getRequest.CreatedBy) + query.Set("taskStatus", getRequest.TaskStatus) + query.Set("taskType", getRequest.TaskType) + query.Set("startDate", getRequest.StartDate) + query.Set("endDate", getRequest.EndDate) req, _ := http.NewRequest("GET", "/tasks/filtered?"+query.Encode(), nil) router.ServeHTTP(w, req) @@ -172,6 +173,40 @@ func TestTaskGroup(t *testing.T) { } }) + 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", + }, + } + + if !reflect.DeepEqual(expectedTasks, responseTasks) { + 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) @@ -207,4 +242,208 @@ func TestTaskGroup(t *testing.T) { t.Error("Result was not correct") } }) + + t.Run("TestCreateTask_Success", func(t *testing.T) { + // Creating a Task instance + startDate := time.Now().UTC() + endDate := time.Now().Add(24 * time.Hour).UTC() + notes := "This is a sample task" + repeating := true + repeatingInterval := "Weekly" + repeatingEndDate := time.Now().Add(7 * 24 * time.Hour).UTC() + taskInfo := `{"info": "Additional information about the task"}` + + taskData := models.Task{ + TaskID: 1, + GroupID: 1, + CreatedBy: "user1", + CreatedDate: time.Now().UTC(), + StartDate: &startDate, + EndDate: &endDate, + Notes: ¬es, + Repeating: repeating, + RepeatingInterval: &repeatingInterval, + RepeatingEndDate: &repeatingEndDate, + TaskStatus: "INCOMPLETE", + TaskType: "med_mgmt", + TaskInfo: &taskInfo, + } + + taskJSON, err := json.Marshal(taskData) + if err != nil { + t.Error("Failed to marshal task data to JSON:", err) + } + + // Create a new request with the task data + req, err := http.NewRequest("POST", "/tasks", bytes.NewBuffer(taskJSON)) + if err != nil { + t.Error("Failed to create HTTP request:", err) + } + + // Create a recorder to capture the response + w := httptest.NewRecorder() + + // Serve the request using the router + router.ServeHTTP(w, req) + + // Assertions + if http.StatusCreated != w.Code { + t.Error("Expected status 201, got", w.Code) + } + + // Validate the response body + var createdTask models.Task + err = json.Unmarshal(w.Body.Bytes(), &createdTask) + if err != nil { + t.Error("Failed to unmarshal JSON:", err) + } + + // Add more specific assertions based on the expected response + if createdTask.TaskID == -1 { + t.Error("Expected task ID to be present") + } + if createdTask.CreatedBy != "user1" { + t.Error("Unexpected created_by value") + } + // Add more assertions as needed + }) + + 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) + if err != nil { + t.Fatal("Failed to create HTTP request:", err) + } + + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + + // Assertions + if http.StatusNoContent != w.Code { + t.Fatal("Expected status 204, got", w.Code) + } + + if body := w.Body.String(); body != "" { + t.Fatal("Expected empty response body, got", body) + } + }) + + t.Run("TestUpdateTaskInfo", func(t *testing.T) { + // Creating a Task instance + startDate := time.Now().UTC() + endDate := time.Now().Add(24 * time.Hour).UTC() + notes := "This is a sample task" + repeating := true + repeatingInterval := "Weekly" + repeatingEndDate := time.Now().Add(7 * 24 * time.Hour).UTC() + taskInfo := `{"info": "Additional information about the task"}` + + taskData := models.Task{ + TaskID: 1, + GroupID: 1, + CreatedBy: "user1", + CreatedDate: time.Now().UTC(), + StartDate: &startDate, + EndDate: &endDate, + Notes: ¬es, + Repeating: repeating, + RepeatingInterval: &repeatingInterval, + RepeatingEndDate: &repeatingEndDate, + TaskStatus: "INCOMPLETE", + TaskType: "med_mgmt", + TaskInfo: &taskInfo, + } + requestBodyJSON, err := json.Marshal(taskData) + if err != nil { + t.Fatal("Failed to marshal task data to JSON:", err) + return + } + + // Perform a PUT request to the /tasks/:tid/info endpoint + req, err := http.NewRequest("PUT", "/tasks/1", bytes.NewBuffer(requestBodyJSON)) + if err != nil { + t.Fatal("Failed to create HTTP request:", err) + return + } + + // Create a recorder to capture the response + w := httptest.NewRecorder() + + // Serve the request using the router + router.ServeHTTP(w, req) + + // Assertions + if http.StatusOK != w.Code { + t.Fatal("Expected status 200, got", w.Code) + return + } + }) + + t.Run("TestGetUsersAssignedToTask", func(t *testing.T) { + // Create a new recorder and request + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/tasks/2/assigned", nil) + + // Serve the request using the router + router.ServeHTTP(w, req) + + // Print the response body for debugging + fmt.Println("Response Body:", w.Body.String()) + + // Assertions + if http.StatusOK != w.Code { + t.Error("Failed to retrieve users assigned to task.") + } + + var responseUsers []string + err := json.Unmarshal(w.Body.Bytes(), &responseUsers) + if err != nil { + t.Error("Failed to unmarshal JSON") + } + + expectedUsers := []string{"user3", "user4"} + if !reflect.DeepEqual(expectedUsers, responseUsers) { + t.Error("Result was not correct") + } + }) } diff --git a/backend/schema/tasks/transactions.go b/backend/schema/tasks/transactions.go index 77f87b2..31895b5 100644 --- a/backend/schema/tasks/transactions.go +++ b/backend/schema/tasks/transactions.go @@ -2,6 +2,7 @@ package tasks import ( "carewallet/models" + "encoding/json" "fmt" "strconv" "time" @@ -154,3 +155,97 @@ func GetTasksByAssignedFromDB(pool *pgx.Conn, userIDs []string) ([]models.Task, return tasks, nil } + +// CreateTaskInDB creates a new task in the database and returns its ID +func CreateTaskInDB(pool *pgx.Conn, newTask models.Task) (int, error) { + query := ` + INSERT INTO task (group_id, created_by, created_date, start_date, end_date, notes, repeating, repeating_interval, repeating_end_date, task_status, task_type, task_info) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING task_id` + + var newTaskID int + err := pool.QueryRow( + query, + newTask.GroupID, + newTask.CreatedBy, + time.Now(), // Assuming created_date should be the current timestamp + newTask.StartDate, + newTask.EndDate, + newTask.Notes, + newTask.Repeating, + newTask.RepeatingInterval, + newTask.RepeatingEndDate, + newTask.TaskStatus, + newTask.TaskType, + newTask.TaskInfo, + ).Scan(&newTaskID) + + return newTaskID, err +} + +// DeleteTaskInDB deletes a task from the database by ID +func DeleteTaskInDB(pool *pgx.Conn, taskID int) error { + // Assuming "task" table structure, adjust the query based on your schema + query := "DELETE FROM task WHERE task_id = $1" + + _, err := pool.Exec(query, taskID) + return err +} + +// UpdateTaskInfoInDB updates the task_info field in the database +func UpdateTaskInfoInDB(pool *pgx.Conn, taskID int, taskInfo json.RawMessage) error { + // Assuming "task" table structure, adjust the query based on your schema + query := "UPDATE task SET task_info = $1 WHERE task_id = $2" + + _, err := pool.Exec(query, taskInfo, taskID) + return err +} + +// GetTaskByID fetches a task from the database by its ID +func GetTaskByID(pool *pgx.Conn, taskID int) (models.Task, error) { + query := ` + SELECT task_id, group_id, created_by, created_date, start_date, end_date, notes, repeating, repeating_interval, repeating_end_date, task_status, task_type, task_info FROM task WHERE task_id = $1` + + var task models.Task + err := pool.QueryRow(query, taskID).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, + ) + return task, err +} + +func GetUsersAssignedToTaskFromDB(pool *pgx.Conn, taskID int) ([]string, error) { + var userIDs []string + + // Get all user IDs assigned to the task + rows, err := pool.Query("SELECT user_id FROM task_assignees WHERE task_id = $1;", taskID) + if err != nil { + fmt.Println(err, "error selecting user assignees") + return nil, err + } + defer rows.Close() + + for rows.Next() { + var userID string + + err := rows.Scan(&userID) + if err != nil { + fmt.Println(err, "error scanning user ID") + return nil, err + } + + fmt.Println(userID) + userIDs = append(userIDs, userID) + } + + return userIDs, nil +} From 0fdb119f011b1984522da07dbe977018c45bf77e Mon Sep 17 00:00:00 2001 From: Matt McCoy <59743922+MattCMcCoy@users.noreply.github.com> Date: Sat, 24 Feb 2024 15:25:45 -0500 Subject: [PATCH 3/3] Feature/user routes & pp (#37) * feat: bottom nav and header of profile page * feat: display group on profile page * feat: circle card buttons, with usable logout * feat: bottom nav changes color based off of location * refactor: top header its own component for profile * feat: snipe caplans firebase info --- backend/db/migrations/1.user.sql | 1 + backend/db/migrations/2.group.sql | 1 + backend/docs/docs.go | 201 ++++++++++++++++++ backend/docs/swagger.json | 201 ++++++++++++++++++ backend/docs/swagger.yaml | 133 ++++++++++++ backend/main.go | 3 + backend/schema/group-roles/routes.go | 17 +- backend/schema/group-roles/transactions.go | 2 +- backend/schema/medication/routes.go | 3 +- backend/schema/medication/transactions.go | 1 - backend/schema/user/routes.go | 152 +++++++++++++ backend/schema/user/transactions.go | 44 ++++ backend/schema/user/user_test.go | 1 + client/assets/arrow-left.svg | 4 + client/assets/bottom-nav/bell.svg | 6 + client/assets/bottom-nav/calendar.svg | 8 + client/assets/bottom-nav/home.svg | 6 + client/assets/bottom-nav/user.svg | 7 + client/assets/home.svg | 6 - client/assets/profile/ellipse.svg | 3 + client/components/profile/CircleCard.tsx | 17 ++ client/components/profile/Group.tsx | 78 +++++++ client/components/profile/Header.tsx | 50 +++++ .../components/profile/ProfileTopHeader.tsx | 38 ++++ client/contexts/CareWalletContext.tsx | 11 +- client/contexts/api.ts | 9 + client/navigation/AppNavigation.tsx | 1 + .../navigation/AppStackBottomTabNavigator.tsx | 44 +++- client/screens/Groups.tsx | 103 --------- client/screens/Profile.tsx | 74 +++++++ client/services/group.ts | 60 +++--- client/services/medication.ts | 3 +- client/services/user.ts | 63 ++++++ client/types/group.ts | 12 ++ client/types/user.ts | 11 + docker-compose.yaml | 6 +- 36 files changed, 1209 insertions(+), 171 deletions(-) create mode 100644 backend/schema/user/routes.go create mode 100644 backend/schema/user/transactions.go create mode 100644 backend/schema/user/user_test.go create mode 100644 client/assets/arrow-left.svg create mode 100644 client/assets/bottom-nav/bell.svg create mode 100644 client/assets/bottom-nav/calendar.svg create mode 100644 client/assets/bottom-nav/home.svg create mode 100644 client/assets/bottom-nav/user.svg delete mode 100644 client/assets/home.svg create mode 100644 client/assets/profile/ellipse.svg create mode 100644 client/components/profile/CircleCard.tsx create mode 100644 client/components/profile/Group.tsx create mode 100644 client/components/profile/Header.tsx create mode 100644 client/components/profile/ProfileTopHeader.tsx create mode 100644 client/contexts/api.ts delete mode 100644 client/screens/Groups.tsx create mode 100644 client/screens/Profile.tsx create mode 100644 client/services/user.ts create mode 100644 client/types/user.ts diff --git a/backend/db/migrations/1.user.sql b/backend/db/migrations/1.user.sql index c0cec92..51cab11 100644 --- a/backend/db/migrations/1.user.sql +++ b/backend/db/migrations/1.user.sql @@ -22,6 +22,7 @@ VALUES -- Care-Wallet Team ('fIoFY26mJnYWH8sNdfuVoxpnVnr1', 'Matt', 'McCoy', 'mattcmccoy01@gmail.com', '', ''), + ('JamnX6TZf0dt6juozMRzNG5LMQd2', 'Andy', 'Cap', 'caplan.and@northeastern.edu', '', ''), ('BLq3MXk4rVg4RKuYiMd7aEmMhsz1', 'Ansh', 'Patel', 'anshrpatel22@gmail.com', '', ''), ('mPeo3d3MiXfnpPJADWgFD9ZcB2M2', 'Olivia', 'Sedarski', 'olivia@gmail.com', '', ''), ('onrQs8HVGBVMPNz4Fk1uE94bSxg1', 'Danny', 'Rollo', 'dannyrollo4@gmail.com', '', ''), diff --git a/backend/db/migrations/2.group.sql b/backend/db/migrations/2.group.sql index a89b784..a85d363 100644 --- a/backend/db/migrations/2.group.sql +++ b/backend/db/migrations/2.group.sql @@ -43,6 +43,7 @@ VALUES -- Care-Wallet Team (5, 'fIoFY26mJnYWH8sNdfuVoxpnVnr1', 'PRIMARY'), + (5, 'JamnX6TZf0dt6juozMRzNG5LMQd2', 'PRIMARY'), (5, '5JgN2PQxCRM9VoCiiFPlQPNqkL32', 'PATIENT'), (5, 'BLq3MXk4rVg4RKuYiMd7aEmMhsz1', 'SECONDARY'), (5, 'mPeo3d3MiXfnpPJADWgFD9ZcB2M2', 'SECONDARY'), diff --git a/backend/docs/docs.go b/backend/docs/docs.go index f958fba..751b7a3 100644 --- a/backend/docs/docs.go +++ b/backend/docs/docs.go @@ -881,6 +881,155 @@ const docTemplate = `{ } } } + }, + "/user": { + "get": { + "description": "gets the information about multiple users given their user id", + "tags": [ + "user" + ], + "summary": "gets the information about multiple users", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "User IDs", + "name": "userIDs", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/{uid}": { + "get": { + "description": "gets the information about a user given their user id", + "tags": [ + "user" + ], + "summary": "gets the information about a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "description": "Updates a user with the provided userId given the updated user.", + "tags": [ + "user" + ], + "summary": "Updates a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "uid", + "in": "path", + "required": true + }, + { + "description": "User Information", + "name": "UserInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.UserInfoBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "Creates a new user with the provided userId.", + "tags": [ + "user" + ], + "summary": "Creates a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "uid", + "in": "path", + "required": true + }, + { + "description": "User Information", + "name": "UserInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.UserInfoBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { @@ -1068,6 +1217,38 @@ const docTemplate = `{ } } }, + "models.User": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "pfp_s3_url": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "push_notification_enabled": { + "type": "boolean" + }, + "user_id": { + "type": "string" + } + } + }, "task_labels.LabelData": { "type": "object", "properties": { @@ -1145,6 +1326,26 @@ const docTemplate = `{ "type": "string" } } + }, + "user.UserInfoBody": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone": { + "type": "string" + } + } } } }` diff --git a/backend/docs/swagger.json b/backend/docs/swagger.json index 05ec608..1336662 100644 --- a/backend/docs/swagger.json +++ b/backend/docs/swagger.json @@ -874,6 +874,155 @@ } } } + }, + "/user": { + "get": { + "description": "gets the information about multiple users given their user id", + "tags": [ + "user" + ], + "summary": "gets the information about multiple users", + "parameters": [ + { + "type": "array", + "items": { + "type": "string" + }, + "collectionFormat": "csv", + "description": "User IDs", + "name": "userIDs", + "in": "query", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/models.User" + } + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } + }, + "/user/{uid}": { + "get": { + "description": "gets the information about a user given their user id", + "tags": [ + "user" + ], + "summary": "gets the information about a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "uid", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "put": { + "description": "Updates a user with the provided userId given the updated user.", + "tags": [ + "user" + ], + "summary": "Updates a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "uid", + "in": "path", + "required": true + }, + { + "description": "User Information", + "name": "UserInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.UserInfoBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + }, + "post": { + "description": "Creates a new user with the provided userId.", + "tags": [ + "user" + ], + "summary": "Creates a user", + "parameters": [ + { + "type": "string", + "description": "User ID", + "name": "uid", + "in": "path", + "required": true + }, + { + "description": "User Information", + "name": "UserInfo", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/user.UserInfoBody" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/models.User" + } + }, + "400": { + "description": "Bad Request", + "schema": { + "type": "string" + } + } + } + } } }, "definitions": { @@ -1061,6 +1210,38 @@ } } }, + "models.User": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "device_id": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "pfp_s3_url": { + "type": "string" + }, + "phone": { + "type": "string" + }, + "push_notification_enabled": { + "type": "boolean" + }, + "user_id": { + "type": "string" + } + } + }, "task_labels.LabelData": { "type": "object", "properties": { @@ -1138,6 +1319,26 @@ "type": "string" } } + }, + "user.UserInfoBody": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "email": { + "type": "string" + }, + "first_name": { + "type": "string" + }, + "last_name": { + "type": "string" + }, + "phone": { + "type": "string" + } + } } } } \ No newline at end of file diff --git a/backend/docs/swagger.yaml b/backend/docs/swagger.yaml index f6b619e..91b65c9 100644 --- a/backend/docs/swagger.yaml +++ b/backend/docs/swagger.yaml @@ -121,6 +121,27 @@ definitions: userID: type: string type: object + models.User: + properties: + address: + type: string + device_id: + type: string + email: + type: string + first_name: + type: string + last_name: + type: string + pfp_s3_url: + type: string + phone: + type: string + push_notification_enabled: + type: boolean + user_id: + type: string + type: object task_labels.LabelData: properties: group_id: @@ -172,6 +193,19 @@ definitions: task_type: type: string type: object + user.UserInfoBody: + properties: + address: + type: string + email: + type: string + first_name: + type: string + last_name: + type: string + phone: + type: string + type: object info: contact: {} description: This is an API for the Care-Wallet App. @@ -749,4 +783,103 @@ paths: summary: Get Filtered Tasks tags: - tasks + /user: + get: + description: gets the information about multiple users given their user id + parameters: + - collectionFormat: csv + description: User IDs + in: query + items: + type: string + name: userIDs + required: true + type: array + responses: + "200": + description: OK + schema: + items: + $ref: '#/definitions/models.User' + type: array + "400": + description: Bad Request + schema: + type: string + summary: gets the information about multiple users + tags: + - user + /user/{uid}: + get: + description: gets the information about a user given their user id + parameters: + - description: User ID + in: path + name: uid + required: true + type: string + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: + type: string + summary: gets the information about a user + tags: + - user + post: + description: Creates a new user with the provided userId. + parameters: + - description: User ID + in: path + name: uid + required: true + type: string + - description: User Information + in: body + name: UserInfo + required: true + schema: + $ref: '#/definitions/user.UserInfoBody' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: + type: string + summary: Creates a user + tags: + - user + put: + description: Updates a user with the provided userId given the updated user. + parameters: + - description: User ID + in: path + name: uid + required: true + type: string + - description: User Information + in: body + name: UserInfo + required: true + schema: + $ref: '#/definitions/user.UserInfoBody' + responses: + "200": + description: OK + schema: + $ref: '#/definitions/models.User' + "400": + description: Bad Request + schema: + type: string + summary: Updates a user + tags: + - user swagger: "2.0" diff --git a/backend/main.go b/backend/main.go index 4b25ed5..e5f1ee6 100644 --- a/backend/main.go +++ b/backend/main.go @@ -11,6 +11,7 @@ import ( "carewallet/schema/medication" "carewallet/schema/task_labels" "carewallet/schema/tasks" + "carewallet/schema/user" "fmt" "os" @@ -46,6 +47,8 @@ func main() { files.FileGroup(v1, &files.PgModel{Conn: conn}) + user.UserGroup(v1, &user.PgModel{Conn: conn}) + group := v1.Group("group") { groups.CareGroups(group, &groups.PgModel{Conn: conn}) diff --git a/backend/schema/group-roles/routes.go b/backend/schema/group-roles/routes.go index 3138d22..93b7c80 100644 --- a/backend/schema/group-roles/routes.go +++ b/backend/schema/group-roles/routes.go @@ -58,12 +58,19 @@ func (pg *PgModel) GetGroupByUID(c *gin.Context) { // @router /group/{groupId}/roles [get] func (pg *PgModel) GetGroupRoles(c *gin.Context) { gid := c.Param("groupId") - if gidInt, err := strconv.Atoi(gid); err == nil { - if careGroups, err := GetAllGroupRolesFromDB(pg.Conn, gidInt); err == nil { - c.JSON(http.StatusOK, careGroups) - return - } + gidInt, err := strconv.Atoi(gid) + if err != nil { c.JSON(http.StatusBadRequest, err.Error()) + return } + + careGroups, err := GetAllGroupRolesFromDB(pg.Conn, gidInt) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, careGroups) } diff --git a/backend/schema/group-roles/transactions.go b/backend/schema/group-roles/transactions.go index b27582c..b38a363 100644 --- a/backend/schema/group-roles/transactions.go +++ b/backend/schema/group-roles/transactions.go @@ -39,7 +39,7 @@ func GetAllGroupRolesFromDB(pool *pgx.Conn, gid int) ([]models.GroupRole, error) err := rows.Scan(&gr.GroupID, &gr.UserID, &gr.Role) if err != nil { - print(err, "from transactions err2 ") + print(err.Error(), "from transactions err2 ") return nil, err } diff --git a/backend/schema/medication/routes.go b/backend/schema/medication/routes.go index 885274e..b61e7f4 100644 --- a/backend/schema/medication/routes.go +++ b/backend/schema/medication/routes.go @@ -34,7 +34,8 @@ func (pg *PgModel) GetMedications(c *gin.Context) { med, err := GetAllMedsFromDB(pg.Conn) if err != nil { - panic(err) + c.JSON(http.StatusBadRequest, err.Error()) + return } c.JSON(http.StatusOK, med) diff --git a/backend/schema/medication/transactions.go b/backend/schema/medication/transactions.go index 1416f0e..34d323b 100644 --- a/backend/schema/medication/transactions.go +++ b/backend/schema/medication/transactions.go @@ -25,7 +25,6 @@ func GetAllMedsFromDB(pool *pgx.Conn) ([]models.Medication, error) { if err != nil { print(err, "from transactions err2 ") - return nil, err } diff --git a/backend/schema/user/routes.go b/backend/schema/user/routes.go new file mode 100644 index 0000000..37d13d4 --- /dev/null +++ b/backend/schema/user/routes.go @@ -0,0 +1,152 @@ +package user + +import ( + "carewallet/models" + "net/http" + "strings" + + "github.com/gin-gonic/gin" + "github.com/jackc/pgx" +) + +type PgModel struct { + Conn *pgx.Conn +} + +func UserGroup(v1 *gin.RouterGroup, c *PgModel) *gin.RouterGroup { + userGroup := v1.Group("user") + { + userGroup.GET("/:uid", c.GetUser) + userGroup.GET("", c.GetUsers) + userGroup.POST("/:uid", c.CreateUser) + userGroup.PUT("/:uid", c.UpdateUser) + } + + return userGroup +} + +type UserInfoBody struct { + FirstName string `json:"first_name"` + LastName string `json:"last_name"` + Email string `json:"email"` + Phone string `json:"phone"` + Address string `json:"address"` +} + +// CreateUser godoc +// +// @summary Creates a user +// @description Creates a new user with the provided userId. +// @tags user +// +// @param uid path string true "User ID" +// @param UserInfo body UserInfoBody true "User Information" +// +// @success 200 {object} models.User +// @failure 400 {object} string +// @router /user/{uid} [POST] +func (pg *PgModel) CreateUser(c *gin.Context) { + var requestBody UserInfoBody + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + user, err := CreateUserInDB(pg.Conn, c.Param("uid"), requestBody) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, user) +} + +// GetUser godoc +// +// @summary gets the information about a user +// @description gets the information about a user given their user id +// @tags user +// +// @param uid path string true "User ID" +// +// @success 200 {object} models.User +// @failure 400 {object} string +// @router /user/{uid} [GET] +func (pg *PgModel) GetUser(c *gin.Context) { + user, err := GetUserInDB(pg.Conn, c.Param("uid")) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, user) +} + +type UsersQuery struct { + UserIDs []string `form:"userIDs"` +} + +// GetUsers godoc +// +// @summary gets the information about multiple users +// @description gets the information about multiple users given their user id +// @tags user +// +// @param userIDs query []string true "User IDs" +// +// @success 200 {array} models.User +// @failure 400 {object} string +// @router /user [GET] +func (pg *PgModel) GetUsers(c *gin.Context) { + + userIDs := c.Query("userIDs") + userQuery := UsersQuery{ + UserIDs: strings.Split(userIDs, ","), + } + + var users []models.User + + for _, element := range userQuery.UserIDs { + user, err := GetUserInDB(pg.Conn, element) + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + users = append(users, user) + } + + c.JSON(http.StatusOK, users) +} + +// UpdateUser godoc +// +// @summary Updates a user +// @description Updates a user with the provided userId given the updated user. +// @tags user +// +// @param uid path string true "User ID" +// @param UserInfo body UserInfoBody true "User Information" +// +// @success 200 {object} models.User +// @failure 400 {object} string +// @router /user/{uid} [PUT] +func (pg *PgModel) UpdateUser(c *gin.Context) { + var requestBody UserInfoBody + + if err := c.BindJSON(&requestBody); err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + user, err := UpdateUserInDB(pg.Conn, c.Param("uid"), requestBody) + + if err != nil { + c.JSON(http.StatusBadRequest, err.Error()) + return + } + + c.JSON(http.StatusOK, user) +} diff --git a/backend/schema/user/transactions.go b/backend/schema/user/transactions.go new file mode 100644 index 0000000..b3f62ef --- /dev/null +++ b/backend/schema/user/transactions.go @@ -0,0 +1,44 @@ +package user + +import ( + "carewallet/models" + + "github.com/jackc/pgx" +) + +func CreateUserInDB(conn *pgx.Conn, uid string, requestBody UserInfoBody) (models.User, error) { + var user models.User + + err := conn.QueryRow("INSERT INTO users (user_id, first_name, last_name, phone, email, address) VALUES ($1, $2, $3, $4, $5, $6) RETURNING user_id, first_name, last_name, phone, email, address", uid, requestBody.FirstName, requestBody.LastName, requestBody.Phone, requestBody.Email, requestBody.Address).Scan(&user.UserID, &user.FirstName, &user.LastName, &user.Phone, &user.Email, &user.Address) + + if err != nil { + print(err.Error(), "from transactions err ") + return models.User{}, err + } + + return user, nil +} + +func UpdateUserInDB(conn *pgx.Conn, uid string, requestBody UserInfoBody) (models.User, error) { + var user models.User + err := conn.QueryRow("UPDATE users SET first_name = $1, last_name = $2, phone = $3, email = $4, address = $5 RETURNING user_id, first_name, last_name, phone, email, address", requestBody.FirstName, requestBody.LastName, requestBody.Phone, requestBody.Email, requestBody.Address).Scan(&user.UserID, &user.FirstName, &user.LastName, &user.Phone, &user.Email, &user.Address) + + if err != nil { + print(err.Error(), "from transactions err ") + return models.User{}, err + } + + return user, nil +} + +func GetUserInDB(conn *pgx.Conn, uid string) (models.User, error) { + var user models.User + err := conn.QueryRow("SELECT user_id, first_name, last_name, phone, email, address FROM users WHERE user_id = $1", uid).Scan(&user.UserID, &user.FirstName, &user.LastName, &user.Phone, &user.Email, &user.Address) + + if err != nil { + print(err.Error(), "from transactions err ") + return models.User{}, err + } + + return user, nil +} diff --git a/backend/schema/user/user_test.go b/backend/schema/user/user_test.go new file mode 100644 index 0000000..a00006b --- /dev/null +++ b/backend/schema/user/user_test.go @@ -0,0 +1 @@ +package user diff --git a/client/assets/arrow-left.svg b/client/assets/arrow-left.svg new file mode 100644 index 0000000..99dedf4 --- /dev/null +++ b/client/assets/arrow-left.svg @@ -0,0 +1,4 @@ + + + + diff --git a/client/assets/bottom-nav/bell.svg b/client/assets/bottom-nav/bell.svg new file mode 100644 index 0000000..c3e3954 --- /dev/null +++ b/client/assets/bottom-nav/bell.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/assets/bottom-nav/calendar.svg b/client/assets/bottom-nav/calendar.svg new file mode 100644 index 0000000..f6135fc --- /dev/null +++ b/client/assets/bottom-nav/calendar.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/client/assets/bottom-nav/home.svg b/client/assets/bottom-nav/home.svg new file mode 100644 index 0000000..917ea06 --- /dev/null +++ b/client/assets/bottom-nav/home.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/client/assets/bottom-nav/user.svg b/client/assets/bottom-nav/user.svg new file mode 100644 index 0000000..e0879e6 --- /dev/null +++ b/client/assets/bottom-nav/user.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/client/assets/home.svg b/client/assets/home.svg deleted file mode 100644 index 1449ac4..0000000 --- a/client/assets/home.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/client/assets/profile/ellipse.svg b/client/assets/profile/ellipse.svg new file mode 100644 index 0000000..bff7bc4 --- /dev/null +++ b/client/assets/profile/ellipse.svg @@ -0,0 +1,3 @@ + + + diff --git a/client/components/profile/CircleCard.tsx b/client/components/profile/CircleCard.tsx new file mode 100644 index 0000000..ba2f8c9 --- /dev/null +++ b/client/components/profile/CircleCard.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Pressable, Text, View } from 'react-native'; + +interface CircleCardProps { + onTouchEnd?: () => void; + ButtonText: string; +} +export function CircleCard({ onTouchEnd, ButtonText }: CircleCardProps) { + return ( + + + + {ButtonText} + + + ); +} diff --git a/client/components/profile/Group.tsx b/client/components/profile/Group.tsx new file mode 100644 index 0000000..18bcb07 --- /dev/null +++ b/client/components/profile/Group.tsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; +import { + ActivityIndicator, + FlatList, + Pressable, + Text, + View +} from 'react-native'; + +import { GroupRole, Role } from '../../types/group'; +import { User } from '../../types/user'; + +interface GroupProps { + roles: GroupRole[]; + rolesAreLoading: boolean; + setActiveUser: (userID: string) => void; + activeUser: string; + users: User[]; + usersAreLoading: boolean; +} + +export function Group({ + roles, + rolesAreLoading, + usersAreLoading, + users, + setActiveUser, + activeUser +}: GroupProps) { + const [canPress, setCanPress] = useState(true); + + if (rolesAreLoading || usersAreLoading) { + return ( + + + Loading... + + ); + } + + if (!roles || !users) { + return ( + + Could Not Load Group... + + ); + } + + return ( + setCanPress(false)} + onScrollEndDrag={() => setCanPress(true)} + horizontal + showsHorizontalScrollIndicator={false} + data={users.filter( + (user) => + user.user_id !== activeUser && + user.user_id !== + (roles.find((role) => role.role === Role.PATIENT)?.user_id ?? '') + )} + renderItem={({ item, index }) => ( + { + if (canPress) setActiveUser(item.user_id); + }} + > + + + {item.first_name} + + + )} + /> + ); +} diff --git a/client/components/profile/Header.tsx b/client/components/profile/Header.tsx new file mode 100644 index 0000000..b386ec4 --- /dev/null +++ b/client/components/profile/Header.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Text, View } from 'react-native'; + +import { useNavigation } from '@react-navigation/native'; +import { styled } from 'nativewind'; + +import ArrowLeft from '../../assets/arrow-left.svg'; +import Ellipse from '../../assets/profile/ellipse.svg'; +import { AppStackNavigation } from '../../navigation/AppNavigation'; +import { GroupRole } from '../../types/group'; +import { User } from '../../types/user'; +import { ProfileTopHeader } from './ProfileTopHeader'; + +const StyledEllipse = styled(Ellipse); + +interface HeaderProps { + user: User | undefined; + role: GroupRole | undefined; +} + +export function Header({ user, role }: HeaderProps) { + const navigate = useNavigation(); + + if (!user) return null; + + return ( + <> + + + } + rightButtonText="Edit" + /> + + {`${role?.role.charAt(0)}${role?.role.slice(1).toLowerCase()} Caregiver`} + + + {user.phone ? user.phone : user.email} + + + + + + + + + ); +} diff --git a/client/components/profile/ProfileTopHeader.tsx b/client/components/profile/ProfileTopHeader.tsx new file mode 100644 index 0000000..ae2acd7 --- /dev/null +++ b/client/components/profile/ProfileTopHeader.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Pressable, Text, View } from 'react-native'; + +import { User } from '../../types/user'; + +interface ProfileTopHeaderProps { + user: User; + onTouchEndLeft?: () => void; + leftButtonText?: JSX.Element | string; + onTouchEndRight?: () => void; + rightButtonText?: string; +} + +export function ProfileTopHeader({ + user, + onTouchEndLeft, + leftButtonText, + onTouchEndRight, + rightButtonText +}: ProfileTopHeaderProps) { + return ( + + + + {leftButtonText} + + + + {user.first_name} {user.last_name} + + + + {rightButtonText} + + + + ); +} diff --git a/client/contexts/CareWalletContext.tsx b/client/contexts/CareWalletContext.tsx index e97ed1d..448a71c 100644 --- a/client/contexts/CareWalletContext.tsx +++ b/client/contexts/CareWalletContext.tsx @@ -2,6 +2,7 @@ import React, { createContext, useContext, useEffect, useState } from 'react'; import { getAuth, onAuthStateChanged } from 'firebase/auth'; +import { getUserGroup } from './api'; import { Group, User } from './types'; type CareWalletContextData = { @@ -29,10 +30,12 @@ export function CareWalletProvider({ setUser(signedInUser); - setGroup({ - groupID: 5, - role: 'TEMP' - }); + // 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 }); + }); + } }); }, []); diff --git a/client/contexts/api.ts b/client/contexts/api.ts new file mode 100644 index 0000000..a1fcb55 --- /dev/null +++ b/client/contexts/api.ts @@ -0,0 +1,9 @@ +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/navigation/AppNavigation.tsx b/client/navigation/AppNavigation.tsx index 945797f..724fff7 100644 --- a/client/navigation/AppNavigation.tsx +++ b/client/navigation/AppNavigation.tsx @@ -10,6 +10,7 @@ export type AppStackParamList = { Main: undefined; Home: undefined; Login: undefined; + Profile: undefined; }; export type AppStackNavigation = NavigationProp; diff --git a/client/navigation/AppStackBottomTabNavigator.tsx b/client/navigation/AppStackBottomTabNavigator.tsx index ae71d1a..6793920 100644 --- a/client/navigation/AppStackBottomTabNavigator.tsx +++ b/client/navigation/AppStackBottomTabNavigator.tsx @@ -3,32 +3,58 @@ import { Text } from 'react-native'; import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; -import Home from '../assets/home.svg'; -import GroupScreen from '../screens/Groups'; +import Bell from '../assets/bottom-nav/bell.svg'; +import Calendar from '../assets/bottom-nav/calendar.svg'; +import Home from '../assets/bottom-nav/home.svg'; +import User from '../assets/bottom-nav/user.svg'; import MedicationList from '../screens/MedicationList'; +import Profile from '../screens/Profile'; const AppStackBottomTab = createBottomTabNavigator(); export function AppStackBottomTabNavigator() { return ( - + , - tabBarLabel: () => Landing + tabBarIcon: ({ color }) => , + tabBarLabel: () => }} component={MedicationList} /> , - tabBarLabel: () => Group + tabBarIcon: ({ color }) => , + tabBarLabel: () => }} - component={GroupScreen} + component={MedicationList} + /> + , + tabBarLabel: () => + }} + component={MedicationList} + /> + , + tabBarLabel: () => + }} + component={Profile} /> ); diff --git a/client/screens/Groups.tsx b/client/screens/Groups.tsx deleted file mode 100644 index 51a002a..0000000 --- a/client/screens/Groups.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Button, StyleSheet, Text, TextInput, View } from 'react-native'; - -import { - addUserToCareGroup, - createCareGroup, - getGroupMembers -} from '../services/group'; - -export default function GroupScreen() { - const [groupName, setGroupName] = useState(''); - const [userId, setUserId] = useState(''); - const [role, setRole] = useState(''); - // Setting this default 1 for now (to test get members), - // but will automically be set to the new group id when a group is created - const [groupId, setGroupId] = useState('1'); - const [members, setMembers] = useState([]); - - // Fetch group members when groupId changes - useEffect(() => { - if (groupId) { - getGroupMembers(groupId) - .then((data) => setMembers(data)) - .catch((error) => console.error('Error fetching members:', error)); - } - }, [groupId]); - - // Create a new care group - const handleCreateGroup = async () => { - try { - const newGroupId = await createCareGroup(groupName); - setGroupId(newGroupId.toString()); - } catch (error) { - console.error('Error creating group:', error); - } - }; - - // Add a user to the care group - const handleAddUser = async () => { - try { - await addUserToCareGroup(userId, groupId, role); - getGroupMembers(groupId) - .then((data) => setMembers(data)) - .catch((error) => console.error('Error fetching members:', error)); - } catch (error) { - console.error('Error adding user to group:', error); - } - }; - - // Rendering (scuffed but it works for now) - return ( - - -