From a22ab9dde9b3d2e6077ebc04cfb7b156aa786a9a Mon Sep 17 00:00:00 2001 From: Owen Sellner <43042601+owen-sellner@users.noreply.github.com> Date: Wed, 8 Nov 2023 19:29:21 -0500 Subject: [PATCH 01/23] fix zIndex (#193) --- frontend/src/theme/common/tableStyles.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/frontend/src/theme/common/tableStyles.tsx b/frontend/src/theme/common/tableStyles.tsx index 5da1320e..e07cb9b9 100644 --- a/frontend/src/theme/common/tableStyles.tsx +++ b/frontend/src/theme/common/tableStyles.tsx @@ -17,7 +17,6 @@ const Table: ComponentStyleConfig = { thead: { position: "sticky", top: 0, - zIndex: "docked", }, }, }, From b7ef41dcc375411512fffd108f2a6109bf969faa Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Sun, 12 Nov 2023 01:19:43 -0500 Subject: [PATCH 02/23] Invite user script (#199) * add invite user script * update README * readme formatting * update readme one last time * update readme one last time --- README.md | 11 ++++++++++- seeding/{create-user.sh => invite-user.sh} | 17 +++++++---------- 2 files changed, 17 insertions(+), 11 deletions(-) rename seeding/{create-user.sh => invite-user.sh} (71%) diff --git a/README.md b/README.md index 8df768fe..86bd3c4d 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,16 @@ cd supportive-housing docker-compose up --build ``` +4. Create an Admin user. In `seeding/.env`, ensure `FIRST_NAME`, `LAST_NAME`, and `EMAIL` are set (you should use your Blueprint email/any email you have access to here). Ensure `ROLE` is set to `Admin`. Run: +```bash +bash ./seeding/invite-user.sh +``` +**IMPORTANT**: If you've reset your local DB and want to re-use an email, ensure it's deleted from Firebase as well (ask the PL for access if you don't have it) + +5. Signup for an account on the app! Ensure that you use the values you used in Step 3. Your password can be anything you remember + +6. Verify your email address. You should receive an email in your inbox with a link - once you click the link, you're good to freely use the app! You can invite any other users through the `Employee Directory` within the `Admin Controls` + ## Useful Commands ### Database Migration @@ -75,7 +85,6 @@ bash ./scripts/restart-docker.sh ### Seeding Before running these scripts, remember to update the `.env` file to ensure you're configuring your data to your needs: ```bash -bash ./seeding/create-user.sh # Create a user with a specific name and role bash ./seeding/create-residents.sh # Create a number of residents bash ./seeding/create-log-records.sh # Create a number of log records ``` diff --git a/seeding/create-user.sh b/seeding/invite-user.sh similarity index 71% rename from seeding/create-user.sh rename to seeding/invite-user.sh index ebee86c2..fb3dae74 100644 --- a/seeding/create-user.sh +++ b/seeding/invite-user.sh @@ -12,19 +12,16 @@ run_sql_script " INSERT INTO users ( first_name, last_name, - auth_id, - role + role, + user_status, + email ) -SELECT +VALUES ( '$FIRST_NAME', '$LAST_NAME', - '$AUTH_ID', - '$ROLE' -WHERE NOT EXISTS ( - SELECT 1 - FROM users - WHERE first_name = '$FIRST_NAME' - AND last_name = '$LAST_NAME' + '$ROLE', + 'Invited', + '$EMAIL' ); " From f912d507b01483b4763147754aa7f5fcfe227c59 Mon Sep 17 00:00:00 2001 From: Carolyn Zhang <82423082+carolynzhang18@users.noreply.github.com> Date: Fri, 17 Nov 2023 16:52:05 -0500 Subject: [PATCH 03/23] Create tags route (#197) * Add unique constraint to tag name * Create tag route * Modify update route to catch IntegrityError * Add name for migration downgrade --- backend/app/models/tags.py | 2 +- backend/app/rest/tags_routes.py | 15 +++++++ .../services/implementations/tags_service.py | 41 ++++++++++++++----- ...0ea2257f1dc6_tag_name_unique_constraint.py | 32 +++++++++++++++ 4 files changed, 78 insertions(+), 12 deletions(-) create mode 100644 backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py diff --git a/backend/app/models/tags.py b/backend/app/models/tags.py index 18a6194d..294744e7 100644 --- a/backend/app/models/tags.py +++ b/backend/app/models/tags.py @@ -8,7 +8,7 @@ class Tag(db.Model): __tablename__ = "tags" tag_id = db.Column(db.Integer, primary_key=True, nullable=False) - name = db.Column(db.String, nullable=False) + name = db.Column(db.String, unique=True, nullable=False) status = db.Column(db.Enum("Deleted", "Active", name="status"), nullable=False) log_records = db.relationship( "LogRecords", secondary="log_record_tag", back_populates="tags" diff --git a/backend/app/rest/tags_routes.py b/backend/app/rest/tags_routes.py index b37e2b50..1be7211b 100644 --- a/backend/app/rest/tags_routes.py +++ b/backend/app/rest/tags_routes.py @@ -65,3 +65,18 @@ def update_tag(tag_id): except Exception as e: error_message = getattr(e, "message", None) return jsonify({"error": (error_message if error_message else str(e))}), 500 + + +@blueprint.route("/", methods=["POST"], strict_slashes=False) +@require_authorization_by_role({"Admin"}) +def create_tag(): + """ + Create a tag + """ + tag = request.json + try: + created_tag = tags_service.create_tag(tag) + return jsonify(created_tag), 201 + except Exception as e: + error_message = getattr(e, "message", None) + return jsonify({"error": (error_message if error_message else str(e))}), 500 diff --git a/backend/app/services/implementations/tags_service.py b/backend/app/services/implementations/tags_service.py index 6d81f363..e8befd1d 100644 --- a/backend/app/services/implementations/tags_service.py +++ b/backend/app/services/implementations/tags_service.py @@ -33,14 +33,33 @@ def delete_tag(self, tag_id): def update_tag(self, tag_id, updated_tag): updated_name = updated_tag["name"] - name_check = Tag.query.filter_by(name=updated_name).first() - if name_check is not None: - raise Exception("Tag name {name} already exists".format(name=updated_name)) - create_update_tag = Tag.query.filter_by(tag_id=tag_id).update( - { - **updated_tag, - } - ) - if not create_update_tag: - raise Exception("Tag with id {tag_id} not found".format(tag_id=tag_id)) - db.session.commit() + try: + create_update_tag = Tag.query.filter_by(tag_id=tag_id).update( + { + **updated_tag, + } + ) + if not create_update_tag: + raise Exception("Tag with id {tag_id} not found".format(tag_id=tag_id)) + db.session.commit() + except Exception as error: + if type(error).__name__ == "IntegrityError": + raise Exception( + "Tag name {name} already exists".format(name=updated_name) + ) + else: + raise error + + def create_tag(self, tag): + try: + new_tag = Tag(**tag) + db.session.add(new_tag) + db.session.commit() + return tag + except Exception as error: + if type(error).__name__ == "IntegrityError": + raise Exception( + "Tag name {name} already exists".format(name=tag["name"]) + ) + else: + raise error diff --git a/backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py b/backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py new file mode 100644 index 00000000..feb2567c --- /dev/null +++ b/backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py @@ -0,0 +1,32 @@ +"""tag name unique constraint + +Revision ID: 0ea2257f1dc6 +Revises: 8b5132609f1f +Create Date: 2023-11-08 20:53:54.334014 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "0ea2257f1dc6" +down_revision = "8b5132609f1f" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("tags", schema=None) as batch_op: + batch_op.create_unique_constraint(None, ["name"]) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("tags", schema=None) as batch_op: + batch_op.drop_constraint("tags_name_key", type_="unique") + + # ### end Alembic commands ### From 90a181e647945216a0c10c354be71316674fb4a0 Mon Sep 17 00:00:00 2001 From: Braydon Wang Date: Wed, 22 Nov 2023 19:45:30 -0500 Subject: [PATCH 04/23] Log record many residents: Backend (#192) * Create & migrate log_record_residents table * changed add record function * change delete and update * modify update logic * change get function * changed filtering by residents * modify comments * change filter logic again * initial frontend changes * fix count function * changing create and edit * Enable multiselect for residents * small fixes * Enable resident multiselect when creating log * small changes * Fix residents array for API request * Multiselect residents for edit log * testing some stuff * fix edit log * change migration files * send empty tags array * change filter query to filter by initial + room num * Pass resident labels * Revert "change filter query to filter by initial + room num" This reverts commit 173d504e0356d8d77bca49de1287d051840798a1. * Revert "Pass resident labels" This reverts commit 3b03c7f5134424a4beb4801abf7dcb146deeb09c. * resolve pr comments * run lint function * run linter * resolve PR comments * Style residents dropdown * Minor style fixes --------- Co-authored-by: Carolyn Zhang --- backend/app/models/__init__.py | 1 + backend/app/models/log_record_residents.py | 34 +++++++++++ backend/app/models/log_records.py | 4 +- backend/app/models/residents.py | 3 + .../implementations/log_records_service.py | 58 ++++++++++++++----- .../interfaces/log_records_service.py | 2 +- .../0c76a8fe211d_add_log_record_residents.py | 54 +++++++++++++++++ .../versions/8b5132609f1f_merging.py | 24 -------- ...d51_add_status_and_email_to_users_table.py | 2 +- frontend/src/APIClients/LogRecordAPIClient.ts | 16 ++--- .../src/components/forms/CreateEmployee.tsx | 2 +- frontend/src/components/forms/CreateLog.tsx | 33 ++++++----- .../src/components/forms/CreateResident.tsx | 2 +- frontend/src/components/forms/EditLog.tsx | 45 ++++++++------ .../src/components/forms/EditResident.tsx | 2 +- .../pages/AdminControls/EmployeeDirectory.tsx | 8 +-- .../AdminControls/EmployeeDirectoryTable.tsx | 10 +--- .../components/pages/HomePage/HomePage.tsx | 4 +- .../pages/HomePage/LogRecordsTable.tsx | 13 ++--- .../ResidentDirectory/ResidentDirectory.tsx | 4 +- .../ResidentDirectoryTable.tsx | 13 ++--- frontend/src/helper/CSVConverter.tsx | 4 +- frontend/src/types/CSVLog.ts | 2 +- frontend/src/types/LogRecordTypes.ts | 10 ++-- 24 files changed, 227 insertions(+), 123 deletions(-) create mode 100644 backend/app/models/log_record_residents.py create mode 100644 backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py delete mode 100644 backend/migrations/versions/8b5132609f1f_merging.py diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index b40e490d..f85fb4f8 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -14,6 +14,7 @@ def init_app(app): from .log_record_tags import LogRecordTag from .residents import Residents from .buildings import Buildings + from .log_record_residents import LogRecordResidents app.app_context().push() db.init_app(app) diff --git a/backend/app/models/log_record_residents.py b/backend/app/models/log_record_residents.py new file mode 100644 index 00000000..2ea12f25 --- /dev/null +++ b/backend/app/models/log_record_residents.py @@ -0,0 +1,34 @@ +from sqlalchemy import inspect +from sqlalchemy.orm.properties import ColumnProperty + +from . import db + + +class LogRecordResidents(db.Model): + __tablename__ = "log_record_residents" + + id = db.Column(db.Integer, primary_key=True, nullable=False) + log_record_id = db.Column( + db.Integer, db.ForeignKey("log_records.log_id"), nullable=False + ) + resident_id = db.Column(db.Integer, db.ForeignKey("residents.id"), nullable=False) + + def to_dict(self, include_relationships=False): + # define the entities table + cls = type(self) + + mapper = inspect(cls) + formatted = {} + for column in mapper.attrs: + field = column.key + attr = getattr(self, field) + # if it's a regular column, extract the value + if isinstance(column, ColumnProperty): + formatted[field] = attr + # otherwise, it's a relationship field + # (currently not applicable, but may be useful for entity groups) + elif include_relationships: + # recursively format the relationship + # don't format the relationship's relationships + formatted[field] = [obj.to_dict() for obj in attr] + return formatted diff --git a/backend/app/models/log_records.py b/backend/app/models/log_records.py index aab98794..2243a524 100644 --- a/backend/app/models/log_records.py +++ b/backend/app/models/log_records.py @@ -7,7 +7,6 @@ class LogRecords(db.Model): __tablename__ = "log_records" log_id = db.Column(db.Integer, primary_key=True, nullable=False) employee_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False) - resident_id = db.Column(db.Integer, db.ForeignKey("residents.id"), nullable=False) datetime = db.Column(db.DateTime(timezone=True), nullable=False) flagged = db.Column(db.Boolean, nullable=False) attn_to = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=True) @@ -17,6 +16,9 @@ class LogRecords(db.Model): tags = db.relationship( "Tag", secondary="log_record_tag", back_populates="log_records" ) + residents = db.relationship( + "Residents", secondary="log_record_residents", back_populates="log_records" + ) building = db.relationship("Buildings", back_populates="log_record") def to_dict(self, include_relationships=False): diff --git a/backend/app/models/residents.py b/backend/app/models/residents.py index 8e2bbc3a..51611c42 100644 --- a/backend/app/models/residents.py +++ b/backend/app/models/residents.py @@ -12,6 +12,9 @@ class Residents(db.Model): date_left = db.Column(db.Date, nullable=True) building_id = db.Column(db.Integer, db.ForeignKey("buildings.id"), nullable=False) building = db.relationship("Buildings", back_populates="resident") + log_records = db.relationship( + "LogRecords", secondary="log_record_residents", back_populates="residents" + ) resident_id = db.column_property(initial + cast(room_num, String)) diff --git a/backend/app/services/implementations/log_records_service.py b/backend/app/services/implementations/log_records_service.py index 2d0c9002..17c33c20 100644 --- a/backend/app/services/implementations/log_records_service.py +++ b/backend/app/services/implementations/log_records_service.py @@ -1,5 +1,7 @@ from ..interfaces.log_records_service import ILogRecordsService from ...models.log_records import LogRecords +from ...models.residents import Residents +from ...models.user import User from ...models.tags import Tag from ...models import db from datetime import datetime @@ -23,12 +25,15 @@ def __init__(self, logger): def add_record(self, log_record): new_log_record = log_record.copy() - + residents = new_log_record["residents"] tag_names = new_log_record["tags"] + + del new_log_record["residents"] del new_log_record["tags"] try: new_log_record = LogRecords(**new_log_record) + self.construct_residents(new_log_record, residents) self.construct_tags(new_log_record, tag_names) db.session.add(new_log_record) @@ -37,6 +42,15 @@ def add_record(self, log_record): except Exception as postgres_error: raise postgres_error + def construct_residents(self, log_record, residents): + residents = list(set(residents)) + for resident_id in residents: + resident = Residents.query.filter_by(id=resident_id).first() + + if not resident: + raise Exception(f"Resident with id {resident_id} does not exist") + log_record.residents.append(resident) + def construct_tags(self, log_record, tag_names): for tag_name in tag_names: tag = Tag.query.filter_by(name=tag_name).first() @@ -57,7 +71,7 @@ def to_json_list(self, logs): "first_name": log[2], "last_name": log[3], }, - "resident_id": log[4], + "residents": log[4], "attn_to": { "id": log[5], "first_name": log[6], @@ -94,13 +108,15 @@ def filter_by_employee_id(self, employee_id): return sql_statement return f"\nemployee_id={employee_id}" - def filter_by_resident_id(self, resident_id): - if type(resident_id) == list: - sql_statement = f"\nresident_id={resident_id[0]}" - for i in range(1, len(resident_id)): - sql_statement = sql_statement + f"\nOR resident_id={resident_id[i]}" + def filter_by_residents(self, residents): + if type(residents) == list: + sql_statement = f"\n'{residents[0]}'=ANY (resident_ids)" + for i in range(1, len(residents)): + sql_statement = ( + sql_statement + f"\nAND '{residents[i]}'=ANY (resident_ids)" + ) return sql_statement - return f"\nresident_id={resident_id}" + return f"\n'{residents}'=ANY (resident_ids)" def filter_by_attn_to(self, attn_to): if type(attn_to) == list: @@ -150,7 +166,7 @@ def filter_log_records(self, filters=None): options = { "building_id": self.filter_by_building_id, "employee_id": self.filter_by_employee_id, - "resident_id": self.filter_by_resident_id, + "residents": self.filter_by_residents, "attn_to": self.filter_by_attn_to, "date_range": self.filter_by_date_range, "tags": self.filter_by_tags, @@ -166,6 +182,14 @@ def filter_log_records(self, filters=None): sql = sql + "\nAND " + options[filter](filters.get(filter)) return sql + def join_resident_attributes(self): + return "\nLEFT JOIN\n \ + (SELECT logs.log_id, ARRAY_AGG(residents.id) AS resident_ids, ARRAY_AGG(CONCAT(residents.initial, residents.room_num)) AS residents FROM log_records logs\n \ + JOIN log_record_residents lrr ON logs.log_id = lrr.log_record_id\n \ + JOIN residents ON lrr.resident_id = residents.id\n \ + GROUP BY logs.log_id \n \ + ) r ON logs.log_id = r.log_id\n" + def join_tag_attributes(self): return "\nLEFT JOIN\n \ (SELECT logs.log_id, ARRAY_AGG(tags.name) AS tag_names FROM log_records logs\n \ @@ -183,7 +207,7 @@ def get_log_records( logs.employee_id,\n \ employees.first_name AS employee_first_name,\n \ employees.last_name AS employee_last_name,\n \ - CONCAT(residents.initial, residents.room_num) AS resident_id,\n \ + r.residents,\n \ logs.attn_to,\n \ attn_tos.first_name AS attn_to_first_name,\n \ attn_tos.last_name AS attn_to_last_name,\n \ @@ -196,9 +220,9 @@ def get_log_records( FROM log_records logs\n \ LEFT JOIN users attn_tos ON logs.attn_to = attn_tos.id\n \ JOIN users employees ON logs.employee_id = employees.id\n \ - JOIN residents ON logs.resident_id = residents.id\n \ JOIN buildings on logs.building_id = buildings.id" + sql += self.join_resident_attributes() sql += self.join_tag_attributes() sql += self.filter_log_records(filters) @@ -225,10 +249,10 @@ def count_log_records(self, filters=None): FROM log_records logs\n \ LEFT JOIN users attn_tos ON logs.attn_to = attn_tos.id\n \ JOIN users employees ON logs.employee_id = employees.id\n \ - JOIN residents ON logs.resident_id = residents.id\n \ JOIN buildings on logs.building_id = buildings.id" - sql += f"\n{self.join_tag_attributes()}" + sql += self.join_resident_attributes() + sql += self.join_tag_attributes() sql += self.filter_log_records(filters) num_results = db.session.execute(text(sql)) @@ -246,6 +270,7 @@ def delete_log_record(self, log_id): raise Exception( "Log record with id {log_id} not found".format(log_id=log_id) ) + log_record_to_delete.residents = [] log_record_to_delete.tags = [] db.session.delete(log_record_to_delete) db.session.commit() @@ -274,10 +299,15 @@ def update_log_record(self, log_id, updated_log_record): LogRecords.tags: None, } ) + + log_record = LogRecords.query.filter_by(log_id=log_id).first() + if log_record: + log_record.residents = [] + self.construct_residents(log_record, updated_log_record["residents"]) + updated_log_record = LogRecords.query.filter_by(log_id=log_id).update( { LogRecords.employee_id: updated_log_record["employee_id"], - LogRecords.resident_id: updated_log_record["resident_id"], LogRecords.flagged: updated_log_record["flagged"], LogRecords.building_id: updated_log_record["building_id"], LogRecords.note: updated_log_record["note"], diff --git a/backend/app/services/interfaces/log_records_service.py b/backend/app/services/interfaces/log_records_service.py index fb13cb44..f002743c 100644 --- a/backend/app/services/interfaces/log_records_service.py +++ b/backend/app/services/interfaces/log_records_service.py @@ -13,7 +13,7 @@ def add_record(self, log_record): :param user_id: user id of the user adding the log record :type user_id: int - :param resident_id: resident's id + :param residents: list of resident ids :param flagged: checkbox if attention is needed :type flagged: boolean :param note: note that user inputs diff --git a/backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py b/backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py new file mode 100644 index 00000000..b4309b69 --- /dev/null +++ b/backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py @@ -0,0 +1,54 @@ +"""add log_record_residents + +Revision ID: 0c76a8fe211d +Revises: 24fad25f60e3 +Create Date: 2023-10-19 00:02:43.259307 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "0c76a8fe211d" +down_revision = "24fad25f60e3" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "log_record_residents", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("log_record_id", sa.Integer(), nullable=False), + sa.Column("resident_id", sa.Integer(), nullable=False), + sa.ForeignKeyConstraint( + ["log_record_id"], + ["log_records.log_id"], + ), + sa.ForeignKeyConstraint( + ["resident_id"], + ["residents.id"], + ), + sa.PrimaryKeyConstraint("id"), + ) + with op.batch_alter_table("log_records", schema=None) as batch_op: + batch_op.drop_constraint("log_records_resident_id_fkey", type_="foreignkey") + batch_op.drop_column("resident_id") + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("log_records", schema=None) as batch_op: + batch_op.add_column( + sa.Column("resident_id", sa.INTEGER(), autoincrement=False, nullable=False) + ) + batch_op.create_foreign_key( + "log_records_resident_id_fkey", "residents", ["resident_id"], ["id"] + ) + + op.drop_table("log_record_residents") + # ### end Alembic commands ### diff --git a/backend/migrations/versions/8b5132609f1f_merging.py b/backend/migrations/versions/8b5132609f1f_merging.py deleted file mode 100644 index 10153b85..00000000 --- a/backend/migrations/versions/8b5132609f1f_merging.py +++ /dev/null @@ -1,24 +0,0 @@ -"""merging - -Revision ID: 8b5132609f1f -Revises: 24fad25f60e3, 65a56c245ad7 -Create Date: 2023-10-04 23:41:43.310280 - -""" -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision = "8b5132609f1f" -down_revision = ("24fad25f60e3", "65a56c245ad7") -branch_labels = None -depends_on = None - - -def upgrade(): - pass - - -def downgrade(): - pass diff --git a/backend/migrations/versions/a2a0a16b6d51_add_status_and_email_to_users_table.py b/backend/migrations/versions/a2a0a16b6d51_add_status_and_email_to_users_table.py index 92447730..5708b312 100644 --- a/backend/migrations/versions/a2a0a16b6d51_add_status_and_email_to_users_table.py +++ b/backend/migrations/versions/a2a0a16b6d51_add_status_and_email_to_users_table.py @@ -12,7 +12,7 @@ # revision identifiers, used by Alembic. revision = "a2a0a16b6d51" -down_revision = "82f36cdf325f" +down_revision = "65a56c245ad7" branch_labels = None depends_on = None diff --git a/frontend/src/APIClients/LogRecordAPIClient.ts b/frontend/src/APIClients/LogRecordAPIClient.ts index 4dc23873..838eae7c 100644 --- a/frontend/src/APIClients/LogRecordAPIClient.ts +++ b/frontend/src/APIClients/LogRecordAPIClient.ts @@ -16,7 +16,7 @@ const countLogRecords = async ({ employeeId = [], attnTo = [], dateRange = [], - residentId = [], + residents = [], tags = [], flagged = false, }: CountLogRecordFilters): Promise => { @@ -32,7 +32,7 @@ const countLogRecords = async ({ employeeId, attnTo, dateRange, - residentId, + residents, tags, flagged, }, @@ -51,7 +51,7 @@ const filterLogRecords = async ({ employeeId = [], attnTo = [], dateRange = [], - residentId = [], + residents = [], tags = [], flagged = false, returnAll = false, @@ -70,7 +70,7 @@ const filterLogRecords = async ({ employeeId, attnTo, dateRange, - residentId, + residents, tags, flagged, }, @@ -89,7 +89,7 @@ const filterLogRecords = async ({ const createLog = async ({ employeeId, - residentId, + residents, datetime, flagged, note, @@ -106,7 +106,7 @@ const createLog = async ({ "/log_records/", { employeeId, - residentId, + residents, datetime, flagged, note, @@ -140,7 +140,7 @@ const deleteLogRecord = async (logId: number): Promise => { const editLogRecord = async ({ logId, employeeId, - residentId, + residents, datetime, flagged, note, @@ -157,7 +157,7 @@ const editLogRecord = async ({ `/log_records/${logId}`, { employeeId, - residentId, + residents, datetime, flagged, note, diff --git a/frontend/src/components/forms/CreateEmployee.tsx b/frontend/src/components/forms/CreateEmployee.tsx index b5215b96..3cd288ed 100644 --- a/frontend/src/components/forms/CreateEmployee.tsx +++ b/frontend/src/components/forms/CreateEmployee.tsx @@ -29,7 +29,7 @@ type Props = { getRecords: (pageNumber: number) => Promise; setUserPageNum: React.Dispatch>; countUsers: () => Promise; -} +}; const RoleOptions = ["Relief Staff", "Admin", "Regular Staff"]; const CreateEmployee = ({ diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index 06317325..19ba75a0 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -120,7 +120,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { }), ); const [buildingId, setBuildingId] = useState(-1); - const [resident, setResident] = useState(-1); + const [residents, setResidents] = useState([]); const [tags, setTags] = useState([]); const [attnTo, setAttnTo] = useState(-1); const [notes, setNotes] = useState(""); @@ -172,14 +172,17 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { setBuildingError(selectedOption === null); }; - const handleResidentChange = ( - selectedOption: SingleValue<{ label: string; value: number }>, + const handleResidentsChange = ( + selectedResidents: MultiValue, ) => { - if (selectedOption !== null) { - setResident(selectedOption.value); + const mutableSelectedResidents: ResidentLabel[] = Array.from( + selectedResidents, + ); + if (mutableSelectedResidents !== null) { + setResidents(mutableSelectedResidents.map((residentLabel) => residentLabel.value)); } - - setResidentError(selectedOption === null); + setResidentError(mutableSelectedResidents.length === 0); + }; const handleTagsChange = ( @@ -245,7 +248,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { }), ); setBuildingId(-1); - setResident(-1); + setResidents([]); setTags([]); setAttnTo(-1); setNotes(""); @@ -272,7 +275,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { setDateError(date === null); setTimeError(time === ""); setBuildingError(buildingId === -1); - setResidentError(resident === -1); + setResidentError(residents.length === 0); setNotesError(notes === ""); // If any required fields are empty, prevent form submission @@ -281,7 +284,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { date === null || time === "" || buildingId === -1 || - resident === -1 || + residents.length === 0 || notes === "" ) { return; @@ -294,7 +297,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { const attentionTo = attnTo === -1 ? undefined : attnTo; const res = await LogRecordAPIClient.createLog({ employeeId: employee.value, - residentId: resident, + residents, datetime: combineDateTime(date, time), flagged, note: notes, @@ -397,11 +400,13 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { - Resident + Residents item.label === logRecord.residentId, + isMulti + closeMenuOnSelect={false} + placeholder="Select Residents" + onChange={handleResidentsChange} + defaultValue={residentOptions.filter( + (item) => logRecord.residents.includes(item.label), )} + styles={selectStyle} /> Resident is required. diff --git a/frontend/src/components/forms/EditResident.tsx b/frontend/src/components/forms/EditResident.tsx index 6f1d10b9..2b4e9022 100644 --- a/frontend/src/components/forms/EditResident.tsx +++ b/frontend/src/components/forms/EditResident.tsx @@ -86,7 +86,7 @@ const EditResident = ({ "Resident has been successfully updated", "success", ); - getRecords(userPageNum) + getRecords(userPageNum); } else { newToast( "Error updating resident", diff --git a/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx b/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx index 96a824a9..be09840a 100644 --- a/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx +++ b/frontend/src/components/pages/AdminControls/EmployeeDirectory.tsx @@ -71,10 +71,10 @@ const EmployeeDirectoryPage = (): React.ReactElement => { /> - { employeeId: employeeIds, attnTo: attentionToIds, dateRange: dateRange[0] === "" && dateRange[1] === "" ? [] : dateRange, - residentId: residentsIds, + residents: residentsIds, tags: tagsValues, flagged, resultsPerPage, @@ -102,7 +102,7 @@ const HomePage = (): React.ReactElement => { employeeId: employeeIds, attnTo: attentionToIds, dateRange, - residentId: residentsIds, + residents: residentsIds, tags: tagsValues, flagged, }); diff --git a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx index b4eb5018..19fa59d7 100644 --- a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx +++ b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx @@ -118,11 +118,8 @@ const LogRecordsTable = ({ } catch (error) { return; } - const newUserPageNum = ( - logRecords.length === 1 - ? userPageNum - 1 - : userPageNum - ); + const newUserPageNum = + logRecords.length === 1 ? userPageNum - 1 : userPageNum; countRecords(); setShowAlert(true); setUserPageNum(newUserPageNum); @@ -156,7 +153,7 @@ const LogRecordsTable = ({ Date Time - Resident + Residents Note Employee Attn To @@ -175,7 +172,9 @@ const LogRecordsTable = ({ {date} {time} - {record.residentId} + + {record.residents?.join("\n")} + {record.note} diff --git a/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx b/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx index c7d2f586..13967422 100644 --- a/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx +++ b/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx @@ -70,8 +70,8 @@ const ResidentDirectory = (): React.ReactElement => { countResidents={countResidents} /> - { employee: `"${logRecord.employee.firstName} ${logRecord.employee.lastName}"`, flagged: logRecord.flagged, note: `"${logRecord.note}"`, - residentId: `"${logRecord.residentId}"`, + residents: `"${logRecord.residents.join(", ")}"`, tags: logRecord.tags != null ? logRecord.tags.join("; ") : "", }; }; @@ -29,7 +29,7 @@ const CSVConverter = (data: LogRecord[]): boolean => { "employee", "flagged", "note", - "residentId", + "residents", "tags", ]; csvRows.push(headers.join(",")); diff --git a/frontend/src/types/CSVLog.ts b/frontend/src/types/CSVLog.ts index a091904b..8db488a3 100644 --- a/frontend/src/types/CSVLog.ts +++ b/frontend/src/types/CSVLog.ts @@ -5,6 +5,6 @@ export type CSVLog = { employee: string; flagged: boolean; note: string; - residentId: string; + residents: string; tags: string; }; diff --git a/frontend/src/types/LogRecordTypes.ts b/frontend/src/types/LogRecordTypes.ts index 04a8cde9..42e22299 100644 --- a/frontend/src/types/LogRecordTypes.ts +++ b/frontend/src/types/LogRecordTypes.ts @@ -17,7 +17,7 @@ export type LogRecord = { datetime: string; flagged: boolean; note: string; - residentId: string; + residents: string[]; tags: string[]; }; @@ -37,7 +37,7 @@ export type PostLogRecordsResponse = Pick< | "employee" | "flagged" | "note" - | "residentId" + | "residents" > | null; export type CountLogRecordFilters = { @@ -45,14 +45,14 @@ export type CountLogRecordFilters = { employeeId?: number[]; attnTo?: number[]; dateRange?: string[]; - residentId?: number[]; + residents?: number[]; tags?: string[]; flagged?: boolean; }; export type CreateLogRecordParams = { employeeId: number; - residentId: number; + residents: number[]; datetime: Date; flagged: boolean; note: string; @@ -64,7 +64,7 @@ export type CreateLogRecordParams = { export type EditLogRecordParams = { logId: number; employeeId: number; - residentId: number; + residents: number[]; datetime: Date; flagged: boolean; note: string; From e123bf5aa00998a2c88768b83da8a816ac277b57 Mon Sep 17 00:00:00 2001 From: Kevin Pierce Date: Wed, 22 Nov 2023 21:06:24 -0500 Subject: [PATCH 05/23] Sign-up Page Functionality (#202) * Copied over changes, nuked old Signup page * Not invited error message * Email already in use error * Finalized fixes * Now displays invlaid email error FIRST * Combined auth error message generator, removed redundant type * Fixed password issue --- backend/app/rest/auth_routes.py | 4 + backend/app/rest/user_routes.py | 4 + .../services/implementations/user_service.py | 11 +- .../utilities/exceptions/auth_exceptions.py | 17 ++ frontend/src/APIClients/AuthAPIClient.ts | 16 +- frontend/src/APIClients/CommonAPIClient.ts | 11 +- frontend/src/components/auth/Signup.tsx | 122 --------- frontend/src/components/forms/Login.tsx | 11 +- frontend/src/components/forms/Signup.tsx | 234 +++++++++++------- frontend/src/helper/authError.ts | 15 ++ frontend/src/helper/authErrorMessage.ts | 11 - frontend/src/types/AuthTypes.ts | 2 + 12 files changed, 218 insertions(+), 240 deletions(-) create mode 100644 backend/app/utilities/exceptions/auth_exceptions.py delete mode 100644 frontend/src/components/auth/Signup.tsx create mode 100644 frontend/src/helper/authError.ts delete mode 100644 frontend/src/helper/authErrorMessage.ts diff --git a/backend/app/rest/auth_routes.py b/backend/app/rest/auth_routes.py index cf6d78d2..9c6758ea 100644 --- a/backend/app/rest/auth_routes.py +++ b/backend/app/rest/auth_routes.py @@ -3,6 +3,7 @@ InvalidPasswordException, TooManyLoginAttemptsException, ) +from ..utilities.exceptions.auth_exceptions import EmailAlreadyInUseException from flask import Blueprint, current_app, jsonify, request from twilio.rest import Client @@ -188,6 +189,9 @@ def register(): **cookie_options, ) return response, 200 + except EmailAlreadyInUseException as e: + error_message = getattr(e, "message", None) + return jsonify({"error": (error_message if error_message else str(e))}), 409 except Exception as e: error_message = getattr(e, "message", None) return jsonify({"error": (error_message if error_message else str(e))}), 500 diff --git a/backend/app/rest/user_routes.py b/backend/app/rest/user_routes.py index e222ebb8..82f236fb 100644 --- a/backend/app/rest/user_routes.py +++ b/backend/app/rest/user_routes.py @@ -12,6 +12,7 @@ from ..services.implementations.email_service import EmailService from ..services.implementations.user_service import UserService from ..utilities.csv_utils import generate_csv_from_list +from ..utilities.exceptions.auth_exceptions import UserNotInvitedException user_service = UserService(current_app.logger) @@ -91,6 +92,9 @@ def get_user_status(): email = request.args.get("email") user_status = user_service.get_user_status_by_email(email) return jsonify({"user_status": user_status, "email": email}), 201 + except UserNotInvitedException as e: + error_message = getattr(e, "message", None) + return jsonify({"error": (error_message if error_message else str(e))}), 403 except Exception as e: error_message = getattr(e, "message", None) return jsonify({"error": (error_message if error_message else str(e))}), 500 diff --git a/backend/app/services/implementations/user_service.py b/backend/app/services/implementations/user_service.py index 8a89eda3..287cb4a0 100644 --- a/backend/app/services/implementations/user_service.py +++ b/backend/app/services/implementations/user_service.py @@ -4,6 +4,9 @@ from ...models.user import User from ...models import db from ...resources.user_dto import UserDTO +from ...utilities.exceptions.auth_exceptions import ( + UserNotInvitedException, EmailAlreadyInUseException +) class UserService(IUserService): @@ -146,7 +149,7 @@ def get_user_status_by_email(self, email): user = User.query.filter_by(email=email).first() if not user: - raise Exception("user with email {email} not found".format(email)) + raise UserNotInvitedException return user.user_status except Exception as e: @@ -210,7 +213,11 @@ def activate_user(self, user, auth_id=None, signup_method="PASSWORD"): firebase_user = None try: - if self.get_user_status_by_email(user.email) == "Invited": + cur_user_status = self.get_user_status_by_email(user.email) + + if cur_user_status == "Active": + raise EmailAlreadyInUseException + if cur_user_status == "Invited": if signup_method == "PASSWORD": firebase_user = firebase_admin.auth.create_user( email=user.email, password=user.password diff --git a/backend/app/utilities/exceptions/auth_exceptions.py b/backend/app/utilities/exceptions/auth_exceptions.py new file mode 100644 index 00000000..d0a4404f --- /dev/null +++ b/backend/app/utilities/exceptions/auth_exceptions.py @@ -0,0 +1,17 @@ +class UserNotInvitedException(Exception): + """ + Raised when a user that has not been invited attempts to register + """ + + def __init__(self): + self.message = "This email address has not been invited. Please try again with a different email." + super().__init__(self.message) + +class EmailAlreadyInUseException(Exception): + """ + Raised when a user attempts to register with an email of a previously activated user + """ + + def __init__(self): + self.message = "This email is already in use. Please try again with a different email." + super().__init__(self.message) \ No newline at end of file diff --git a/frontend/src/APIClients/AuthAPIClient.ts b/frontend/src/APIClients/AuthAPIClient.ts index bde196bc..af7a4734 100644 --- a/frontend/src/APIClients/AuthAPIClient.ts +++ b/frontend/src/APIClients/AuthAPIClient.ts @@ -4,7 +4,10 @@ import { OperationVariables, } from "@apollo/client"; import { AxiosError } from "axios"; -import getLoginErrMessage from "../helper/authErrorMessage"; +import { + getAuthErrMessage +} +from "../helper/authError"; import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; import { AuthenticatedUser, @@ -33,7 +36,7 @@ const login = async ( if (axiosErr.response && axiosErr.response.status === 401) { return { errCode: axiosErr.response.status, - errMessage: getLoginErrMessage(axiosErr.response), + errMessage: getAuthErrMessage(axiosErr.response, 'LOGIN'), }; } return { @@ -99,7 +102,7 @@ const register = async ( lastName: string, email: string, password: string, -): Promise => { +): Promise => { try { const { data } = await baseAPIClient.post( "/auth/register", @@ -108,6 +111,13 @@ const register = async ( ); return data; } catch (error) { + const axiosErr = (error as any) as AxiosError; + if (axiosErr.response && axiosErr.response.status === 409) { + return { + errCode: axiosErr.response.status, + errMessage: getAuthErrMessage(axiosErr.response, 'SIGNUP'), + }; + } return null; } }; diff --git a/frontend/src/APIClients/CommonAPIClient.ts b/frontend/src/APIClients/CommonAPIClient.ts index 57e82598..25a39554 100644 --- a/frontend/src/APIClients/CommonAPIClient.ts +++ b/frontend/src/APIClients/CommonAPIClient.ts @@ -1,6 +1,8 @@ +import { AxiosError } from "axios"; import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; import baseAPIClient from "./BaseAPIClient"; +import { ErrorResponse } from "../types/AuthTypes"; const inviteUser = async ( email: string, @@ -24,7 +26,7 @@ const inviteUser = async ( } }; -const getUserStatus = async (email: string): Promise => { +const getUserStatus = async (email: string): Promise => { try { if (email === "") { return ""; @@ -44,6 +46,13 @@ const getUserStatus = async (email: string): Promise => { } return "Not invited"; } catch (error) { + const axiosErr = (error as any) as AxiosError; + if (axiosErr.response && axiosErr.response.status === 403) { + return { + errCode: axiosErr.response.status, + errMessage: axiosErr.response.data.error, + }; + } return "Not invited"; } }; diff --git a/frontend/src/components/auth/Signup.tsx b/frontend/src/components/auth/Signup.tsx deleted file mode 100644 index 8e4ac0d0..00000000 --- a/frontend/src/components/auth/Signup.tsx +++ /dev/null @@ -1,122 +0,0 @@ -import React, { useContext } from "react"; -import { Redirect } from "react-router-dom"; - -import authAPIClient from "../../APIClients/AuthAPIClient"; -import { HOME_PAGE } from "../../constants/Routes"; -import AuthContext from "../../contexts/AuthContext"; -import commonApiClient from "../../APIClients/CommonAPIClient"; -import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; - -type SignupProps = { - email: string; - setEmail: (email: string) => void; - firstName: string; - setFirstName: (firstName: string) => void; - lastName: string; - setLastName: (lastName: string) => void; - password: string; - setPassword: (password: string) => void; - toggle: boolean; - setToggle: (toggle: boolean) => void; -}; - -const Signup = ({ - email, - setEmail, - firstName, - setFirstName, - lastName, - setLastName, - password, - setPassword, - toggle, - setToggle, -}: SignupProps): React.ReactElement => { - const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext); - const onSignupClick = async () => { - const isInvited = await commonApiClient.isUserInvited(email); - if (isInvited) { - const registerResponse = await authAPIClient.register( - firstName, - lastName, - email, - password, - ); - if (registerResponse) { - const { requiresTwoFa, authUser } = registerResponse; - if (requiresTwoFa) { - setToggle(!toggle); - } else { - localStorage.setItem( - AUTHENTICATED_USER_KEY, - JSON.stringify(authUser), - ); - setAuthenticatedUser(authUser); - } - } - } else { - // TODO: make this alert better and also differentiate between - // when a user is not invited and when a user's account already exists - // eslint-disable-next-line no-alert - window.alert("user not invited"); - } - }; - - if (authenticatedUser) { - return ; - } - - if (toggle) { - return ( -
-

Signup

-
-
- setFirstName(event.target.value)} - placeholder="first name" - /> -
-
- setLastName(event.target.value)} - placeholder="last name" - /> -
-
- setEmail(event.target.value)} - placeholder="username@domain.com" - /> -
-
- setPassword(event.target.value)} - placeholder="password" - /> -
-
- -
-
-
- ); - } - return <>; -}; - -export default Signup; diff --git a/frontend/src/components/forms/Login.tsx b/frontend/src/components/forms/Login.tsx index cfd37f26..06f09234 100644 --- a/frontend/src/components/forms/Login.tsx +++ b/frontend/src/components/forms/Login.tsx @@ -15,6 +15,7 @@ import { HOME_PAGE, SIGNUP_PAGE } from "../../constants/Routes"; import AuthContext from "../../contexts/AuthContext"; import { ErrorResponse, AuthTokenResponse } from "../../types/AuthTypes"; import commonApiClient from "../../APIClients/CommonAPIClient"; +import { isAuthErrorResponse } from "../../helper/authError"; type CredentialsProps = { email: string; @@ -26,12 +27,6 @@ type CredentialsProps = { setToggle: (toggle: boolean) => void; }; -const isLoginErrorResponse = ( - res: AuthTokenResponse | ErrorResponse, -): res is ErrorResponse => { - return res !== null && "errCode" in res; -}; - const Login = ({ email, setEmail, @@ -77,11 +72,11 @@ const Login = ({ const onLogInClick = async () => { setLoginClicked(true); const isInvited = await commonApiClient.isUserInvited(email); - if (isInvited) { + if (isInvited !== "Not Invited") { const loginResponse: | AuthTokenResponse | ErrorResponse = await authAPIClient.login(email, password); - if (isLoginErrorResponse(loginResponse)) { + if (isAuthErrorResponse(loginResponse)) { setPasswordError(true); setPasswordErrStr(loginResponse.errMessage); } else if (loginResponse) { diff --git a/frontend/src/components/forms/Signup.tsx b/frontend/src/components/forms/Signup.tsx index bc7cfd4d..7d8d471c 100644 --- a/frontend/src/components/forms/Signup.tsx +++ b/frontend/src/components/forms/Signup.tsx @@ -1,11 +1,20 @@ -import React, { useContext } from "react"; +import React, { useState, useContext } from "react"; import { Redirect, useHistory } from "react-router-dom"; -import { Box, Button, Flex, Input, Text } from "@chakra-ui/react"; +import { + Box, + Button, + Flex, + FormControl, + FormErrorMessage, + Input, + Text +} from "@chakra-ui/react"; import authAPIClient from "../../APIClients/AuthAPIClient"; import { HOME_PAGE, LOGIN_PAGE } from "../../constants/Routes"; import AuthContext from "../../contexts/AuthContext"; import commonApiClient from "../../APIClients/CommonAPIClient"; import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; +import { isAuthErrorResponse } from "../../helper/authError"; type SignupProps = { email: string; @@ -20,6 +29,8 @@ type SignupProps = { setToggle: (toggle: boolean) => void; }; +const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; + const Signup = ({ email, setEmail, @@ -32,38 +43,98 @@ const Signup = ({ toggle, setToggle, }: SignupProps): React.ReactElement => { + const [signupClicked, setSignupClicked] = useState(false); + const [emailError, setEmailError] = useState(false); + const [emailErrorStr, setEmailErrorStr] = useState(""); + const [passwordError, setPasswordError] = useState(false); + const [passwordErrorStr, setPasswordErrorStr] = useState(""); + const { authenticatedUser, setAuthenticatedUser } = useContext(AuthContext); const history = useHistory(); + const handleEmailChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value as string; + if (signupClicked) { + if (emailRegex.test(inputValue)) { + setEmailErrorStr("") + setEmailError(false) + } else { + setEmailErrorStr("Please enter a valid email.") + setEmailError(true) + } + } + setEmail(inputValue) + }; + + const handlePasswordChange = (e: React.ChangeEvent) => { + const inputValue = e.target.value as string; + setPassword(inputValue) + + if (signupClicked) { + if (inputValue.length >= 6) { + setPasswordErrorStr("") + setPasswordError(false) + } + else { + setPasswordErrorStr("Password must be 6 characters long.") + setPasswordError(true) + } + } + } + const onSignupClick = async () => { + setSignupClicked(true) + + if (!emailRegex.test(email)) { + setEmailErrorStr("Please enter a valid email.") + setEmailError(true) + return + } + + if (password.length < 6) { + setPasswordErrorStr("Password must be 6 characters long.") + setPasswordError(true) + return + } + const isInvited = await commonApiClient.isUserInvited(email); - if (isInvited) { - const registerResponse = await authAPIClient.register( - firstName, - lastName, - email, - password, - ); - if (registerResponse) { - const { requiresTwoFa, authUser } = registerResponse; - if (requiresTwoFa) { - setToggle(!toggle); - } else { - localStorage.setItem( - AUTHENTICATED_USER_KEY, - JSON.stringify(authUser), - ); - setAuthenticatedUser(authUser); + if (isInvited !== "Not Invited") { + if (isAuthErrorResponse(isInvited)) { + setEmailErrorStr(isInvited.errMessage) + setEmailError(true) + } + else { + const registerResponse = await authAPIClient.register( + firstName, + lastName, + email, + password, + ); + if (registerResponse) { + if (isAuthErrorResponse(registerResponse)) { + setEmailErrorStr(registerResponse.errMessage) + setEmailError(true) + } + else { + const { requiresTwoFa, authUser } = registerResponse; + if (requiresTwoFa) { + setToggle(!toggle); + } else { + localStorage.setItem( + AUTHENTICATED_USER_KEY, + JSON.stringify(authUser), + ); + setAuthenticatedUser(authUser); + } + } } } - } else { - // TODO: make this alert better and also differentiate between - // when a user is not invited and when a user's account already exists - // eslint-disable-next-line no-alert - window.alert("user not invited"); } }; + const isCreateAccountBtnDisabled = () => + emailError || passwordError || email === '' || password === '' || firstName === '' || lastName === '' + const onLogInClick = () => { history.push(LOGIN_PAGE); }; @@ -76,74 +147,55 @@ const Signup = ({ return ( - - - Sign Up - - - - + + + Sign Up + setFirstName(event.target.value)} /> - - setLastName(event.target.value)} /> - - - setEmail(event.target.value)} - /> - - - setPassword(event.target.value)} - /> - - + + + {emailErrorStr} + + + + {passwordErrorStr} + - - - - - Already have an account? - - - Log In Now - + + + Already have an account? + + + Log In Now + + + diff --git a/frontend/src/helper/authError.ts b/frontend/src/helper/authError.ts new file mode 100644 index 00000000..21dfd40c --- /dev/null +++ b/frontend/src/helper/authError.ts @@ -0,0 +1,15 @@ +import { AxiosError } from "axios"; +import { AuthTokenResponse, ErrorResponse, AuthFlow } from "../types/AuthTypes"; + +export const getAuthErrMessage = (axiosErrRes: AxiosError["response"], flow: AuthFlow): string => { + if (axiosErrRes && axiosErrRes.data && axiosErrRes.data.error) { + return axiosErrRes.data.error; + } + return `Error ${flow === 'LOGIN' ? "logging in" : "signing up"}. Please try again later.`; +} + +export const isAuthErrorResponse = ( + res: string | AuthTokenResponse | ErrorResponse, +): res is ErrorResponse => { + return res !== null && typeof res !== 'string' && "errCode" in res; +}; diff --git a/frontend/src/helper/authErrorMessage.ts b/frontend/src/helper/authErrorMessage.ts deleted file mode 100644 index c2258c84..00000000 --- a/frontend/src/helper/authErrorMessage.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { AxiosError } from "axios"; - -// Helper to get login error message -const getLoginErrMessage = (axiosErrRes: AxiosError["response"]): string => { - if (axiosErrRes && axiosErrRes.data && axiosErrRes.data.error) { - return axiosErrRes.data.error; - } - return "Error logging in. Please try again later."; -}; - -export default getLoginErrMessage; diff --git a/frontend/src/types/AuthTypes.ts b/frontend/src/types/AuthTypes.ts index 4e55e7d6..4c33ec07 100644 --- a/frontend/src/types/AuthTypes.ts +++ b/frontend/src/types/AuthTypes.ts @@ -27,3 +27,5 @@ export type ErrorResponse = { errCode: number; errMessage: string; }; + +export type AuthFlow = 'LOGIN' | 'SIGNUP'; From 16d75b857cea4bfdfed4a9d80dc8d2b3cbdf5d27 Mon Sep 17 00:00:00 2001 From: Carolyn Zhang <82423082+carolynzhang18@users.noreply.github.com> Date: Wed, 22 Nov 2023 21:07:39 -0500 Subject: [PATCH 06/23] Tags Default Sort By Last Modified (#204) * Add last_modified column to tags table * Get tags from most to least recently modified * Fix db revision history * Linter * Tags model fixes --- backend/app/models/tags.py | 3 ++ .../services/implementations/tags_service.py | 2 +- .../0dbe140cf6f5_add_last_modified_to_tags.py | 32 +++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py diff --git a/backend/app/models/tags.py b/backend/app/models/tags.py index 294744e7..12990560 100644 --- a/backend/app/models/tags.py +++ b/backend/app/models/tags.py @@ -10,6 +10,9 @@ class Tag(db.Model): tag_id = db.Column(db.Integer, primary_key=True, nullable=False) name = db.Column(db.String, unique=True, nullable=False) status = db.Column(db.Enum("Deleted", "Active", name="status"), nullable=False) + last_modified = db.Column( + db.DateTime, server_default=db.func.now(), onupdate=db.func.now(), nullable=False + ) log_records = db.relationship( "LogRecords", secondary="log_record_tag", back_populates="tags" ) diff --git a/backend/app/services/implementations/tags_service.py b/backend/app/services/implementations/tags_service.py index e8befd1d..86b7fe00 100644 --- a/backend/app/services/implementations/tags_service.py +++ b/backend/app/services/implementations/tags_service.py @@ -19,7 +19,7 @@ def __init__(self, logger): def get_tags(self): try: - tags_results = Tag.query.all() + tags_results = Tag.query.order_by(Tag.last_modified.desc()).all() tags_results = list(map(lambda tag: tag.to_dict(), tags_results)) return {"tags": tags_results} except Exception as postgres_error: diff --git a/backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py b/backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py new file mode 100644 index 00000000..18915ce2 --- /dev/null +++ b/backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py @@ -0,0 +1,32 @@ +"""add last_modified to tags + +Revision ID: 0dbe140cf6f5 +Revises: 8b5132609f1f +Create Date: 2023-11-09 01:59:44.190531 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "0dbe140cf6f5" +down_revision = "0ea2257f1dc6" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("tags", schema=None) as batch_op: + batch_op.add_column(sa.Column("last_modified", sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), onupdate=sa.text('CURRENT_TIMESTAMP'), nullable=False)) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("tags", schema=None) as batch_op: + batch_op.drop_column("last_modified") + + # ### end Alembic commands ### From 7e1561ea5040f8ea55605e836c40066e71f067ef Mon Sep 17 00:00:00 2001 From: Aathithan Chandrabalan <75588919+achandrabalan@users.noreply.github.com> Date: Wed, 22 Nov 2023 21:18:18 -0500 Subject: [PATCH 07/23] Employee Directory - Sort By Last Modified (#198) * init * working state * clean up * adding defer and removing unnecessary updates * nit fixes * fix up migration files * nullable false * clean up: * further cleaning * clean clean --------- Co-authored-by: Connor Bechthold --- backend/app/models/user.py | 7 +++- backend/app/resources/user_dto.py | 5 ++- .../services/implementations/user_service.py | 5 ++- ...0ea2257f1dc6_tag_name_unique_constraint.py | 2 +- .../698e5724baae_last_modified_column.py | 40 +++++++++++++++++++ 5 files changed, 54 insertions(+), 5 deletions(-) create mode 100644 backend/migrations/versions/698e5724baae_last_modified_column.py diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 5fb184f7..552215e3 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -1,4 +1,4 @@ -from sqlalchemy import inspect, case, null +from sqlalchemy import func, inspect, case, null from sqlalchemy.orm.properties import ColumnProperty from . import db @@ -19,6 +19,11 @@ class User(db.Model): nullable=False, ) email = db.Column(db.String, nullable=False) + last_modified = db.deferred( + db.Column( + db.DateTime, server_default=func.now(), onupdate=func.now(), nullable=False + ) + ) __table_args__ = ( db.CheckConstraint( diff --git a/backend/app/resources/user_dto.py b/backend/app/resources/user_dto.py index a15e6279..559423bb 100644 --- a/backend/app/resources/user_dto.py +++ b/backend/app/resources/user_dto.py @@ -1,8 +1,11 @@ class UserDTO: - def __init__(self, id, first_name, last_name, email, role, user_status): + def __init__( + self, id, first_name, last_name, email, role, user_status, last_modified + ): self.id = id self.first_name = first_name self.last_name = last_name self.email = email self.role = role self.user_status = user_status + self.last_modified = last_modified diff --git a/backend/app/services/implementations/user_service.py b/backend/app/services/implementations/user_service.py index 287cb4a0..249fc2c6 100644 --- a/backend/app/services/implementations/user_service.py +++ b/backend/app/services/implementations/user_service.py @@ -116,10 +116,11 @@ def get_auth_id_by_user_id(self, user_id): def get_users(self, return_all, page_number, results_per_page): try: if return_all: - users = User.query.all() + users = User.query.order_by(User.last_modified.desc()).all() else: users = ( - User.query.limit(results_per_page) + User.query.order_by(User.last_modified.desc()) + .limit(results_per_page) .offset((page_number - 1) * results_per_page) .all() ) diff --git a/backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py b/backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py index feb2567c..b205dae0 100644 --- a/backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py +++ b/backend/migrations/versions/0ea2257f1dc6_tag_name_unique_constraint.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "0ea2257f1dc6" -down_revision = "8b5132609f1f" +down_revision = "24fad25f60e3" branch_labels = None depends_on = None diff --git a/backend/migrations/versions/698e5724baae_last_modified_column.py b/backend/migrations/versions/698e5724baae_last_modified_column.py new file mode 100644 index 00000000..761400d1 --- /dev/null +++ b/backend/migrations/versions/698e5724baae_last_modified_column.py @@ -0,0 +1,40 @@ +"""last_modified_column + +Revision ID: 698e5724baae +Revises: 8b5132609f1f +Create Date: 2023-11-08 20:26:08.758322 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "698e5724baae" +down_revision = "0ea2257f1dc6" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.add_column( + sa.Column( + "last_modified", + sa.DateTime(), + server_default=sa.text("CURRENT_TIMESTAMP"), + onupdate=sa.text("CURRENT_TIMESTAMP"), + nullable=False, + ) + ) + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.drop_column("last_modified") + + # ### end Alembic commands ### From 95a4a0e36bcc624e7bafb25b87eb780bff3ae0ed Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Thu, 23 Nov 2023 13:21:22 -0500 Subject: [PATCH 08/23] reorder checksums (#206) --- .../versions/0c76a8fe211d_add_log_record_residents.py | 2 +- .../versions/0dbe140cf6f5_add_last_modified_to_tags.py | 2 +- ...d_column.py => 698e5724baae_add_last_modified_to_users.py} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename backend/migrations/versions/{698e5724baae_last_modified_column.py => 698e5724baae_add_last_modified_to_users.py} (93%) diff --git a/backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py b/backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py index b4309b69..7dc48ac5 100644 --- a/backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py +++ b/backend/migrations/versions/0c76a8fe211d_add_log_record_residents.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "0c76a8fe211d" -down_revision = "24fad25f60e3" +down_revision = "0ea2257f1dc6" branch_labels = None depends_on = None diff --git a/backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py b/backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py index 18915ce2..46c34d1d 100644 --- a/backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py +++ b/backend/migrations/versions/0dbe140cf6f5_add_last_modified_to_tags.py @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "0dbe140cf6f5" -down_revision = "0ea2257f1dc6" +down_revision = "698e5724baae" branch_labels = None depends_on = None diff --git a/backend/migrations/versions/698e5724baae_last_modified_column.py b/backend/migrations/versions/698e5724baae_add_last_modified_to_users.py similarity index 93% rename from backend/migrations/versions/698e5724baae_last_modified_column.py rename to backend/migrations/versions/698e5724baae_add_last_modified_to_users.py index 761400d1..c20b93be 100644 --- a/backend/migrations/versions/698e5724baae_last_modified_column.py +++ b/backend/migrations/versions/698e5724baae_add_last_modified_to_users.py @@ -1,4 +1,4 @@ -"""last_modified_column +"""add last_modified to users Revision ID: 698e5724baae Revises: 8b5132609f1f @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = "698e5724baae" -down_revision = "0ea2257f1dc6" +down_revision = "0c76a8fe211d" branch_labels = None depends_on = None From ce3bf7c740b212d1ad5e629ecc490f47da5efaa8 Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Thu, 23 Nov 2023 15:48:57 -0500 Subject: [PATCH 09/23] revert last modified to auth and user DTO (#207) --- backend/app/models/user.py | 6 ++---- backend/app/resources/auth_dto.py | 3 ++- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/backend/app/models/user.py b/backend/app/models/user.py index 552215e3..161f4f4e 100644 --- a/backend/app/models/user.py +++ b/backend/app/models/user.py @@ -19,10 +19,8 @@ class User(db.Model): nullable=False, ) email = db.Column(db.String, nullable=False) - last_modified = db.deferred( - db.Column( - db.DateTime, server_default=func.now(), onupdate=func.now(), nullable=False - ) + last_modified = db.Column( + db.DateTime, server_default=func.now(), onupdate=func.now(), nullable=False ) __table_args__ = ( diff --git a/backend/app/resources/auth_dto.py b/backend/app/resources/auth_dto.py index a1bb86eb..b110bcb2 100644 --- a/backend/app/resources/auth_dto.py +++ b/backend/app/resources/auth_dto.py @@ -13,6 +13,7 @@ def __init__( email, role, user_status, + last_modified ): Token.__init__(self, access_token, refresh_token) - UserDTO.__init__(self, id, first_name, last_name, email, role, user_status) + UserDTO.__init__(self, id, first_name, last_name, email, role, user_status, last_modified) From ab345847646bfac19dd68b257bde46e9d3d10d98 Mon Sep 17 00:00:00 2001 From: phamkelly17 <86501308+phamkelly17@users.noreply.github.com> Date: Sun, 26 Nov 2023 14:17:41 -0500 Subject: [PATCH 10/23] Get Buildings Route (#196) * backend for get buildings route * replace hardcoded BUIDLINGS var with get request * pr comments * fix edit resident so that the buildings show up * fix bug --- backend/app/rest/__init__.py | 2 ++ backend/app/rest/buildings_routes.py | 20 ++++++++++++ .../implementations/buildings_service.py | 31 +++++++++++++++++++ .../services/interfaces/buildings_service.py | 14 +++++++++ frontend/src/APIClients/BuildingAPIClient.ts | 27 ++++++++++++++++ frontend/src/components/forms/CreateLog.tsx | 19 ++++++++---- .../src/components/forms/CreateResident.tsx | 25 ++++++++++----- frontend/src/components/forms/EditLog.tsx | 14 +++------ .../src/components/forms/EditResident.tsx | 19 +++++------- .../pages/HomePage/LogRecordsTable.tsx | 14 +++++++++ .../pages/HomePage/SearchAndFilters.tsx | 22 ++++++++----- .../ResidentDirectory/ResidentDirectory.tsx | 20 ++++++++++-- .../ResidentDirectoryTable.tsx | 4 +++ frontend/src/types/BuildingTypes.ts | 10 ++++++ 14 files changed, 198 insertions(+), 43 deletions(-) create mode 100644 backend/app/rest/buildings_routes.py create mode 100644 backend/app/services/implementations/buildings_service.py create mode 100644 backend/app/services/interfaces/buildings_service.py create mode 100644 frontend/src/APIClients/BuildingAPIClient.ts diff --git a/backend/app/rest/__init__.py b/backend/app/rest/__init__.py index e7010a6b..d1d9335f 100644 --- a/backend/app/rest/__init__.py +++ b/backend/app/rest/__init__.py @@ -8,6 +8,7 @@ def init_app(app): log_records_routes, residents_routes, tags_routes, + buildings_routes, ) app.register_blueprint(user_routes.blueprint) @@ -18,3 +19,4 @@ def init_app(app): app.register_blueprint(log_records_routes.blueprint) app.register_blueprint(residents_routes.blueprint) app.register_blueprint(tags_routes.blueprint) + app.register_blueprint(buildings_routes.blueprint) diff --git a/backend/app/rest/buildings_routes.py b/backend/app/rest/buildings_routes.py new file mode 100644 index 00000000..f209cfe9 --- /dev/null +++ b/backend/app/rest/buildings_routes.py @@ -0,0 +1,20 @@ +from flask import Blueprint, current_app, jsonify, request +from ..middlewares.auth import require_authorization_by_role +from ..services.implementations.buildings_service import BuildingsService + +buildings_service = BuildingsService(current_app.logger) +blueprint = Blueprint("buildings", __name__, url_prefix="/buildings") + + +@blueprint.route("/", methods=["GET"], strict_slashes=False) +@require_authorization_by_role({"Relief Staff", "Regular Staff", "Admin"}) +def get_buildings(): + """ + Get buildings. + """ + try: + building_results = buildings_service.get_buildings() + return jsonify(building_results), 201 + except Exception as e: + error_message = getattr(e, "message", None) + return jsonify({"error": (error_message if error_message else str(e))}), 500 diff --git a/backend/app/services/implementations/buildings_service.py b/backend/app/services/implementations/buildings_service.py new file mode 100644 index 00000000..d09db406 --- /dev/null +++ b/backend/app/services/implementations/buildings_service.py @@ -0,0 +1,31 @@ +from flask import jsonify +from ..interfaces.buildings_service import IBuildingsService +from ...models.buildings import Buildings +from ...models import db + + +class BuildingsService(IBuildingsService): + """ + Buildings implementation + """ + + def __init__(self, logger): + """ + Create an instance of BuildingsService + + :param logger: application's logger instance + :type logger: logger + """ + self.logger = logger + + def get_buildings(self): + try: + buildings_results = Buildings.query.all() + + return { + "buildings": list( + map(lambda building: building.to_dict(), buildings_results) + ) + } + except Exception as postgres_error: + raise postgres_error diff --git a/backend/app/services/interfaces/buildings_service.py b/backend/app/services/interfaces/buildings_service.py new file mode 100644 index 00000000..19fa7397 --- /dev/null +++ b/backend/app/services/interfaces/buildings_service.py @@ -0,0 +1,14 @@ +from abc import ABC, abstractmethod + + +class IBuildingsService(ABC): + """ + BuildingsService interface with buildings methods + """ + + @abstractmethod + def get_buildings(self): + """ + Gets buildings in json format. + """ + pass diff --git a/frontend/src/APIClients/BuildingAPIClient.ts b/frontend/src/APIClients/BuildingAPIClient.ts new file mode 100644 index 00000000..300a1c7e --- /dev/null +++ b/frontend/src/APIClients/BuildingAPIClient.ts @@ -0,0 +1,27 @@ +import axios, { AxiosError } from "axios"; +import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; +import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; +import baseAPIClient from "./BaseAPIClient"; +import { GetBuildingsResponse } from "../types/BuildingTypes"; + +const getBuildings = async (): Promise => { + try { + const bearerToken = `Bearer ${getLocalStorageObjProperty( + AUTHENTICATED_USER_KEY, + "accessToken", + )}`; + const { data } = await baseAPIClient.get( + `/buildings`, + { + headers: { Authorization: bearerToken }, + }, + ); + return data; + } catch (error) { + return null; + } +}; + +export default { + getBuildings, +}; diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index 19ba75a0..1088daf1 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -34,6 +34,8 @@ import ResidentAPIClient from "../../APIClients/ResidentAPIClient"; import { getLocalStorageObj } from "../../utils/LocalStorageUtils"; import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; import LogRecordAPIClient from "../../APIClients/LogRecordAPIClient"; +import BuildingAPIClient from "../../APIClients/BuildingAPIClient"; +import { BuildingLabel } from "../../types/BuildingTypes"; import selectStyle from "../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../theme/forms/datePickerStyles"; import { UserLabel } from "../../types/UserTypes"; @@ -56,11 +58,6 @@ type AlertDataOptions = { }; // Ideally we should be storing this information in the database -const BUILDINGS = [ - { label: "144", value: 1 }, - { label: "362", value: 2 }, - { label: "402", value: 3 }, -]; const ALERT_DATA: AlertDataOptions = { DEFAULT: { @@ -128,6 +125,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { const [employeeOptions, setEmployeeOptions] = useState([]); const [residentOptions, setResidentOptions] = useState([]); + const [buildingOptions, setBuildingOptions] = useState([]); const [isCreateOpen, setCreateOpen] = React.useState(false); @@ -210,6 +208,15 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { // fetch resident + employee data for log creation const getLogEntryOptions = async () => { + const buildingsData = await BuildingAPIClient.getBuildings(); + + if (buildingsData && buildingsData.buildings.length !== 0) { + const buildingLabels: BuildingLabel[] = buildingsData.buildings.map( + (building) => ({ label: building.name!, value: building.id! }), + ); + setBuildingOptions(buildingLabels); + } + const residentsData = await ResidentAPIClient.getResidents({ returnAll: true, }); @@ -390,7 +397,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { Building Promise; countRecords: () => Promise; setUserPageNum: React.Dispatch>; + buildingOptions: BuildingLabel[]; }; type AlertData = { @@ -59,13 +61,6 @@ type AlertDataOptions = { [key: string]: AlertData; }; -// Ideally we should be storing this information in the database -const BUILDINGS = [ - { label: "144", value: 1 }, - { label: "362", value: 2 }, - { label: "402", value: 3 }, -]; - const ALERT_DATA: AlertDataOptions = { DEFAULT: { status: "info", @@ -110,6 +105,7 @@ const EditLog = ({ getRecords, countRecords, setUserPageNum, + buildingOptions, }: Props) => { // currently, the select for employees is locked and should default to current user. Need to check if admins/regular staff are allowed to change this const [employee, setEmployee] = useState(getCurUserSelectOption()); @@ -348,11 +344,11 @@ const EditLog = ({ Building item.value === buildingId, )} onChange={handleBuildingChange} diff --git a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx index 19fa59d7..643d40ca 100644 --- a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx +++ b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx @@ -29,7 +29,9 @@ import EditLog from "../../forms/EditLog"; import LogRecordAPIClient from "../../../APIClients/LogRecordAPIClient"; import ResidentAPIClient from "../../../APIClients/ResidentAPIClient"; import UserAPIClient from "../../../APIClients/UserAPIClient"; +import BuildingAPIClient from "../../../APIClients/BuildingAPIClient"; import { UserLabel } from "../../../types/UserTypes"; +import { BuildingLabel } from "../../../types/BuildingTypes"; import ConfirmationModal from "../../common/ConfirmationModal"; type Props = { @@ -57,6 +59,8 @@ const LogRecordsTable = ({ const [showAlert, setShowAlert] = useState(false); + const [buildingOptions, setBuildingOptions] = useState([]); + // Menu states const [deleteOpenMap, setDeleteOpenMap] = useState<{ [key: number]: boolean; @@ -100,6 +104,15 @@ const LogRecordsTable = ({ setResidentOptions(residentLabels); } + const buildingsData = await BuildingAPIClient.getBuildings(); + + if (buildingsData && buildingsData.buildings.length !== 0) { + const buildingLabels: BuildingLabel[] = buildingsData.buildings.map( + (building) => ({ label: building.name!, value: building.id! }), + ); + setBuildingOptions(buildingLabels); + } + const usersData = await UserAPIClient.getUsers({ returnAll: true }); if (usersData && usersData.users.length !== 0) { const userLabels: UserLabel[] = usersData.users @@ -222,6 +235,7 @@ const LogRecordsTable = ({ getRecords={getRecords} countRecords={countRecords} setUserPageNum={setUserPageNum} + buildingOptions={buildingOptions} /> >; setFlagged: React.Dispatch>; }; -// Ideally we should be storing this information in the database -const BUILDINGS = [ - { label: "144", value: 1 }, - { label: "362", value: 2 }, - { label: "402", value: 3 }, -]; // Replace this with the tags from the db once the API and table are made const TAGS: Tag[] = [ @@ -77,11 +72,23 @@ const SearchAndFilters = ({ setBuildings, setFlagged, }: Props): React.ReactElement => { + const [buildingOptions, setBuildingOptions] = useState([]); const [userLabels, setUserLabels] = useState(); const [residentLabels, setResidentLabels] = useState(); const dateChangeToast = CreateToast(); + const getBuildingsOptions = async () => { + const buildingsData = await BuildingAPIClient.getBuildings(); + + if (buildingsData && buildingsData.buildings.length !== 0) { + const buildingLabels: BuildingLabel[] = buildingsData.buildings.map( + (building) => ({ label: building.name!, value: building.id! }), + ); + setBuildingOptions(buildingLabels); + } + }; + const getUsers = async () => { const data = await UserAPIClient.getUsers({ returnAll: true }); const users = data?.users; @@ -179,6 +186,7 @@ const SearchAndFilters = ({ }; useEffect(() => { + getBuildingsOptions(); getUsers(); getResidents(); }, []); @@ -306,7 +314,7 @@ const SearchAndFilters = ({ Building setFirstName(event.target.value)} - /> - setLastName(event.target.value)} - /> - + + + Sign Up + + + setFirstName(event.target.value)} /> - {emailErrorStr} - - + + setLastName(event.target.value)} /> - {passwordErrorStr} - - - - + + + + + {emailErrorStr} + + + + + + {passwordErrorStr} + + + + + + + + Already have an account? Log In Now - + diff --git a/frontend/src/theme/forms/inputStyles.tsx b/frontend/src/theme/forms/inputStyles.tsx index 9211c3d3..de142516 100644 --- a/frontend/src/theme/forms/inputStyles.tsx +++ b/frontend/src/theme/forms/inputStyles.tsx @@ -39,7 +39,7 @@ const Input: ComponentStyleConfig = { border: "1px solid", borderColor: "gray.100", borderRadius: "4px", - height: "7vh", + height: "8vh", fontWeight: "400", fontSize: "22px", fontFamily: "DM Sans", From 8796389667b8447b493f2c3eb585a1ca6f015eb3 Mon Sep 17 00:00:00 2001 From: Kevin Pierce Date: Sun, 26 Nov 2023 16:00:24 -0500 Subject: [PATCH 13/23] [BUG] Pagination Total Num of Pages Fix (#209) --- frontend/src/components/common/Pagination.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/components/common/Pagination.tsx b/frontend/src/components/common/Pagination.tsx index c519a1eb..5f6fa1d9 100644 --- a/frontend/src/components/common/Pagination.tsx +++ b/frontend/src/components/common/Pagination.tsx @@ -40,7 +40,7 @@ const Pagination = ({ setResultsPerPage, getRecords, }: Props): React.ReactElement => { - const numPages = Math.ceil(numRecords / resultsPerPage); + const numPages = Math.ceil(Math.max(1, numRecords) / resultsPerPage); const handleNumberInputChange = ( newUserPageNumString: string, From 435b9dc4a8bddc1354b9a3703e5acda222099dea Mon Sep 17 00:00:00 2001 From: Daniel Kim <93679428+danielk1345@users.noreply.github.com> Date: Wed, 29 Nov 2023 17:19:17 -0800 Subject: [PATCH 14/23] Cascading Delete Tags (#201) * initial commit * update cascading delete * address PR comments * update db --- backend/app/models/log_record_tags.py | 2 +- .../services/implementations/tags_service.py | 11 +++++-- backend/migrations/versions/117790caec65_.py | 32 +++++++++++++++++++ scripts/exec-db.sh | 0 4 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 backend/migrations/versions/117790caec65_.py mode change 100644 => 100755 scripts/exec-db.sh diff --git a/backend/app/models/log_record_tags.py b/backend/app/models/log_record_tags.py index e052e234..4cc188ef 100644 --- a/backend/app/models/log_record_tags.py +++ b/backend/app/models/log_record_tags.py @@ -11,7 +11,7 @@ class LogRecordTag(db.Model): log_record_id = db.Column( db.Integer, db.ForeignKey("log_records.log_id"), nullable=False ) - tag_id = db.Column(db.Integer, db.ForeignKey("tags.tag_id"), nullable=False) + tag_id = db.Column(db.Integer, db.ForeignKey("tags.tag_id", ondelete='CASCADE'), nullable=False) def to_dict(self, include_relationships=False): # define the entities table diff --git a/backend/app/services/implementations/tags_service.py b/backend/app/services/implementations/tags_service.py index 86b7fe00..fbd7abcc 100644 --- a/backend/app/services/implementations/tags_service.py +++ b/backend/app/services/implementations/tags_service.py @@ -1,5 +1,6 @@ from ..interfaces.tags_service import ITagsService from ...models.tags import Tag +from ...models.log_record_tags import LogRecordTag from ...models import db @@ -26,9 +27,13 @@ def get_tags(self): raise postgres_error def delete_tag(self, tag_id): - deleted_tag = Tag.query.filter_by(tag_id=tag_id).update({"status": "Deleted"}) - if not deleted_tag: - raise Exception("Tag with id {tag_id} not found".format(tag_id=tag_id)) + tags_to_delete = Tag.query.filter_by(tag_id=tag_id).first() + if not tags_to_delete: + raise Exception( + "Log record with id {log_id} not found".format(log_id=log_id) + ) + tags_to_delete.log_records = [] + db.session.delete(tags_to_delete) db.session.commit() def update_tag(self, tag_id, updated_tag): diff --git a/backend/migrations/versions/117790caec65_.py b/backend/migrations/versions/117790caec65_.py new file mode 100644 index 00000000..9905f14c --- /dev/null +++ b/backend/migrations/versions/117790caec65_.py @@ -0,0 +1,32 @@ +"""empty message + +Revision ID: 117790caec65 +Revises: 8b5132609f1f +Create Date: 2023-11-16 01:53:04.353305 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '117790caec65' +down_revision = '8b5132609f1f' +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tags', schema=None) as batch_op: + batch_op.drop_column('status') + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table('tags', schema=None) as batch_op: + batch_op.add_column(sa.Column('status', postgresql.ENUM('Deleted', 'Active', name='status'), autoincrement=False, nullable=False)) + + # ### end Alembic commands ### diff --git a/scripts/exec-db.sh b/scripts/exec-db.sh old mode 100644 new mode 100755 From 137f17364c7503b662188e8413a8d24387feb5ac Mon Sep 17 00:00:00 2001 From: Braydon Wang Date: Wed, 29 Nov 2023 21:42:01 -0500 Subject: [PATCH 15/23] add message to migration file name (#212) --- ...7790caec65_.py => 117790caec65_remove_status_from_tags.py} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename backend/migrations/versions/{117790caec65_.py => 117790caec65_remove_status_from_tags.py} (93%) diff --git a/backend/migrations/versions/117790caec65_.py b/backend/migrations/versions/117790caec65_remove_status_from_tags.py similarity index 93% rename from backend/migrations/versions/117790caec65_.py rename to backend/migrations/versions/117790caec65_remove_status_from_tags.py index 9905f14c..6ed94d13 100644 --- a/backend/migrations/versions/117790caec65_.py +++ b/backend/migrations/versions/117790caec65_remove_status_from_tags.py @@ -1,4 +1,4 @@ -"""empty message +"""remove status from tags Revision ID: 117790caec65 Revises: 8b5132609f1f @@ -11,7 +11,7 @@ # revision identifiers, used by Alembic. revision = '117790caec65' -down_revision = '8b5132609f1f' +down_revision = '0dbe140cf6f5' branch_labels = None depends_on = None From 59f41e08a10a493dab406d4e1fb12d0135f459ce Mon Sep 17 00:00:00 2001 From: Braydon Wang Date: Wed, 29 Nov 2023 21:44:55 -0500 Subject: [PATCH 16/23] Connecting tags when creating, filtering and editing log record (#203) * initial changes * change edit log * change filter and update it to use id rather than name * small fixes and lint * reorder lines * remove tag status * remove tag status field --- backend/app/models/tags.py | 1 - .../implementations/log_records_service.py | 24 ++++++------ frontend/src/APIClients/TagAPIClient.ts | 24 ++++++++++++ frontend/src/components/forms/CreateLog.tsx | 35 +++++++++++++----- frontend/src/components/forms/EditLog.tsx | 31 +++++++++++----- .../components/pages/HomePage/HomePage.tsx | 4 +- .../pages/HomePage/LogRecordsTable.tsx | 19 +++++++++- .../pages/HomePage/SearchAndFilters.tsx | 37 ++++++++++++------- frontend/src/types/LogRecordTypes.ts | 6 +-- frontend/src/types/TagTypes.ts | 14 +++++++ frontend/src/types/TagsTypes.ts | 5 --- 11 files changed, 142 insertions(+), 58 deletions(-) create mode 100644 frontend/src/APIClients/TagAPIClient.ts create mode 100644 frontend/src/types/TagTypes.ts delete mode 100644 frontend/src/types/TagsTypes.ts diff --git a/backend/app/models/tags.py b/backend/app/models/tags.py index 12990560..6dc87c14 100644 --- a/backend/app/models/tags.py +++ b/backend/app/models/tags.py @@ -9,7 +9,6 @@ class Tag(db.Model): tag_id = db.Column(db.Integer, primary_key=True, nullable=False) name = db.Column(db.String, unique=True, nullable=False) - status = db.Column(db.Enum("Deleted", "Active", name="status"), nullable=False) last_modified = db.Column( db.DateTime, server_default=db.func.now(), onupdate=db.func.now(), nullable=False ) diff --git a/backend/app/services/implementations/log_records_service.py b/backend/app/services/implementations/log_records_service.py index 17c33c20..4f7e23c7 100644 --- a/backend/app/services/implementations/log_records_service.py +++ b/backend/app/services/implementations/log_records_service.py @@ -25,8 +25,9 @@ def __init__(self, logger): def add_record(self, log_record): new_log_record = log_record.copy() + residents = new_log_record["residents"] - tag_names = new_log_record["tags"] + tags = new_log_record["tags"] del new_log_record["residents"] del new_log_record["tags"] @@ -34,7 +35,7 @@ def add_record(self, log_record): try: new_log_record = LogRecords(**new_log_record) self.construct_residents(new_log_record, residents) - self.construct_tags(new_log_record, tag_names) + self.construct_tags(new_log_record, tags) db.session.add(new_log_record) db.session.commit() @@ -51,12 +52,13 @@ def construct_residents(self, log_record, residents): raise Exception(f"Resident with id {resident_id} does not exist") log_record.residents.append(resident) - def construct_tags(self, log_record, tag_names): - for tag_name in tag_names: - tag = Tag.query.filter_by(name=tag_name).first() + def construct_tags(self, log_record, tags): + tags = list(set(tags)) + for tag_id in tags: + tag = Tag.query.filter_by(tag_id=tag_id).first() if not tag: - raise Exception(f"Tag with name {tag_name} does not exist") + raise Exception(f"Tag with id {tag_id} does not exist") log_record.tags.append(tag) def to_json_list(self, logs): @@ -146,12 +148,12 @@ def filter_by_date_range(self, date_range): return sql def filter_by_tags(self, tags): - if len(tags) >= 1: - sql_statement = f"\n'{tags[0]}'=ANY (tag_names)" + if type(tags) == list: + sql_statement = f"\n'{tags[0]}'=ANY (tag_ids)" for i in range(1, len(tags)): - sql_statement = sql_statement + f"\nAND '{tags[i]}'=ANY (tag_names)" + sql_statement = sql_statement + f"\nAND '{tags[i]}'=ANY (tag_ids)" return sql_statement - return f"\n'{tags}'=ANY (tag_names)" + return f"\n'{tags}'=ANY (tag_ids)" def filter_by_flagged(self, flagged): print(flagged) @@ -192,7 +194,7 @@ def join_resident_attributes(self): def join_tag_attributes(self): return "\nLEFT JOIN\n \ - (SELECT logs.log_id, ARRAY_AGG(tags.name) AS tag_names FROM log_records logs\n \ + (SELECT logs.log_id, ARRAY_AGG(tags.tag_id) AS tag_ids, ARRAY_AGG(tags.name) AS tag_names FROM log_records logs\n \ JOIN log_record_tag lrt ON logs.log_id = lrt.log_record_id\n \ JOIN tags ON lrt.tag_id = tags.tag_id\n \ GROUP BY logs.log_id \n \ diff --git a/frontend/src/APIClients/TagAPIClient.ts b/frontend/src/APIClients/TagAPIClient.ts new file mode 100644 index 00000000..5c53885f --- /dev/null +++ b/frontend/src/APIClients/TagAPIClient.ts @@ -0,0 +1,24 @@ +import { AxiosError } from "axios"; +import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; +import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; +import baseAPIClient from "./BaseAPIClient"; +import { GetTagsResponse } from "../types/TagTypes"; + +const getTags = async (): Promise => { + try { + const bearerToken = `Bearer ${getLocalStorageObjProperty( + AUTHENTICATED_USER_KEY, + "accessToken", + )}`; + const { data } = await baseAPIClient.get(`/tags`, { + headers: { Authorization: bearerToken }, + }); + return data; + } catch (error) { + return null; + } +}; + +export default { + getTags, +}; diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index 1088daf1..d7f945e4 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -31,6 +31,7 @@ import { Col, Row } from "react-bootstrap"; import { AuthenticatedUser } from "../../types/AuthTypes"; import UserAPIClient from "../../APIClients/UserAPIClient"; import ResidentAPIClient from "../../APIClients/ResidentAPIClient"; +import TagAPIClient from "../../APIClients/TagAPIClient"; import { getLocalStorageObj } from "../../utils/LocalStorageUtils"; import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; import LogRecordAPIClient from "../../APIClients/LogRecordAPIClient"; @@ -39,7 +40,8 @@ import { BuildingLabel } from "../../types/BuildingTypes"; import selectStyle from "../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../theme/forms/datePickerStyles"; import { UserLabel } from "../../types/UserTypes"; -import { ResidentLabel } from "../../types/ResidentTypes"; +import { Resident, ResidentLabel } from "../../types/ResidentTypes"; +import { TagLabel } from "../../types/TagTypes"; import combineDateTime from "../../helper/combineDateTime"; type Props = { @@ -118,14 +120,15 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { ); const [buildingId, setBuildingId] = useState(-1); const [residents, setResidents] = useState([]); - const [tags, setTags] = useState([]); + const [tags, setTags] = useState([]); const [attnTo, setAttnTo] = useState(-1); const [notes, setNotes] = useState(""); const [flagged, setFlagged] = useState(false); const [employeeOptions, setEmployeeOptions] = useState([]); - const [residentOptions, setResidentOptions] = useState([]); + const [residentOptions, setResidentOptions] = useState([]); const [buildingOptions, setBuildingOptions] = useState([]); + const [tagOptions, setTagOptions] = useState([]); const [isCreateOpen, setCreateOpen] = React.useState(false); @@ -184,10 +187,14 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { }; const handleTagsChange = ( - selectedTags: MultiValue<{ label: string; value: string }>, + selectedTags: MultiValue, ) => { - const newTagsList = selectedTags.map((tag) => tag.value); - setTags(newTagsList); + const mutableSelectedTags: TagLabel[] = Array.from( + selectedTags, + ); + if (mutableSelectedTags !== null) { + setTags(mutableSelectedTags.map((tagLabel) => tagLabel.value)); + } }; const handleAttnToChange = ( @@ -206,7 +213,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { setNotesError(inputValue === ""); }; - // fetch resident + employee data for log creation + // fetch resident + employee + tag data for log creation const getLogEntryOptions = async () => { const buildingsData = await BuildingAPIClient.getBuildings(); @@ -239,6 +246,16 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { })); setEmployeeOptions(userLabels); } + + const tagsData = await TagAPIClient.getTags(); + if (tagsData && tagsData.tags.length !== 0) { + const tagLabels: TagLabel[] = tagsData.tags + .map((tag) => ({ + label: tag.name, + value: tag.tagId, + })); + setTagOptions(tagLabels); + } }; const handleCreateOpen = () => { @@ -426,9 +443,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { Tags logRecord.tags.includes(item.label), + )} /> diff --git a/frontend/src/components/pages/HomePage/HomePage.tsx b/frontend/src/components/pages/HomePage/HomePage.tsx index 49e5bfa4..77def219 100644 --- a/frontend/src/components/pages/HomePage/HomePage.tsx +++ b/frontend/src/components/pages/HomePage/HomePage.tsx @@ -10,7 +10,7 @@ import SearchAndFilters from "./SearchAndFilters"; import ExportCSVButton from "../../common/ExportCSVButton"; import { BuildingLabel } from "../../../types/BuildingTypes"; import { ResidentLabel } from "../../../types/ResidentTypes"; -import { Tag } from "../../../types/TagsTypes"; +import { TagLabel } from "../../../types/TagTypes"; import { UserLabel } from "../../../types/UserTypes"; import LogRecordAPIClient from "../../../APIClients/LogRecordAPIClient"; @@ -30,7 +30,7 @@ const HomePage = (): React.ReactElement => { const [employees, setEmployees] = useState([]); const [startDate, setStartDate] = useState(); const [endDate, setEndDate] = useState(); - const [tags, setTags] = useState([]); + const [tags, setTags] = useState([]); const [attentionTos, setAttentionTos] = useState([]); const [buildings, setBuildings] = useState([]); const [flagged, setFlagged] = useState(false); diff --git a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx index 643d40ca..9aa4c864 100644 --- a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx +++ b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx @@ -28,8 +28,11 @@ import AuthContext from "../../../contexts/AuthContext"; import EditLog from "../../forms/EditLog"; import LogRecordAPIClient from "../../../APIClients/LogRecordAPIClient"; import ResidentAPIClient from "../../../APIClients/ResidentAPIClient"; +import TagAPIClient from "../../../APIClients/TagAPIClient"; import UserAPIClient from "../../../APIClients/UserAPIClient"; import BuildingAPIClient from "../../../APIClients/BuildingAPIClient"; +import { ResidentLabel } from "../../../types/ResidentTypes"; +import { TagLabel } from "../../../types/TagTypes"; import { UserLabel } from "../../../types/UserTypes"; import { BuildingLabel } from "../../../types/BuildingTypes"; import ConfirmationModal from "../../common/ConfirmationModal"; @@ -71,7 +74,8 @@ const LogRecordsTable = ({ // Dropdown option states const [employeeOptions, setEmployeeOptions] = useState([]); - const [residentOptions, setResidentOptions] = useState([]); + const [residentOptions, setResidentOptions] = useState([]); + const [tagOptions, setTagOptions] = useState([]); // Handle delete confirmation toggle const handleDeleteToggle = (logId: number) => { @@ -89,7 +93,7 @@ const LogRecordsTable = ({ })); }; - // fetch resident + employee data for log creation + // fetch resident + employee + tag data for log creation const getLogEntryOptions = async () => { const residentsData = await ResidentAPIClient.getResidents({ returnAll: true, @@ -123,6 +127,16 @@ const LogRecordsTable = ({ })); setEmployeeOptions(userLabels); } + + const tagsData = await TagAPIClient.getTags(); + if (tagsData && tagsData.tags.length !== 0) { + const tagLabels: TagLabel[] = tagsData.tags + .map((tag) => ({ + label: tag.name, + value: tag.tagId, + })); + setTagOptions(tagLabels); + } }; const deleteLogRecord = async (itemId: number) => { @@ -232,6 +246,7 @@ const LogRecordsTable = ({ toggleClose={() => handleEditToggle(record.logId)} employeeOptions={employeeOptions} residentOptions={residentOptions} + tagOptions={tagOptions} getRecords={getRecords} countRecords={countRecords} setUserPageNum={setUserPageNum} diff --git a/frontend/src/components/pages/HomePage/SearchAndFilters.tsx b/frontend/src/components/pages/HomePage/SearchAndFilters.tsx index 7f973c4b..b4e7d704 100644 --- a/frontend/src/components/pages/HomePage/SearchAndFilters.tsx +++ b/frontend/src/components/pages/HomePage/SearchAndFilters.tsx @@ -21,10 +21,11 @@ import selectStyle from "../../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../../theme/forms/datePickerStyles"; import { BuildingLabel } from "../../../types/BuildingTypes"; import { Resident, ResidentLabel } from "../../../types/ResidentTypes"; -import { Tag } from "../../../types/TagsTypes"; +import { Tag, TagLabel } from "../../../types/TagTypes"; import { User, UserLabel } from "../../../types/UserTypes"; import UserAPIClient from "../../../APIClients/UserAPIClient"; import ResidentAPIClient from "../../../APIClients/ResidentAPIClient"; +import TagAPIClient from "../../../APIClients/TagAPIClient"; import BuildingAPIClient from "../../../APIClients/BuildingAPIClient"; import CreateToast from "../../common/Toasts"; @@ -33,7 +34,7 @@ type Props = { employees: UserLabel[]; startDate: Date | undefined; endDate: Date | undefined; - tags: Tag[]; + tags: TagLabel[]; attentionTos: UserLabel[]; buildings: BuildingLabel[]; flagged: boolean; @@ -41,19 +42,12 @@ type Props = { setEmployees: React.Dispatch>; setStartDate: React.Dispatch>; setEndDate: React.Dispatch>; - setTags: React.Dispatch>; + setTags: React.Dispatch>; setAttentionTos: React.Dispatch>; setBuildings: React.Dispatch>; setFlagged: React.Dispatch>; }; -// Replace this with the tags from the db once the API and table are made -const TAGS: Tag[] = [ - { label: "Tag A", value: "A" }, - { label: "Tag B", value: "B" }, - { label: "Tag C", value: "C" }, -]; - const SearchAndFilters = ({ residents, employees, @@ -75,6 +69,7 @@ const SearchAndFilters = ({ const [buildingOptions, setBuildingOptions] = useState([]); const [userLabels, setUserLabels] = useState(); const [residentLabels, setResidentLabels] = useState(); + const [tagLabels, setTagLabels] = useState(); const dateChangeToast = CreateToast(); @@ -117,6 +112,20 @@ const SearchAndFilters = ({ } }; + const getTags = async () => { + const data = await TagAPIClient.getTags(); + const tagsData = data?.tags; + if (tagsData) { + const labels = tagsData.map((tag: Tag) => { + return { + label: tag.name, + value: tag.tagId, + } as TagLabel; + }); + setTagLabels(labels); + } + }; + const handleBuildingChange = ( selectedBuildings: MultiValue, ) => { @@ -169,8 +178,8 @@ const SearchAndFilters = ({ setResidents(mutableSelectedResidents); }; - const handleTagsChange = (selectedTags: MultiValue) => { - const mutableSelectedTags: Tag[] = Array.from(selectedTags); + const handleTagsChange = (selectedTags: MultiValue) => { + const mutableSelectedTags: TagLabel[] = Array.from(selectedTags); setTags(mutableSelectedTags); }; @@ -189,6 +198,7 @@ const SearchAndFilters = ({ getBuildingsOptions(); getUsers(); getResidents(); + getTags(); }, []); return ( @@ -289,13 +299,12 @@ const SearchAndFilters = ({ Tags