From bb278a74d5d14e22569cfde54656606aacaa4538 Mon Sep 17 00:00:00 2001 From: Kelly Pham Date: Sun, 19 Nov 2023 16:13:58 -0500 Subject: [PATCH 1/8] add filters --- backend/app/rest/residents_routes.py | 6 ++- .../implementations/residents_service.py | 37 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/backend/app/rest/residents_routes.py b/backend/app/rest/residents_routes.py index 2efb0d1a..576c146c 100644 --- a/backend/app/rest/residents_routes.py +++ b/backend/app/rest/residents_routes.py @@ -111,6 +111,10 @@ def get_residents(): page_number = int(request.args.get("page_number")) except: pass + try: + filters = json.loads(request.args.get("filters")) + except: + filters = None results_per_page = 10 try: @@ -121,7 +125,7 @@ def get_residents(): try: resident_id = request.args.get("resident_id") residents_results = residents_service.get_residents( - return_all, page_number, results_per_page, resident_id + return_all, page_number, results_per_page, resident_id, filters ) return jsonify(residents_results), 201 except Exception as e: diff --git a/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index b9d0a982..5af20641 100644 --- a/backend/app/services/implementations/residents_service.py +++ b/backend/app/services/implementations/residents_service.py @@ -101,7 +101,7 @@ def delete_resident(self, resident_id): db.session.commit() def get_residents( - self, return_all, page_number, results_per_page, resident_id=None + self, return_all, page_number, results_per_page, resident_id=None, filters=None ): try: if resident_id: @@ -120,6 +120,41 @@ def get_residents( .with_entities(Residents, Buildings.name.label("building")) .all() ) + elif filters: + resident_id = filters.get("resident_id") + building_id = filters.get("building_id") + date_left = None + date_joined = None + + if filters.get("date_range") is not None: + date_joined, date_left = filters.get("date_range") + if date_joined is not None: + date_joined = datetime.strptime(date_joined, "%Y-%m-%d").replace( + hour=0, minute=0 + ) + if date_left is not None: + date_left = datetime.strptime(date_left, "%Y-%m-%d").replace( + hour=0, minute=0 + ) + + residents_results = ( + Residents.query.join( + Buildings, Buildings.id == Residents.building_id + ) + .with_entities(Residents, Buildings.name.label("building")) + ) + + if building_id is not None: + residents_results = residents_results.filter(Residents.building_id.in_(building_id)) + if resident_id is not None: + residents_results = residents_results.filter(Residents.resident_id.in_(resident_id)) + if date_joined is not None and date_left is not None: + residents_results = residents_results.filter(Residents.date_joined >= date_joined) + residents_results = residents_results.filter(Residents.date_left <= date_left) + elif date_joined is not None: + residents_results = residents_results.filter(Residents.date_joined >= date_joined) + elif date_left is not None: + residents_results = residents_results.filter(Residents.date_left <= date_left) else: residents_results = ( Residents.query.join( From 96158c86482d398b64416cd43c3d281da559a889 Mon Sep 17 00:00:00 2001 From: Kelly Pham Date: Sun, 19 Nov 2023 16:18:59 -0500 Subject: [PATCH 2/8] linter --- .../implementations/residents_service.py | 43 +++++++++++-------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index 5af20641..1a11a30d 100644 --- a/backend/app/services/implementations/residents_service.py +++ b/backend/app/services/implementations/residents_service.py @@ -127,34 +127,43 @@ def get_residents( date_joined = None if filters.get("date_range") is not None: - date_joined, date_left = filters.get("date_range") - if date_joined is not None: - date_joined = datetime.strptime(date_joined, "%Y-%m-%d").replace( - hour=0, minute=0 - ) + date_joined, date_left = filters.get("date_range") + if date_joined is not None: + date_joined = datetime.strptime( + date_joined, "%Y-%m-%d" + ).replace(hour=0, minute=0) if date_left is not None: date_left = datetime.strptime(date_left, "%Y-%m-%d").replace( hour=0, minute=0 ) - residents_results = ( - Residents.query.join( - Buildings, Buildings.id == Residents.building_id - ) - .with_entities(Residents, Buildings.name.label("building")) - ) + residents_results = Residents.query.join( + Buildings, Buildings.id == Residents.building_id + ).with_entities(Residents, Buildings.name.label("building")) if building_id is not None: - residents_results = residents_results.filter(Residents.building_id.in_(building_id)) + residents_results = residents_results.filter( + Residents.building_id.in_(building_id) + ) if resident_id is not None: - residents_results = residents_results.filter(Residents.resident_id.in_(resident_id)) + residents_results = residents_results.filter( + Residents.resident_id.in_(resident_id) + ) if date_joined is not None and date_left is not None: - residents_results = residents_results.filter(Residents.date_joined >= date_joined) - residents_results = residents_results.filter(Residents.date_left <= date_left) + residents_results = residents_results.filter( + Residents.date_joined >= date_joined + ) + residents_results = residents_results.filter( + Residents.date_left <= date_left + ) elif date_joined is not None: - residents_results = residents_results.filter(Residents.date_joined >= date_joined) + residents_results = residents_results.filter( + Residents.date_joined >= date_joined + ) elif date_left is not None: - residents_results = residents_results.filter(Residents.date_left <= date_left) + residents_results = residents_results.filter( + Residents.date_left <= date_left + ) else: residents_results = ( Residents.query.join( From 7bb09ea5cd78ecc6e6534d206350b9ebe83ba9bc Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Fri, 29 Dec 2023 22:41:32 -0500 Subject: [PATCH 3/8] create sep function for getting resident by ID and propogate duplicate error msg to frontend --- backend/app/rest/residents_routes.py | 15 ++++---- .../implementations/residents_service.py | 37 +++++++++++++------ .../services/interfaces/residents_service.py | 15 +++++--- frontend/src/APIClients/ResidentAPIClient.ts | 4 +- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/backend/app/rest/residents_routes.py b/backend/app/rest/residents_routes.py index 6b74fa13..9f8ffff1 100644 --- a/backend/app/rest/residents_routes.py +++ b/backend/app/rest/residents_routes.py @@ -24,9 +24,9 @@ def add_resident(): # Check for the existence of a resident prior to adding them fmt_resident_id = resident.get("initial") + str(resident.get("room_num")) try: - res = residents_service.get_residents(False, 1, 10, fmt_resident_id) - if len(res["residents"]) > 0: - return jsonify({"error": "Resident already with id {fmt_resident_id} already exists".format(fmt_resident_id=fmt_resident_id)}), 409 + existing_resident = residents_service.get_resident_by_id(fmt_resident_id) + if existing_resident: + return jsonify({"error": "Resident with ID {fmt_resident_id} already exists.".format(fmt_resident_id=fmt_resident_id)}), 409 except Exception as e: error_message = getattr(e, "message", None) return jsonify({"error": (error_message if error_message else str(e))}), 500 @@ -55,9 +55,9 @@ def update_resident(resident_id): # Check for the existence of a resident prior to adding them fmt_resident_id = updated_resident.get("initial") + str(updated_resident.get("room_num")) try: - res = residents_service.get_residents(False, 1, 10, fmt_resident_id) - if len(res["residents"]) == 1 and res["residents"][0]["id"] != resident_id: - return jsonify({"error": "Resident with id {fmt_resident_id} already exists".format(fmt_resident_id=fmt_resident_id)}), 409 + existing_resident = residents_service.get_resident_by_id(fmt_resident_id) + if existing_resident and existing_resident["id"] != resident_id: + return jsonify({"error": "Resident with ID {fmt_resident_id} already exists.".format(fmt_resident_id=fmt_resident_id)}), 409 except Exception as e: error_message = getattr(e, "message", None) return jsonify({"error": (error_message if error_message else str(e))}), 500 @@ -144,9 +144,8 @@ def get_residents(): pass try: - resident_id = request.args.get("resident_id") residents_results = residents_service.get_residents( - return_all, page_number, results_per_page, resident_id, filters + return_all, page_number, results_per_page, filters ) return jsonify(residents_results), 201 except Exception as e: diff --git a/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index 1a11a30d..da0d1181 100644 --- a/backend/app/services/implementations/residents_service.py +++ b/backend/app/services/implementations/residents_service.py @@ -1,6 +1,6 @@ from ..interfaces.residents_service import IResidentsService from ...models.residents import Residents -from ...models.log_records import LogRecords +from ...models.log_record_residents import LogRecordResidents from ...models.buildings import Buildings from ...models import db from datetime import datetime @@ -22,6 +22,13 @@ def __init__(self, logger): """ self.logger = logger + def to_resident_json(self, resident): + resident, building = resident[0], resident[1] + + resident_dict = resident.to_dict() + resident_dict["building"]["name"] = building + return resident_dict + def to_residents_json_list(self, resident_results): residents_json_list = [] for result in resident_results: @@ -81,7 +88,7 @@ def update_resident(self, resident_id, updated_resident): db.session.commit() def delete_resident(self, resident_id): - resident_log_records = LogRecords.query.filter_by( + resident_log_records = LogRecordResidents.query.filter_by( resident_id=resident_id ).count() if resident_log_records == 0: @@ -100,19 +107,25 @@ def delete_resident(self, resident_id): ) db.session.commit() + def get_resident_by_id(self, resident_id): + try: + resident = ( + Residents.query.join( + Buildings, Buildings.id == Residents.building_id + ) + .with_entities(Residents, Buildings.name.label("building")) + .filter_by(resident_id=resident_id) + .first() + ) + return self.to_resident_json(resident) + except Exception as postgres_error: + raise postgres_error + def get_residents( - self, return_all, page_number, results_per_page, resident_id=None, filters=None + self, return_all, page_number, results_per_page, filters=None ): try: - if resident_id: - residents_results = ( - Residents.query.join( - Buildings, Buildings.id == Residents.building_id - ) - .with_entities(Residents, Buildings.name.label("building")) - .filter_by(resident_id=resident_id) - ) - elif return_all: + if return_all: residents_results = ( Residents.query.join( Buildings, Buildings.id == Residents.building_id diff --git a/backend/app/services/interfaces/residents_service.py b/backend/app/services/interfaces/residents_service.py index 24796097..a677de27 100644 --- a/backend/app/services/interfaces/residents_service.py +++ b/backend/app/services/interfaces/residents_service.py @@ -50,14 +50,19 @@ def delete_resident(self, resident_id): @abstractmethod def get_residents( - self, return_all, page_number, results_per_page, resident_id=None + self, return_all, page_number, results_per_page, filters=None ): """ Gets residents in json format. - :param resident_id: id of resident to be deleted in the format of initial+room_num - :type resident_id: string - :param id: id of resident to be deleted, the primary key of the resident - :type resident_id: initial + :param return_all: whether to return all associated records or paginate + :type return_all: boolean + :param page_number: page number of records to retrieve + :type page_number: int + :param results_per_page: max number of records to retrieve for the page + :type results_per_page: int + :param filters: filters to apply to the query in json format + :type filters: json + :raises Exception: if resident retrieval fails """ pass diff --git a/frontend/src/APIClients/ResidentAPIClient.ts b/frontend/src/APIClients/ResidentAPIClient.ts index 64fb4cdb..83d9b4bc 100644 --- a/frontend/src/APIClients/ResidentAPIClient.ts +++ b/frontend/src/APIClients/ResidentAPIClient.ts @@ -78,7 +78,7 @@ const createResident = async ({ if (axiosErr.response && axiosErr.response.status === 409) { return { - errMessage: "Resident with the specified user ID already exists." + errMessage: axiosErr.response.data.error ?? `Resident with the specified user ID already exists.` }; } return false; @@ -128,7 +128,7 @@ const editResident = async ({ if (axiosErr.response && axiosErr.response.status === 409) { return { - errMessage: "Resident with the specified user ID already exists." + errMessage: axiosErr.response.data.error ?? "Resident with the specified user ID already exists." }; } return false; From 53d939632c78039cf216b2da98a56e428ead679c Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Sat, 30 Dec 2023 00:11:30 -0500 Subject: [PATCH 4/8] clean up naming --- .../implementations/log_records_service.py | 42 +++++++++---------- frontend/src/APIClients/LogRecordAPIClient.ts | 24 +++++------ .../components/pages/HomePage/HomePage.tsx | 12 +++--- frontend/src/types/LogRecordTypes.ts | 6 +-- 4 files changed, 42 insertions(+), 42 deletions(-) diff --git a/backend/app/services/implementations/log_records_service.py b/backend/app/services/implementations/log_records_service.py index 4f7e23c7..205bf29a 100644 --- a/backend/app/services/implementations/log_records_service.py +++ b/backend/app/services/implementations/log_records_service.py @@ -92,23 +92,23 @@ def to_json_list(self, logs): except Exception as postgres_error: raise postgres_error - def filter_by_building_id(self, building_id): - if type(building_id) == list: - sql_statement = f"\nlogs.building_id={building_id[0]}" - for i in range(1, len(building_id)): + def filter_by_buildings(self, buildings): + if type(buildings) == list: + sql_statement = f"\nlogs.building_id={buildings[0]}" + for i in range(1, len(buildings)): sql_statement = ( - sql_statement + f"\nOR logs.building_id={building_id[i]}" + sql_statement + f"\nOR logs.building_id={buildings[i]}" ) return sql_statement - return f"\logs.building_id={building_id}" + return f"\logs.building_id={buildings}" - def filter_by_employee_id(self, employee_id): - if type(employee_id) == list: - sql_statement = f"\nemployee_id={employee_id[0]}" - for i in range(1, len(employee_id)): - sql_statement = sql_statement + f"\nOR employee_id={employee_id[i]}" + def filter_by_employees(self, employees): + if type(employees) == list: + sql_statement = f"\nemployee_id={employees[0]}" + for i in range(1, len(employees)): + sql_statement = sql_statement + f"\nOR employee_id={employees[i]}" return sql_statement - return f"\nemployee_id={employee_id}" + return f"\nemployee_id={employees}" def filter_by_residents(self, residents): if type(residents) == list: @@ -120,13 +120,13 @@ def filter_by_residents(self, residents): return sql_statement return f"\n'{residents}'=ANY (resident_ids)" - def filter_by_attn_to(self, attn_to): - if type(attn_to) == list: - sql_statement = f"\nattn_to={attn_to[0]}" - for i in range(1, len(attn_to)): - sql_statement = sql_statement + f"\nOR attn_to={attn_to[i]}" + def filter_by_attn_tos(self, attn_tos): + if type(attn_tos) == list: + sql_statement = f"\nattn_to={attn_tos[0]}" + for i in range(1, len(attn_tos)): + sql_statement = sql_statement + f"\nOR attn_to={attn_tos[i]}" return sql_statement - return f"\nattn_to={attn_to}" + return f"\nattn_to={attn_tos}" def filter_by_date_range(self, date_range): sql = "" @@ -166,10 +166,10 @@ def filter_log_records(self, filters=None): is_first_filter = True options = { - "building_id": self.filter_by_building_id, - "employee_id": self.filter_by_employee_id, + "buildings": self.filter_by_buildings, + "employees": self.filter_by_employees, "residents": self.filter_by_residents, - "attn_to": self.filter_by_attn_to, + "attn_tos": self.filter_by_attn_tos, "date_range": self.filter_by_date_range, "tags": self.filter_by_tags, "flagged": self.filter_by_flagged, diff --git a/frontend/src/APIClients/LogRecordAPIClient.ts b/frontend/src/APIClients/LogRecordAPIClient.ts index 838eae7c..85b10c2a 100644 --- a/frontend/src/APIClients/LogRecordAPIClient.ts +++ b/frontend/src/APIClients/LogRecordAPIClient.ts @@ -12,9 +12,9 @@ import { } from "../types/LogRecordTypes"; const countLogRecords = async ({ - buildingId = [], - employeeId = [], - attnTo = [], + buildings = [], + employees = [], + attnTos = [], dateRange = [], residents = [], tags = [], @@ -28,9 +28,9 @@ const countLogRecords = async ({ const { data } = await baseAPIClient.get(`/log_records/count`, { params: { filters: { - buildingId, - employeeId, - attnTo, + buildings, + employees, + attnTos, dateRange, residents, tags, @@ -47,9 +47,9 @@ const countLogRecords = async ({ }; const filterLogRecords = async ({ - buildingId = [], - employeeId = [], - attnTo = [], + buildings = [], + employees = [], + attnTos = [], dateRange = [], residents = [], tags = [], @@ -66,9 +66,9 @@ const filterLogRecords = async ({ const { data } = await baseAPIClient.get(`/log_records`, { params: { filters: { - buildingId, - employeeId, - attnTo, + buildings, + employees, + attnTos, dateRange, residents, tags, diff --git a/frontend/src/components/pages/HomePage/HomePage.tsx b/frontend/src/components/pages/HomePage/HomePage.tsx index 50a101d4..b545c91c 100644 --- a/frontend/src/components/pages/HomePage/HomePage.tsx +++ b/frontend/src/components/pages/HomePage/HomePage.tsx @@ -68,9 +68,9 @@ const HomePage = (): React.ReactElement => { setTableLoaded(false) const data = await LogRecordAPIClient.filterLogRecords({ - buildingId: buildingIds, - employeeId: employeeIds, - attnTo: attentionToIds, + buildings: buildingIds, + employees: employeeIds, + attnTos: attentionToIds, dateRange: dateRange[0] === "" && dateRange[1] === "" ? [] : dateRange, residents: residentsIds, tags: tagsValues, @@ -105,9 +105,9 @@ const HomePage = (): React.ReactElement => { const tagsValues = tags.map((tag) => tag.value); const data = await LogRecordAPIClient.countLogRecords({ - buildingId: buildingIds, - employeeId: employeeIds, - attnTo: attentionToIds, + buildings: buildingIds, + employees: employeeIds, + attnTos: attentionToIds, dateRange, residents: residentsIds, tags: tagsValues, diff --git a/frontend/src/types/LogRecordTypes.ts b/frontend/src/types/LogRecordTypes.ts index 55af3672..49b834ce 100644 --- a/frontend/src/types/LogRecordTypes.ts +++ b/frontend/src/types/LogRecordTypes.ts @@ -41,9 +41,9 @@ export type PostLogRecordsResponse = Pick< > | null; export type CountLogRecordFilters = { - buildingId?: number[]; - employeeId?: number[]; - attnTo?: number[]; + buildings?: number[]; + employees?: number[]; + attnTos?: number[]; dateRange?: string[]; residents?: number[]; tags?: number[]; From 96d0f08759878d70e6012f54c050a735e74dd94d Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Sat, 30 Dec 2023 17:11:38 -0500 Subject: [PATCH 5/8] move status calculation to backend and add filtering for status --- backend/app/rest/residents_routes.py | 4 +- .../implementations/residents_service.py | 126 +++++++++++------- .../ResidentDirectoryTable.tsx | 21 +-- frontend/src/types/ResidentTypes.ts | 5 +- 4 files changed, 87 insertions(+), 69 deletions(-) diff --git a/backend/app/rest/residents_routes.py b/backend/app/rest/residents_routes.py index 9f8ffff1..b67c1fdc 100644 --- a/backend/app/rest/residents_routes.py +++ b/backend/app/rest/residents_routes.py @@ -132,10 +132,12 @@ def get_residents(): page_number = int(request.args.get("page_number")) except: pass + + filters = None try: filters = json.loads(request.args.get("filters")) except: - filters = None + pass results_per_page = 10 try: diff --git a/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index da0d1181..d7728264 100644 --- a/backend/app/services/implementations/residents_service.py +++ b/backend/app/services/implementations/residents_service.py @@ -1,11 +1,12 @@ +from flask import current_app from ..interfaces.residents_service import IResidentsService from ...models.residents import Residents from ...models.log_record_residents import LogRecordResidents from ...models.buildings import Buildings from ...models import db from datetime import datetime -from sqlalchemy import select, cast, Date -import json +from sqlalchemy.sql.expression import or_, and_ +from pytz import timezone class ResidentsService(IResidentsService): @@ -22,22 +23,38 @@ def __init__(self, logger): """ self.logger = logger - def to_resident_json(self, resident): + def to_resident_json(self, resident, current_date): resident, building = resident[0], resident[1] resident_dict = resident.to_dict() resident_dict["building"]["name"] = building + resident_dict["status"] = self.get_resident_status(current_date, + resident_dict["date_joined"], resident_dict["date_left"] + ) + return resident_dict - def to_residents_json_list(self, resident_results): + def to_residents_json_list(self, residents, current_date): residents_json_list = [] - for result in resident_results: + for result in residents: resident, building = result[0], result[1] resident_dict = resident.to_dict() resident_dict["building"]["name"] = building + resident_dict["status"] = self.get_resident_status(current_date, + resident_dict["date_joined"], resident_dict["date_left"] + ) + residents_json_list.append(resident_dict) return residents_json_list + + def get_resident_status(self, current_date, date_joined, date_left): + if current_date < date_joined: + return "Future" + elif date_left is None or current_date <= date_left: + return "Current" + else: + return "Past" def convert_to_date_obj(self, date): return datetime.strptime(date, "%Y-%m-%d") @@ -109,6 +126,8 @@ def delete_resident(self, resident_id): def get_resident_by_id(self, resident_id): try: + current_date = datetime.now(timezone('US/Eastern')).strftime('%Y-%m-%d') + resident = ( Residents.query.join( Buildings, Buildings.id == Residents.building_id @@ -117,7 +136,7 @@ def get_resident_by_id(self, resident_id): .filter_by(resident_id=resident_id) .first() ) - return self.to_resident_json(resident) + return self.to_resident_json(resident, current_date) if resident else None except Exception as postgres_error: raise postgres_error @@ -125,17 +144,18 @@ def get_residents( self, return_all, page_number, results_per_page, filters=None ): try: - if return_all: - residents_results = ( - Residents.query.join( - Buildings, Buildings.id == Residents.building_id - ) - .with_entities(Residents, Buildings.name.label("building")) - .all() + current_date = datetime.now(timezone('US/Eastern')).strftime('%Y-%m-%d') + + residents_results = ( + Residents.query.join( + Buildings, Buildings.id == Residents.building_id ) - elif filters: - resident_id = filters.get("resident_id") - building_id = filters.get("building_id") + .with_entities(Residents, Buildings.name.label("building")) + ) + if filters: + residents = filters.get("residents") + buildings = filters.get("buildings") + statuses = filters.get("statuses") date_left = None date_joined = None @@ -149,46 +169,52 @@ def get_residents( date_left = datetime.strptime(date_left, "%Y-%m-%d").replace( hour=0, minute=0 ) + + if filters.get("current_date") is not None: + current_date = filters.get("current_date") + current_date = datetime.strptime(date_joined, "%Y-%m-%d").replace(hour=0, minute=0) - residents_results = Residents.query.join( - Buildings, Buildings.id == Residents.building_id - ).with_entities(Residents, Buildings.name.label("building")) - - if building_id is not None: - residents_results = residents_results.filter( - Residents.building_id.in_(building_id) - ) - if resident_id is not None: + if buildings is not None: residents_results = residents_results.filter( - Residents.resident_id.in_(resident_id) + Residents.building_id.in_(buildings) ) - if date_joined is not None and date_left is not None: + if residents is not None: residents_results = residents_results.filter( - Residents.date_joined >= date_joined + Residents.id.in_(residents) ) - residents_results = residents_results.filter( - Residents.date_left <= date_left - ) - elif date_joined is not None: - residents_results = residents_results.filter( - Residents.date_joined >= date_joined - ) - elif date_left is not None: - residents_results = residents_results.filter( - Residents.date_left <= date_left - ) - else: - residents_results = ( - Residents.query.join( - Buildings, Buildings.id == Residents.building_id - ) - .limit(results_per_page) - .offset((page_number - 1) * results_per_page) - .with_entities(Residents, Buildings.name.label("building")) - .all() - ) + if statuses is not None: + conditions = [] + + #Construct the conditions for each case + for status in statuses: + if status == "Future": + conditions.append(Residents.date_joined > current_date); + elif status == "Current": + conditions.append(and_(Residents.date_joined <= current_date, + or_(Residents.date_left.is_(None), Residents.date_left >= current_date))) + elif status == "Past": + conditions.append(Residents.date_left < current_date) + + #OR them together and add to filter + residents_results = residents_results.filter(or_(*conditions)) + + else: + if date_joined is not None: + residents_results = residents_results.filter( + Residents.date_joined >= date_joined + ) + if date_left is not None: + residents_results = residents_results.filter( + Residents.date_left <= date_left + ) + + if not return_all: + residents_results = residents_results.limit(results_per_page).offset((page_number - 1) * results_per_page) + + residents_results = residents_results.all() - return {"residents": self.to_residents_json_list(residents_results)} + return {"residents": self.to_residents_json_list(residents_results, current_date)} + except Exception as postgres_error: raise postgres_error diff --git a/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryTable.tsx b/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryTable.tsx index 6ae3450d..ad4e5f1c 100644 --- a/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryTable.tsx +++ b/frontend/src/components/pages/ResidentDirectory/ResidentDirectoryTable.tsx @@ -55,28 +55,19 @@ const getStatusColor = (status: string): string => { return color; }; -const getFormattedDatesAndStatus = (resident: Resident) => { +const getFormattedDates = (resident: Resident) => { const startDateObj = convertToDate(resident.dateJoined); const startDate = getFormattedDateAndTime(startDateObj, true); let endDate; - let status = ResidentStatus.CURRENT; - const currentDate = new Date(); - currentDate.setHours(0,0,0,0); if (resident.dateLeft != null) { const endDateObj = convertToDate(resident.dateLeft); endDate = getFormattedDateAndTime(endDateObj, true); - if (endDateObj < currentDate) { - status = ResidentStatus.PAST; - } - } - if (currentDate < startDateObj) { - status = ResidentStatus.FUTURE; } + return { startDate, endDate, - status, }; }; @@ -174,9 +165,7 @@ const ResidentDirectoryTable = ({ {residents.map((resident) => { - const { startDate, endDate, status } = getFormattedDatesAndStatus( - resident, - ); + const { startDate, endDate } = getFormattedDates(resident); // TODO: Remove non-null assertion from residentId return ( @@ -186,12 +175,12 @@ const ResidentDirectoryTable = ({ textAlign="center" > - {status} + {resident.status} {resident.building.name} diff --git a/frontend/src/types/ResidentTypes.ts b/frontend/src/types/ResidentTypes.ts index 8bc21607..b9e1ae15 100644 --- a/frontend/src/types/ResidentTypes.ts +++ b/frontend/src/types/ResidentTypes.ts @@ -11,6 +11,7 @@ export type Resident = { dateJoined: string; dateLeft?: string; building: BuildingRecord; + status: ResidentStatus; }; export type ResidentLabel = { @@ -28,10 +29,10 @@ export type CountResidentsResponse = { export type CreateResidentParams = Omit< Resident, - "id" | "residentId" | "dateLeft" | "building" + "id" | "residentId" | "dateLeft" | "building" | "status" > & { buildingId: number }; -export type EditResidentParams = Omit & { +export type EditResidentParams = Omit & { buildingId: number; }; From 224e81d63d55a22394f2a6e5ffc86c87e9265a27 Mon Sep 17 00:00:00 2001 From: Connor Bechthold Date: Mon, 1 Jan 2024 01:04:39 -0500 Subject: [PATCH 6/8] create filter component for resident directory, add clear functionality to dates, update backend route for counting residents to support filters, general code cleanup --- backend/app/rest/residents_routes.py | 9 +- .../implementations/residents_service.py | 125 +++++---- .../services/interfaces/residents_service.py | 4 +- frontend/src/APIClients/ResidentAPIClient.ts | 29 ++- frontend/src/components/forms/CreateLog.tsx | 32 ++- .../src/components/forms/CreateResident.tsx | 6 +- frontend/src/components/forms/EditLog.tsx | 23 +- .../src/components/forms/EditResident.tsx | 9 +- .../components/pages/HomePage/HomePage.tsx | 15 +- .../pages/HomePage/SearchAndFilters.tsx | 141 +++++----- .../pages/ResidentDirectory/Filters.tsx | 245 ++++++++++++++++++ .../ResidentDirectory/ResidentDirectory.tsx | 81 +++++- frontend/src/theme/common/buttonStyles.tsx | 5 + frontend/src/theme/forms/datePickerStyles.tsx | 3 + frontend/src/types/BuildingTypes.ts | 6 - frontend/src/types/ResidentTypes.ts | 21 +- frontend/src/types/SharedTypes.ts | 4 + frontend/src/types/TagTypes.ts | 5 - frontend/src/types/UserTypes.ts | 5 - 19 files changed, 567 insertions(+), 201 deletions(-) create mode 100644 frontend/src/components/pages/ResidentDirectory/Filters.tsx create mode 100644 frontend/src/types/SharedTypes.ts diff --git a/backend/app/rest/residents_routes.py b/backend/app/rest/residents_routes.py index b67c1fdc..a1b12314 100644 --- a/backend/app/rest/residents_routes.py +++ b/backend/app/rest/residents_routes.py @@ -159,10 +159,15 @@ def get_residents(): @require_authorization_by_role({"Relief Staff", "Regular Staff", "Admin"}) def count_residents(): """ - Get number of residents + Get number of residents. Can optionally add filters """ try: - residents = residents_service.count_residents() + filters = json.loads(request.args.get("filters")) + except: + filters = None + + try: + residents = residents_service.count_residents(filters) return jsonify(residents), 201 except Exception as e: error_message = getattr(e, "message", None) diff --git a/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index d7728264..a5b9964a 100644 --- a/backend/app/services/implementations/residents_service.py +++ b/backend/app/services/implementations/residents_service.py @@ -75,6 +75,60 @@ def is_date_left_invalid_resident(self, resident): return False + def construct_filters(self, query, filters, current_date): + + residents = filters.get("residents") + buildings = filters.get("buildings") + statuses = filters.get("statuses") + date_left = None + date_joined = None + + if filters.get("date_range") is not None: + date_joined, date_left = filters.get("date_range") + if date_joined is not None: + date_joined = datetime.strptime( + date_joined, "%Y-%m-%d" + ).replace(hour=0, minute=0) + if date_left is not None: + date_left = datetime.strptime(date_left, "%Y-%m-%d").replace( + hour=0, minute=0 + ) + + if buildings is not None: + query = query.filter( + Residents.building_id.in_(buildings) + ) + if residents is not None: + query = query.filter( + Residents.id.in_(residents) + ) + if statuses is not None: + conditions = [] + + #Construct the conditions for each case + for status in statuses: + if status == "Future": + conditions.append(Residents.date_joined > current_date); + elif status == "Current": + conditions.append(and_(Residents.date_joined <= current_date, + or_(Residents.date_left.is_(None), Residents.date_left >= current_date))) + elif status == "Past": + conditions.append(Residents.date_left < current_date) + + #OR them together and add to filter + query = query.filter(or_(*conditions)) + + if date_joined is not None: + query = query.filter( + Residents.date_joined >= date_joined + ) + if date_left is not None: + query = query.filter( + Residents.date_left <= date_left + ) + + return query + def add_resident(self, resident): try: new_resident = Residents(**resident) @@ -153,60 +207,7 @@ def get_residents( .with_entities(Residents, Buildings.name.label("building")) ) if filters: - residents = filters.get("residents") - buildings = filters.get("buildings") - statuses = filters.get("statuses") - date_left = None - date_joined = None - - if filters.get("date_range") is not None: - date_joined, date_left = filters.get("date_range") - if date_joined is not None: - date_joined = datetime.strptime( - date_joined, "%Y-%m-%d" - ).replace(hour=0, minute=0) - if date_left is not None: - date_left = datetime.strptime(date_left, "%Y-%m-%d").replace( - hour=0, minute=0 - ) - - if filters.get("current_date") is not None: - current_date = filters.get("current_date") - current_date = datetime.strptime(date_joined, "%Y-%m-%d").replace(hour=0, minute=0) - - if buildings is not None: - residents_results = residents_results.filter( - Residents.building_id.in_(buildings) - ) - if residents is not None: - residents_results = residents_results.filter( - Residents.id.in_(residents) - ) - if statuses is not None: - conditions = [] - - #Construct the conditions for each case - for status in statuses: - if status == "Future": - conditions.append(Residents.date_joined > current_date); - elif status == "Current": - conditions.append(and_(Residents.date_joined <= current_date, - or_(Residents.date_left.is_(None), Residents.date_left >= current_date))) - elif status == "Past": - conditions.append(Residents.date_left < current_date) - - #OR them together and add to filter - residents_results = residents_results.filter(or_(*conditions)) - - else: - if date_joined is not None: - residents_results = residents_results.filter( - Residents.date_joined >= date_joined - ) - if date_left is not None: - residents_results = residents_results.filter( - Residents.date_left <= date_left - ) + residents_results = self.construct_filters(residents_results, filters, current_date) if not return_all: residents_results = residents_results.limit(results_per_page).offset((page_number - 1) * results_per_page) @@ -218,9 +219,21 @@ def get_residents( except Exception as postgres_error: raise postgres_error - def count_residents(self): + def count_residents(self, filters): try: - count = Residents.query.count() + + residents_results = ( + Residents.query.join( + Buildings, Buildings.id == Residents.building_id + ) + .with_entities(Residents, Buildings.name.label("building")) + ) + + if filters: + current_date = datetime.now(timezone('US/Eastern')).strftime('%Y-%m-%d') + residents_results = self.construct_filters(residents_results, filters, current_date) + + count = residents_results.count() return {"num_results": count} except Exception as postgres_error: diff --git a/backend/app/services/interfaces/residents_service.py b/backend/app/services/interfaces/residents_service.py index a677de27..d8f30618 100644 --- a/backend/app/services/interfaces/residents_service.py +++ b/backend/app/services/interfaces/residents_service.py @@ -67,9 +67,11 @@ def get_residents( pass @abstractmethod - def count_residents(self): + def count_residents(self, filters): """ Count the total number of residents + :param filters: filters for the query + :type filters: json :return: count of residents :rtype: int :raises Exception: if resident count fails diff --git a/frontend/src/APIClients/ResidentAPIClient.ts b/frontend/src/APIClients/ResidentAPIClient.ts index 83d9b4bc..af375a81 100644 --- a/frontend/src/APIClients/ResidentAPIClient.ts +++ b/frontend/src/APIClients/ResidentAPIClient.ts @@ -6,6 +6,8 @@ import { CountResidentsResponse, CreateResidentParams, EditResidentParams, + GetResidentsParams, + CountResidentsParams, } from "../types/ResidentTypes"; import { ResidentErrorResponse } from "../types/ErrorTypes" import { getLocalStorageObjProperty } from "../utils/LocalStorageUtils"; @@ -15,7 +17,11 @@ const getResidents = async ({ returnAll = false, pageNumber = 1, resultsPerPage = 10, -}): Promise => { + residents, + buildings, + statuses, + dateRange +}: GetResidentsParams): Promise => { try { const bearerToken = `Bearer ${getLocalStorageObjProperty( AUTHENTICATED_USER_KEY, @@ -25,6 +31,12 @@ const getResidents = async ({ `/residents/`, { params: { + filters: { + residents, + buildings, + statuses, + dateRange + }, returnAll, pageNumber, resultsPerPage, @@ -38,7 +50,12 @@ const getResidents = async ({ } }; -const countResidents = async (): Promise => { +const countResidents = async ({ + residents, + buildings, + statuses, + dateRange +}: CountResidentsParams): Promise => { try { const bearerToken = `Bearer ${getLocalStorageObjProperty( AUTHENTICATED_USER_KEY, @@ -47,6 +64,14 @@ const countResidents = async (): Promise => { const { data } = await baseAPIClient.get( `/residents/count`, { + params: { + filters: { + residents, + buildings, + statuses, + dateRange + }, + }, headers: { Authorization: bearerToken }, }, ); diff --git a/frontend/src/components/forms/CreateLog.tsx b/frontend/src/components/forms/CreateLog.tsx index d7f945e4..3233ac92 100644 --- a/frontend/src/components/forms/CreateLog.tsx +++ b/frontend/src/components/forms/CreateLog.tsx @@ -36,13 +36,11 @@ 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"; -import { Resident, ResidentLabel } from "../../types/ResidentTypes"; -import { TagLabel } from "../../types/TagTypes"; +import { Resident } from "../../types/ResidentTypes"; import combineDateTime from "../../helper/combineDateTime"; +import { SelectLabel } from "../../types/SharedTypes"; type Props = { getRecords: (pageNumber: number) => Promise; @@ -109,7 +107,7 @@ const getCurUserSelectOption = () => { const CreateLog = ({ getRecords, countRecords, setUserPageNum }: 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()); + const [employee, setEmployee] = useState(getCurUserSelectOption()); const [date, setDate] = useState(new Date()); const [time, setTime] = useState( date.toLocaleTimeString([], { @@ -125,10 +123,10 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { const [notes, setNotes] = useState(""); const [flagged, setFlagged] = useState(false); - const [employeeOptions, setEmployeeOptions] = useState([]); - const [residentOptions, setResidentOptions] = useState([]); - const [buildingOptions, setBuildingOptions] = useState([]); - const [tagOptions, setTagOptions] = useState([]); + const [employeeOptions, setEmployeeOptions] = useState([]); + const [residentOptions, setResidentOptions] = useState([]); + const [buildingOptions, setBuildingOptions] = useState([]); + const [tagOptions, setTagOptions] = useState([]); const [isCreateOpen, setCreateOpen] = React.useState(false); @@ -174,9 +172,9 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { }; const handleResidentsChange = ( - selectedResidents: MultiValue, + selectedResidents: MultiValue, ) => { - const mutableSelectedResidents: ResidentLabel[] = Array.from( + const mutableSelectedResidents: SelectLabel[] = Array.from( selectedResidents, ); if (mutableSelectedResidents !== null) { @@ -187,9 +185,9 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { }; const handleTagsChange = ( - selectedTags: MultiValue, + selectedTags: MultiValue, ) => { - const mutableSelectedTags: TagLabel[] = Array.from( + const mutableSelectedTags: SelectLabel[] = Array.from( selectedTags, ); if (mutableSelectedTags !== null) { @@ -218,7 +216,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { const buildingsData = await BuildingAPIClient.getBuildings(); if (buildingsData && buildingsData.buildings.length !== 0) { - const buildingLabels: BuildingLabel[] = buildingsData.buildings.map( + const buildingLabels: SelectLabel[] = buildingsData.buildings.map( (building) => ({ label: building.name!, value: building.id! }), ); setBuildingOptions(buildingLabels); @@ -230,7 +228,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { if (residentsData && residentsData.residents.length !== 0) { // TODO: Remove the type assertions here - const residentLabels: ResidentLabel[] = residentsData.residents.map( + const residentLabels: SelectLabel[] = residentsData.residents.map( (r) => ({ label: r.residentId!, value: r.id! }), ); setResidentOptions(residentLabels); @@ -238,7 +236,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { const usersData = await UserAPIClient.getUsers({ returnAll: true }); if (usersData && usersData.users.length !== 0) { - const userLabels: UserLabel[] = usersData.users + const userLabels: SelectLabel[] = usersData.users .filter((user) => user.userStatus === "Active") .map((user) => ({ label: user.firstName, @@ -249,7 +247,7 @@ const CreateLog = ({ getRecords, countRecords, setUserPageNum }: Props) => { const tagsData = await TagAPIClient.getTags(); if (tagsData && tagsData.tags.length !== 0) { - const tagLabels: TagLabel[] = tagsData.tags + const tagLabels: SelectLabel[] = tagsData.tags .map((tag) => ({ label: tag.name, value: tag.tagId, diff --git a/frontend/src/components/forms/CreateResident.tsx b/frontend/src/components/forms/CreateResident.tsx index 0f30f388..98c055ba 100644 --- a/frontend/src/components/forms/CreateResident.tsx +++ b/frontend/src/components/forms/CreateResident.tsx @@ -30,9 +30,9 @@ import selectStyle from "../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../theme/forms/datePickerStyles"; import ResidentAPIClient from "../../APIClients/ResidentAPIClient"; import BuildingAPIClient from "../../APIClients/BuildingAPIClient"; -import { BuildingLabel } from "../../types/BuildingTypes"; import { convertToString } from "../../helper/dateHelpers"; import { isResidentErrorResponse } from "../../helper/error" +import { SelectLabel } from "../../types/SharedTypes"; type Props = { getRecords: (pageNumber: number) => Promise; @@ -45,7 +45,7 @@ const CreateResident = ({ setUserPageNum, countResidents, }: Props): React.ReactElement => { - const [buildingOptions, setBuildingOptions] = useState([]); + const [buildingOptions, setBuildingOptions] = useState([]); const [initials, setInitials] = useState(""); const [roomNumber, setRoomNumber] = useState(""); const [moveInDate, setMoveInDate] = useState(new Date()); @@ -122,7 +122,7 @@ const CreateResident = ({ const buildingsData = await BuildingAPIClient.getBuildings(); if (buildingsData && buildingsData.buildings.length !== 0) { - const buildingLabels: BuildingLabel[] = buildingsData.buildings.map( + const buildingLabels: SelectLabel[] = buildingsData.buildings.map( (building) => ({ label: building.name!, value: building.id! }), ); setBuildingOptions(buildingLabels); diff --git a/frontend/src/components/forms/EditLog.tsx b/frontend/src/components/forms/EditLog.tsx index 12fcdda0..430130a0 100644 --- a/frontend/src/components/forms/EditLog.tsx +++ b/frontend/src/components/forms/EditLog.tsx @@ -33,25 +33,22 @@ import AUTHENTICATED_USER_KEY from "../../constants/AuthConstants"; import LogRecordAPIClient from "../../APIClients/LogRecordAPIClient"; import selectStyle from "../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../theme/forms/datePickerStyles"; -import { ResidentLabel } from "../../types/ResidentTypes"; -import { BuildingLabel } from "../../types/BuildingTypes"; -import { TagLabel } from "../../types/TagTypes"; -import { UserLabel } from "../../types/UserTypes"; import { LogRecord } from "../../types/LogRecordTypes"; import { combineDateTime } from "../../helper/dateHelpers"; +import { SelectLabel } from "../../types/SharedTypes"; type Props = { logRecord: LogRecord; userPageNum: number; isOpen: boolean; toggleClose: () => void; - employeeOptions: UserLabel[]; - residentOptions: ResidentLabel[]; - tagOptions: TagLabel[]; + employeeOptions: SelectLabel[]; + residentOptions: SelectLabel[]; + tagOptions: SelectLabel[]; getRecords: (pageNumber: number) => Promise; countRecords: () => Promise; setUserPageNum: React.Dispatch>; - buildingOptions: BuildingLabel[]; + buildingOptions: SelectLabel[]; }; type AlertData = { @@ -111,7 +108,7 @@ const EditLog = ({ 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()); + const [employee, setEmployee] = useState(getCurUserSelectOption()); const [date, setDate] = useState(new Date()); const [time, setTime] = useState( date.toLocaleTimeString([], { @@ -169,9 +166,9 @@ const EditLog = ({ }; const handleResidentsChange = ( - selectedResidents: MultiValue, + selectedResidents: MultiValue, ) => { - const mutableSelectedResidents: ResidentLabel[] = Array.from( + const mutableSelectedResidents: SelectLabel[] = Array.from( selectedResidents, ); if (mutableSelectedResidents !== null) { @@ -182,9 +179,9 @@ const EditLog = ({ }; const handleTagsChange = ( - selectedTags: MultiValue, + selectedTags: MultiValue, ) => { - const mutableSelectedTags: TagLabel[] = Array.from( + const mutableSelectedTags: SelectLabel[] = Array.from( selectedTags, ); if (mutableSelectedTags !== null) { diff --git a/frontend/src/components/forms/EditResident.tsx b/frontend/src/components/forms/EditResident.tsx index 8df8ce26..fc7c3b9b 100644 --- a/frontend/src/components/forms/EditResident.tsx +++ b/frontend/src/components/forms/EditResident.tsx @@ -23,22 +23,19 @@ import { ScaleFade, Divider, } from "@chakra-ui/react"; -import type { AlertStatus } from "@chakra-ui/react"; -import { AddIcon } from "@chakra-ui/icons"; import { SingleDatepicker } from "chakra-dayzed-datepicker"; -import { Card, Col, Row } from "react-bootstrap"; +import { Col, Row } from "react-bootstrap"; import ResidentAPIClient from "../../APIClients/ResidentAPIClient"; import { Resident } from "../../types/ResidentTypes"; -import BuildingAPIClient from "../../APIClients/BuildingAPIClient"; -import { BuildingLabel } from "../../types/BuildingTypes"; import selectStyle from "../../theme/forms/selectStyles"; import { singleDatePickerStyle } from "../../theme/forms/datePickerStyles"; import CreateToast from "../common/Toasts"; import { convertToDate, convertToString } from "../../helper/dateHelpers"; import { isResidentErrorResponse } from "../../helper/error" +import { SelectLabel } from "../../types/SharedTypes"; type Props = { - buildingOptions: BuildingLabel[], + buildingOptions: SelectLabel[], resident: Resident; isOpen: boolean; userPageNum: number; diff --git a/frontend/src/components/pages/HomePage/HomePage.tsx b/frontend/src/components/pages/HomePage/HomePage.tsx index b545c91c..349e8054 100644 --- a/frontend/src/components/pages/HomePage/HomePage.tsx +++ b/frontend/src/components/pages/HomePage/HomePage.tsx @@ -8,11 +8,8 @@ import { LogRecord } from "../../../types/LogRecordTypes"; import LogRecordsTable from "./LogRecordsTable"; import SearchAndFilters from "./SearchAndFilters"; import ExportCSVButton from "../../common/ExportCSVButton"; -import { BuildingLabel } from "../../../types/BuildingTypes"; -import { ResidentLabel } from "../../../types/ResidentTypes"; -import { TagLabel } from "../../../types/TagTypes"; -import { UserLabel } from "../../../types/UserTypes"; import LogRecordAPIClient from "../../../APIClients/LogRecordAPIClient"; +import { SelectLabel } from "../../../types/SharedTypes"; const HomePage = (): React.ReactElement => { /* TODO: change inputs to correct types @@ -26,13 +23,13 @@ const HomePage = (): React.ReactElement => { */ // TODO: search by resident // Filter state - const [residents, setResidents] = useState([]); - const [employees, setEmployees] = useState([]); + const [residents, setResidents] = useState([]); + const [employees, setEmployees] = useState([]); const [startDate, setStartDate] = useState(); const [endDate, setEndDate] = useState(); - const [tags, setTags] = useState([]); - const [attentionTos, setAttentionTos] = useState([]); - const [buildings, setBuildings] = useState([]); + const [tags, setTags] = useState([]); + const [attentionTos, setAttentionTos] = useState([]); + const [buildings, setBuildings] = useState([]); const [flagged, setFlagged] = useState(false); // Record/page state diff --git a/frontend/src/components/pages/HomePage/SearchAndFilters.tsx b/frontend/src/components/pages/HomePage/SearchAndFilters.tsx index b4e7d704..81d7b401 100644 --- a/frontend/src/components/pages/HomePage/SearchAndFilters.tsx +++ b/frontend/src/components/pages/HomePage/SearchAndFilters.tsx @@ -13,38 +13,42 @@ import { FormControl, FormLabel, Text, + IconButton, + InputGroup, + InputRightElement, } from "@chakra-ui/react"; +import { SmallCloseIcon } from "@chakra-ui/icons"; import { Card } from "react-bootstrap"; import Select, { MultiValue, SingleValue } from "react-select"; import { SingleDatepicker } from "chakra-dayzed-datepicker"; 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, TagLabel } from "../../../types/TagTypes"; -import { User, UserLabel } from "../../../types/UserTypes"; +import { Resident } from "../../../types/ResidentTypes"; +import { Tag } from "../../../types/TagTypes"; +import { User } 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"; +import { SelectLabel } from "../../../types/SharedTypes"; type Props = { - residents: ResidentLabel[]; - employees: UserLabel[]; + residents: SelectLabel[]; + employees: SelectLabel[]; startDate: Date | undefined; endDate: Date | undefined; - tags: TagLabel[]; - attentionTos: UserLabel[]; - buildings: BuildingLabel[]; + tags: SelectLabel[]; + attentionTos: SelectLabel[]; + buildings: SelectLabel[]; flagged: boolean; - setResidents: React.Dispatch>; - setEmployees: React.Dispatch>; + setResidents: React.Dispatch>; + setEmployees: React.Dispatch>; setStartDate: React.Dispatch>; setEndDate: React.Dispatch>; - setTags: React.Dispatch>; - setAttentionTos: React.Dispatch>; - setBuildings: React.Dispatch>; + setTags: React.Dispatch>; + setAttentionTos: React.Dispatch>; + setBuildings: React.Dispatch>; setFlagged: React.Dispatch>; }; @@ -66,10 +70,10 @@ const SearchAndFilters = ({ setBuildings, setFlagged, }: Props): React.ReactElement => { - const [buildingOptions, setBuildingOptions] = useState([]); - const [userLabels, setUserLabels] = useState(); - const [residentLabels, setResidentLabels] = useState(); - const [tagLabels, setTagLabels] = useState(); + const [buildingOptions, setBuildingOptions] = useState([]); + const [userLabels, setUserLabels] = useState(); + const [residentLabels, setResidentLabels] = useState(); + const [tagLabels, setTagLabels] = useState(); const dateChangeToast = CreateToast(); @@ -77,7 +81,7 @@ const SearchAndFilters = ({ const buildingsData = await BuildingAPIClient.getBuildings(); if (buildingsData && buildingsData.buildings.length !== 0) { - const buildingLabels: BuildingLabel[] = buildingsData.buildings.map( + const buildingLabels: SelectLabel[] = buildingsData.buildings.map( (building) => ({ label: building.name!, value: building.id! }), ); setBuildingOptions(buildingLabels); @@ -92,7 +96,7 @@ const SearchAndFilters = ({ return { label: `${user.firstName} ${user.lastName}`, value: user.id, - } as UserLabel; + } as SelectLabel; }); setUserLabels(labels); } @@ -106,7 +110,7 @@ const SearchAndFilters = ({ return { label: `${resident.residentId}`, value: resident.id, - } as ResidentLabel; + } as SelectLabel; }); setResidentLabels(labels); } @@ -120,28 +124,28 @@ const SearchAndFilters = ({ return { label: tag.name, value: tag.tagId, - } as TagLabel; + } as SelectLabel; }); setTagLabels(labels); } }; const handleBuildingChange = ( - selectedBuildings: MultiValue, + selectedBuildings: MultiValue, ) => { - const mutableSelectedBuildings: BuildingLabel[] = Array.from( + const mutableSelectedBuildings: SelectLabel[] = Array.from( selectedBuildings, ); setBuildings(mutableSelectedBuildings); }; - const handleAttnToChange = (selectedAttnTos: MultiValue) => { - const mutableSelectedAttnTos: UserLabel[] = Array.from(selectedAttnTos); + const handleAttnToChange = (selectedAttnTos: MultiValue) => { + const mutableSelectedAttnTos: SelectLabel[] = Array.from(selectedAttnTos); setAttentionTos(mutableSelectedAttnTos); }; - const handleEmployeesChange = (selectedEmployees: MultiValue) => { - const mutableSelectedEmployees: UserLabel[] = Array.from(selectedEmployees); + const handleEmployeesChange = (selectedEmployees: MultiValue) => { + const mutableSelectedEmployees: SelectLabel[] = Array.from(selectedEmployees); setEmployees(mutableSelectedEmployees); }; @@ -170,16 +174,16 @@ const SearchAndFilters = ({ }; const handleResidentsChange = ( - selectedResidents: MultiValue, + selectedResidents: MultiValue, ) => { - const mutableSelectedResidents: ResidentLabel[] = Array.from( + const mutableSelectedResidents: SelectLabel[] = Array.from( selectedResidents, ); setResidents(mutableSelectedResidents); }; - const handleTagsChange = (selectedTags: MultiValue) => { - const mutableSelectedTags: TagLabel[] = Array.from(selectedTags); + const handleTagsChange = (selectedTags: MultiValue) => { + const mutableSelectedTags: SelectLabel[] = Array.from(selectedTags); setTags(mutableSelectedTags); }; @@ -216,7 +220,7 @@ const SearchAndFilters = ({ options={residentLabels} isMulti closeMenuOnSelect={false} - placeholder="Select Resident" + placeholder="Select Residents" onChange={handleResidentsChange} styles={selectStyle} /> @@ -228,27 +232,34 @@ const SearchAndFilters = ({ options={userLabels} isMulti closeMenuOnSelect={false} - placeholder="Select Employee" + placeholder="Select Employees" onChange={handleEmployeesChange} styles={selectStyle} /> - + Date - + + + {startDate && + + setStartDate(undefined)} aria-label="clear" variant="icon" icon={}/> + + } + - + + + {endDate && + + setEndDate(undefined)} aria-label="clear" variant="icon" icon={}/> + + } + - - - diff --git a/frontend/src/components/pages/ResidentDirectory/Filters.tsx b/frontend/src/components/pages/ResidentDirectory/Filters.tsx new file mode 100644 index 00000000..2be42965 --- /dev/null +++ b/frontend/src/components/pages/ResidentDirectory/Filters.tsx @@ -0,0 +1,245 @@ +import React, { useEffect, useState } from "react"; +import { + Box, + Grid, + GridItem, + Button, + FormControl, + FormLabel, + Text, + InputGroup, + InputRightElement, + IconButton, + Flex, +} from "@chakra-ui/react"; +import { CloseIcon, SmallCloseIcon } from "@chakra-ui/icons"; +import { Card } from "react-bootstrap"; +import Select, { MultiValue, SingleValue } from "react-select"; +import { SingleDatepicker } from "chakra-dayzed-datepicker"; +import selectStyle from "../../../theme/forms/selectStyles"; +import { singleDatePickerStyle } from "../../../theme/forms/datePickerStyles"; +import { Resident, ResidentStatus, StatusLabel } from "../../../types/ResidentTypes"; +import ResidentAPIClient from "../../../APIClients/ResidentAPIClient"; +import CreateToast from "../../common/Toasts"; +import { SelectLabel } from "../../../types/SharedTypes"; + +type Props = { + residentSelections: SelectLabel[]; + buildingSelections: SelectLabel[]; + statusSelections: StatusLabel[]; + buildingOptions: SelectLabel[]; + startDate: Date | undefined; + endDate: Date | undefined; + setResidentSelections: React.Dispatch>; + setBuildingSelections: React.Dispatch>; + setStatusSelections: React.Dispatch>; + setStartDate: React.Dispatch>; + setEndDate: React.Dispatch>; +}; + +const Filters = ({ + residentSelections, + buildingSelections, + statusSelections, + buildingOptions, + startDate, + endDate, + setResidentSelections, + setBuildingSelections, + setStatusSelections, + setStartDate, + setEndDate, +}: Props): React.ReactElement => { + + const statusOptions: StatusLabel[] = [{label: ResidentStatus.CURRENT, value: ResidentStatus.CURRENT}, + {label: ResidentStatus.PAST, value: ResidentStatus.PAST}, + {label: ResidentStatus.FUTURE, value: ResidentStatus.FUTURE} + ] + + const [residentOptions, setResidentOptions] = useState(); + const dateChangeToast = CreateToast(); + + const getResidentOptions = async () => { + const data = await ResidentAPIClient.getResidents({ returnAll: true }); + const residentsData = data?.residents; + if (residentsData) { + const residentOpts = residentsData.map((resident: Resident) => { + return { + label: `${resident.residentId}`, + value: resident.id, + } as SelectLabel; + }); + setResidentOptions(residentOpts); + } + }; + + const handleBuildingsChange = ( + selectedBuildings: MultiValue, + ) => { + const mutableSelectedBuildings: SelectLabel[] = Array.from( + selectedBuildings, + ); + setBuildingSelections(mutableSelectedBuildings); + }; + + const handleResidentsChange = ( + selectedResidents: MultiValue, + ) => { + const mutableSelectedResidents: SelectLabel[] = Array.from( + selectedResidents, + ); + setResidentSelections(mutableSelectedResidents); + console.log(residentSelections) + }; + + const handleStatusesChange = ( + selectedStatuses: MultiValue, + ) => { + const mutableSelectedStatuses: StatusLabel[] = Array.from( + selectedStatuses, + ); + setStatusSelections(mutableSelectedStatuses); + }; + + const handleStartDateChange = (newStartDate: Date) => { + if (endDate && newStartDate > endDate) { + dateChangeToast( + "Invalid Date", + "The start date must be before the end date.", + "error", + ); + return; + } + setStartDate(newStartDate); + }; + + const handleEndDateChange = (newEndDate: Date) => { + if (startDate && startDate > newEndDate) { + dateChangeToast( + "Invalid Date", + "The end date must be after the start date.", + "error", + ); + return; + } + setEndDate(newEndDate); + }; + + const handleClearAll = () => { + setResidentSelections([]); + setBuildingSelections([]); + setEndDate(undefined); + setStartDate(undefined); + }; + + useEffect(() => { + getResidentOptions(); + }, []); + + return ( + + + + FILTER BY + + + + + Residents{" "} + + + + Status +