diff --git a/backend/app/rest/residents_routes.py b/backend/app/rest/residents_routes.py index 9f8ffff1..4ec64888 100644 --- a/backend/app/rest/residents_routes.py +++ b/backend/app/rest/residents_routes.py @@ -137,6 +137,12 @@ def get_residents(): except: filters = None + filters = None + try: + filters = json.loads(request.args.get("filters")) + except: + pass + results_per_page = 10 try: results_per_page = int(request.args.get("results_per_page")) @@ -157,10 +163,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/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/backend/app/services/implementations/residents_service.py b/backend/app/services/implementations/residents_service.py index da0d1181..a5b9964a 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") @@ -58,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) @@ -109,6 +180,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 +190,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,76 +198,42 @@ 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() - ) - 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")) + current_date = datetime.now(timezone('US/Eastern')).strftime('%Y-%m-%d') - 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( - 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() + residents_results = ( + Residents.query.join( + Buildings, Buildings.id == Residents.building_id ) + .with_entities(Residents, Buildings.name.label("building")) + ) + if filters: + 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) + + 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 - 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/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/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 50a101d4..b4793057 100644 --- a/frontend/src/components/pages/HomePage/HomePage.tsx +++ b/frontend/src/components/pages/HomePage/HomePage.tsx @@ -6,13 +6,10 @@ import NavigationBar from "../../common/NavigationBar"; import CreateLog from "../../forms/CreateLog"; import { LogRecord } from "../../../types/LogRecordTypes"; import LogRecordsTable from "./LogRecordsTable"; -import SearchAndFilters from "./SearchAndFilters"; +import HomePageFilters from "./HomePageFilters"; 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 @@ -68,9 +65,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 +102,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, @@ -170,7 +167,7 @@ const HomePage = (): React.ReactElement => { - >; - 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>; }; -const SearchAndFilters = ({ +const HomePageFilters = ({ residents, employees, startDate, @@ -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={}/> + + } + - - - @@ -360,4 +373,4 @@ const SearchAndFilters = ({ ); }; -export default SearchAndFilters; +export default HomePageFilters; diff --git a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx index b18690c7..457faf8d 100644 --- a/frontend/src/components/pages/HomePage/LogRecordsTable.tsx +++ b/frontend/src/components/pages/HomePage/LogRecordsTable.tsx @@ -31,11 +31,8 @@ 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"; +import { SelectLabel } from "../../../types/SharedTypes"; type Props = { logRecords: LogRecord[]; @@ -84,7 +81,7 @@ const LogRecordsTable = ({ const [showAlert, setShowAlert] = useState(false); - const [buildingOptions, setBuildingOptions] = useState([]); + const [buildingOptions, setBuildingOptions] = useState([]); // Menu states const [deleteOpenMap, setDeleteOpenMap] = useState<{ @@ -95,9 +92,9 @@ const LogRecordsTable = ({ ); // Dropdown option states - const [employeeOptions, setEmployeeOptions] = useState([]); - const [residentOptions, setResidentOptions] = useState([]); - const [tagOptions, setTagOptions] = useState([]); + const [employeeOptions, setEmployeeOptions] = useState([]); + const [residentOptions, setResidentOptions] = useState([]); + const [tagOptions, setTagOptions] = useState([]); // Handle delete confirmation toggle const handleDeleteToggle = (logId: number) => { @@ -123,7 +120,7 @@ const LogRecordsTable = ({ if (residentsData && residentsData.residents.length !== 0) { // TODO: Remove the type assertions here - const residentLabels: UserLabel[] = residentsData.residents.map((r) => ({ + const residentLabels: SelectLabel[] = residentsData.residents.map((r) => ({ label: r.residentId!, value: r.id!, })); @@ -133,7 +130,7 @@ const LogRecordsTable = ({ 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); @@ -141,7 +138,7 @@ const LogRecordsTable = ({ 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, @@ -152,7 +149,7 @@ const LogRecordsTable = ({ const tagsData = await TagAPIClient.getTags(); if (tagsData && tagsData.tags.length !== 0) { - const tagLabels: TagLabel[] = tagsData.tags.map((tag) => ({ + const tagLabels: SelectLabel[] = tagsData.tags.map((tag) => ({ label: tag.name, value: tag.tagId, })); diff --git a/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx b/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx index 88c3f607..398cafef 100644 --- a/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx +++ b/frontend/src/components/pages/ResidentDirectory/ResidentDirectory.tsx @@ -2,30 +2,41 @@ import React, { useState, useRef, useEffect } from "react"; import { Box, Flex, Spacer, Spinner, Text } from "@chakra-ui/react"; import ResidentDirectoryTable from "./ResidentDirectoryTable"; import NavigationBar from "../../common/NavigationBar"; -import { Resident } from "../../../types/ResidentTypes"; -import { BuildingLabel } from "../../../types/BuildingTypes"; +import { Resident, StatusLabel } from "../../../types/ResidentTypes"; import ResidentAPIClient from "../../../APIClients/ResidentAPIClient"; import BuildingAPIClient from "../../../APIClients/BuildingAPIClient"; import Pagination from "../../common/Pagination"; import CreateResident from "../../forms/CreateResident"; +import ResidentDirectoryFilters from "./ResidentDirectoryFilters" +import { SelectLabel } from "../../../types/SharedTypes"; +import { convertToString } from "../../../helper/dateHelpers"; const ResidentDirectory = (): React.ReactElement => { - const [buildingOptions, setBuildingOptions] = useState([]); const [residents, setResidents] = useState([]); + + // Pagination const [numResidents, setNumResidents] = useState(0); const [resultsPerPage, setResultsPerPage] = useState(25); const [pageNum, setPageNum] = useState(1); const [userPageNum, setUserPageNum] = useState(pageNum); - const [tableLoaded, setTableLoaded] = useState(false) - const tableRef = useRef(null); + // Options + const [buildingOptions, setBuildingOptions] = useState([]); + + // Filter state + const [residentSelections, setResidentSelections] = useState([]); + const [buildingSelections, setBuildingSelections] = useState([]); + const [statusSelections, setStatusSelections] = useState([]); + const [startDate, setStartDate] = useState(); + const [endDate, setEndDate] = useState(); + const getBuildingOptions = async () => { const data = await BuildingAPIClient.getBuildings(); if (data) { - const buildingLabels: BuildingLabel[] = data.buildings.map( + const buildingLabels: SelectLabel[] = data.buildings.map( (building) => ({ label: building.name!, value: building.id! }), ); setBuildingOptions(buildingLabels); @@ -33,11 +44,25 @@ const ResidentDirectory = (): React.ReactElement => { }; const getResidents = async (pageNumber: number) => { + + const residentIds = residentSelections.length > 0 ? residentSelections.map((resident) => resident.value) : undefined; + const buildingIds = buildingSelections.length > 0 ? buildingSelections.map((building) => building.value) : undefined; + const statuses = statusSelections.length > 0 ? statusSelections.map((employee) => employee.value) : undefined; + + let dateRange; + if (startDate || endDate) { + dateRange = [startDate ? convertToString(startDate) : null, endDate ? convertToString(endDate) : null] + } + setTableLoaded(false) const data = await ResidentAPIClient.getResidents({ returnAll: false, pageNumber, resultsPerPage, + residents: residentIds, + buildings: buildingIds, + statuses, + dateRange }); // Reset table scroll @@ -55,18 +80,40 @@ const ResidentDirectory = (): React.ReactElement => { }; const countResidents = async () => { - const data = await ResidentAPIClient.countResidents(); + + const residentIds = residentSelections.length > 0 ? residentSelections.map((resident) => resident.value) : undefined; + const buildingIds = buildingSelections.length > 0 ? buildingSelections.map((building) => building.value) : undefined; + const statuses = statusSelections.length > 0 ? statusSelections.map((employee) => employee.value) : undefined; + + let dateRange; + if (startDate || endDate) { + dateRange = [startDate ? convertToString(startDate) : null, endDate ? convertToString(endDate) : null] + } + + const data = await ResidentAPIClient.countResidents({ + residents: residentIds, + buildings: buildingIds, + statuses, + dateRange + }); setNumResidents(data ? data.numResults : 0); }; useEffect(() => { setUserPageNum(1); getResidents(1); - getBuildingOptions(); - }, [resultsPerPage]); + countResidents(); + }, [resultsPerPage, + residentSelections, + buildingSelections, + statusSelections, + startDate, + endDate + ] + ); useEffect(() => { - countResidents(); + getBuildingOptions(); }, []); return ( @@ -90,6 +137,20 @@ const ResidentDirectory = (): React.ReactElement => { /> + + {!tableLoaded ? ( >; + setBuildingSelections: React.Dispatch>; + setStatusSelections: React.Dispatch>; + setStartDate: React.Dispatch>; + setEndDate: React.Dispatch>; +}; + +const ResidentDirectoryFilters = ({ + 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 +