From c44fa4d446bc48c64b9b19a3262a9a2cc7c0d98d Mon Sep 17 00:00:00 2001 From: Shahan Neda Date: Fri, 17 May 2024 01:47:46 -0400 Subject: [PATCH] Donor Onsite Contacts (#127) --- README.md | 11 + backend/.vscode/settings.json | 11 + backend/__init__.py | 0 backend/app/config.py | 1 + backend/app/graphql/meal_request.py | 24 +- backend/app/models/__init__.py | 10 +- backend/app/models/meal_request.py | 57 +- backend/app/resources/meal_request_dto.py | 12 +- backend/app/resources/validate_utils.py | 14 + .../implementations/meal_request_service.py | 69 +- .../onboarding_request_service.py | 12 + .../implementations/onsite_contact_service.py | 15 +- .../interfaces/meal_request_service.py | 5 +- backend/pytest.ini | 2 + backend/tests/graphql/mock_test_data.py | 4 +- .../tests/graphql/test_all_user_mutations.py | 3 + backend/tests/graphql/test_meal_request.py | 60 +- .../graphql/test_reminder_email_service.py | 3 +- .../asp/requests/CreateMealRequest.tsx | 8 +- .../asp/requests/SchedulingFormMealInfo.tsx | 24 +- .../SchedulingFormReviewAndSubmit.tsx | 13 +- frontend/src/components/auth/Join.tsx | 7 +- ...ffSection.tsx => OnsiteContactSection.tsx} | 12 +- .../donation_form/MealDonationForm.tsx | 10 +- .../MealDonationFormContactInfo.tsx | 20 +- .../MealDonationFormReviewAndSubmit.tsx | 11 +- .../components/mealrequest/ASPListView.tsx | 103 ++- .../mealrequest/MealDonorListView.tsx | 248 +++--- frontend/src/pages/EditMealRequestForm.tsx | 172 +++-- frontend/src/pages/MealDonorCalendar.tsx | 2 +- frontend/src/pages/MealRequestForm.tsx | 717 +++++++++--------- frontend/src/pages/Settings.tsx | 42 +- frontend/src/pages/UpcomingPage.tsx | 190 ++--- frontend/src/types/MealRequestTypes.ts | 9 +- frontend/src/utils/useGetOnsiteContacts.ts | 5 +- 35 files changed, 1062 insertions(+), 844 deletions(-) create mode 100644 backend/.vscode/settings.json create mode 100644 backend/__init__.py create mode 100644 backend/pytest.ini rename frontend/src/components/common/{OnsiteStaffSection.tsx => OnsiteContactSection.tsx} (98%) diff --git a/README.md b/README.md index 8d1ee1aa..bd0109c7 100644 --- a/README.md +++ b/README.md @@ -151,6 +151,17 @@ cd frontend make test ``` +### VSCode Python Test runner and debugger + +Use VSCode's python test runner, which makes life MUCH Better. + +- 1. Make sure you have the docker extension, and the `Dev Containers` extension installed. +- 2. Run `docker compose up backend --build` to start up backend container. +- 3. Use the docker extension, right click on the backend container, and click `Attach Visual Studio Code`. +- 4. Inside the container, make sure the Python Extension and the Python Debugger extension is installed. +- 5. Click the tests tab. Use the buttons to run or debug tests. +- 6. If you get a "Test not found" or similar error, the problem could be the wrong python binary is being used. In the bottom right, click on the python version, and try switching to different python binaries. + ## Version Control Guide ### Branching diff --git a/backend/.vscode/settings.json b/backend/.vscode/settings.json new file mode 100644 index 00000000..cefbc899 --- /dev/null +++ b/backend/.vscode/settings.json @@ -0,0 +1,11 @@ +{ + "terminal.integrated.env.linux": { + "PYTHONPATH": "${workspaceFolder}" + }, + "terminal.integrated.env.osx": { + "PYTHONPATH": "${workspaceFolder}" + }, + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true + +} \ No newline at end of file diff --git a/backend/__init__.py b/backend/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/app/config.py b/backend/app/config.py index c954a1b9..f3a7546b 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -9,6 +9,7 @@ class Config(object): # put any configurations here that are common across all environments # list of available configs: https://flask.palletsprojects.com/en/1.1.x/config/ MONGODB_URL = os.getenv("MG_DATABASE_URL") + MONGODB_DB_NAME = os.getenv("MG_DB_NAME") class DevelopmentConfig(Config): diff --git a/backend/app/graphql/meal_request.py b/backend/app/graphql/meal_request.py index 496a1633..d571d592 100644 --- a/backend/app/graphql/meal_request.py +++ b/backend/app/graphql/meal_request.py @@ -37,7 +37,7 @@ class CreateMealRequestResponse(graphene.ObjectType): drop_off_datetime = graphene.DateTime(required=True) status = graphene.Field(graphene.Enum.from_enum(MealStatus), required=True) meal_info = graphene.Field(MealInfoResponse, required=True) - onsite_staff = graphene.List(OnsiteContact) + onsite_contacts = graphene.List(OnsiteContact) class DonationInfo(graphene.ObjectType): @@ -45,6 +45,7 @@ class DonationInfo(graphene.ObjectType): commitment_date = graphene.DateTime() meal_description = graphene.String() additional_info = graphene.String() + donor_onsite_contacts = graphene.List(OnsiteContact) class MealRequestResponse(graphene.ObjectType): @@ -54,7 +55,7 @@ class MealRequestResponse(graphene.ObjectType): drop_off_datetime = graphene.DateTime() drop_off_location = graphene.String() meal_info = graphene.Field(MealInfoResponse) - onsite_staff = graphene.List(OnsiteContact) + onsite_contacts = graphene.List(OnsiteContact) date_created = graphene.DateTime() date_updated = graphene.DateTime() delivery_instructions = graphene.String() @@ -72,7 +73,7 @@ class Arguments: drop_off_time = graphene.Time(required=True) drop_off_location = graphene.String(required=True) delivery_instructions = graphene.String(default_value=None) - onsite_staff = graphene.List(graphene.String, default_value=[]) + onsite_contacts = graphene.List(graphene.String, default_value=[]) # return values meal_requests = graphene.List(CreateMealRequestResponse) @@ -86,7 +87,7 @@ def mutate( drop_off_time, drop_off_location, delivery_instructions, - onsite_staff, + onsite_contacts, ): result = services["meal_request_service"].create_meal_request( requestor_id=requestor_id, @@ -95,7 +96,7 @@ def mutate( drop_off_time=drop_off_time, drop_off_location=drop_off_location, delivery_instructions=delivery_instructions, - onsite_staff=onsite_staff, + onsite_contacts=onsite_contacts, ) return CreateMealRequests(meal_requests=result) @@ -109,7 +110,7 @@ class Arguments: meal_info = MealTypeInput() drop_off_location = graphene.String() delivery_instructions = graphene.String() - onsite_staff = graphene.List(graphene.String) + onsite_contacts = graphene.List(graphene.String) # return values meal_request = graphene.Field(MealRequestResponse) @@ -123,7 +124,7 @@ def mutate( meal_info=None, drop_off_location=None, delivery_instructions=None, - onsite_staff=None, + onsite_contacts=None, ): result = services["meal_request_service"].update_meal_request( requestor_id=requestor_id, @@ -131,7 +132,7 @@ def mutate( drop_off_datetime=drop_off_datetime, drop_off_location=drop_off_location, delivery_instructions=delivery_instructions, - onsite_staff=onsite_staff, + onsite_contacts=onsite_contacts, meal_request_id=meal_request_id, ) @@ -144,6 +145,7 @@ class Arguments: meal_request_ids = graphene.List(graphene.ID, required=True) meal_description = graphene.String(required=True) additional_info = graphene.String(default_value=None) + donor_onsite_contacts = graphene.List(graphene.ID, required=True) meal_requests = graphene.List(MealRequestResponse) @@ -154,12 +156,14 @@ def mutate( meal_request_ids, meal_description, additional_info=None, + donor_onsite_contacts=[], ): result = services["meal_request_service"].commit_to_meal_request( donor_id=requestor, meal_request_ids=meal_request_ids, meal_description=meal_description, additional_info=additional_info, + donor_onsite_contacts=donor_onsite_contacts, ) return CommitToMealRequest(meal_requests=result) @@ -316,7 +320,7 @@ def resolve_getMealRequestsByRequestorId( drop_off_datetime=meal_request_dto.drop_off_datetime, drop_off_location=meal_request_dto.drop_off_location, meal_info=meal_request_dto.meal_info, - onsite_staff=meal_request_dto.onsite_staff, + onsite_contacts=meal_request_dto.onsite_contacts, date_created=meal_request_dto.date_created, date_updated=meal_request_dto.date_updated, delivery_instructions=meal_request_dto.delivery_instructions, @@ -370,7 +374,7 @@ def resolve_getMealRequestsByDonorId( drop_off_datetime=meal_request_dto.drop_off_datetime, drop_off_location=meal_request_dto.drop_off_location, meal_info=meal_request_dto.meal_info, - onsite_staff=meal_request_dto.onsite_staff, + onsite_contacts=meal_request_dto.onsite_contacts, date_created=meal_request_dto.date_created, date_updated=meal_request_dto.date_updated, delivery_instructions=meal_request_dto.delivery_instructions, diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index f2604520..6b9be401 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -11,5 +11,11 @@ def init_app(app): if "USE_MONGOMOCK_CLIENT" in app.config else pymongo.MongoClient ) - if "MONGODB_URL" in app.config: - connect(host=app.config["MONGODB_URL"], mongo_client_class=mongo_client) + if "MONGODB_URL" in app.config and "MONGODB_DB_NAME" in app.config: + connect( + host=app.config["MONGODB_URL"], + mongo_client_class=mongo_client, + db=app.config["MONGODB_DB_NAME"], + ) + else: + raise Exception("MG_DATABASE_URL and MG_DB_NAME must be set in the env file.") diff --git a/backend/app/models/meal_request.py b/backend/app/models/meal_request.py index 2deaa118..c44936a3 100644 --- a/backend/app/models/meal_request.py +++ b/backend/app/models/meal_request.py @@ -3,6 +3,7 @@ from enum import Enum from app.models.onsite_contact import OnsiteContact +from app.resources.meal_request_dto import MealRequestDTO from .user import User @@ -30,6 +31,10 @@ class DonationInfo(mg.EmbeddedDocument): commitment_date = mg.DateTimeField(required=True) meal_description = mg.StringField(required=True) additional_info = mg.StringField(default=None) + # https://docs.mongoengine.org/apireference.html#mongoengine.fields.ReferenceField + donor_onsite_contacts = mg.ListField( + mg.ReferenceField(OnsiteContact, required=True) + ) # 4 = PULL class MealRequest(mg.Document): @@ -44,7 +49,7 @@ class MealRequest(mg.Document): meal_info = mg.EmbeddedDocumentField(MealInfo, required=True) # https://docs.mongoengine.org/apireference.html#mongoengine.fields.ReferenceField - onsite_staff = mg.ListField( + onsite_contacts = mg.ListField( mg.ReferenceField(OnsiteContact, required=True, reverse_delete_rule=4) ) # 4 = PULL date_created = mg.DateTimeField(required=True, default=datetime.utcnow) @@ -53,15 +58,26 @@ class MealRequest(mg.Document): donation_info = mg.EmbeddedDocumentField(DonationInfo, default=None) def validate_onsite_contacts(self): - if self.onsite_staff: + if self.onsite_contacts: # Try to fetch the referenced document to ensure it exists, will throw an error if it doesn't - for contact in self.onsite_staff: + for contact in self.onsite_contacts: contact = OnsiteContact.objects(id=contact.id).first() if not contact or contact.organization_id != self.requestor.id: raise Exception( f"onsite contact {contact.id} not found or not associated with the requestor's organization" ) + if self.donation_info: + for contact in self.donation_info.donor_onsite_contacts: + contact = OnsiteContact.objects(id=contact.id).first() + if ( + not contact + or contact.organization_id != self.donation_info.donor.id + ): + raise Exception( + f"onsite contact {contact.id} not found or not associated with the donor organization" + ) + def to_serializable_dict(self): """ Returns a dict representation of the document that is JSON serializable @@ -71,11 +87,42 @@ def to_serializable_dict(self): meal_request_dict = self.to_mongo().to_dict() id = meal_request_dict.pop("_id", None) meal_request_dict["id"] = str(id) - contacts = [contact.to_mongo().to_dict() for contact in self.onsite_staff] + + contacts = [contact.to_mongo().to_dict() for contact in self.onsite_contacts] for contact in contacts: id = contact.pop("_id") contact["id"] = id - meal_request_dict["onsite_staff"] = contacts + meal_request_dict["onsite_contacts"] = contacts + + if self.donation_info and self.donation_info.donor_onsite_contacts: + contacts = [ + contact.to_mongo().to_dict() + for contact in self.donation_info.donor_onsite_contacts + ] + for contact in contacts: + id = contact.pop("_id") + contact["id"] = id + meal_request_dict["donation_info"]["donor_onsite_contacts"] = contacts + return meal_request_dict + def to_dto(self): + dict = self.to_serializable_dict() + requestor = User.objects(id=dict["requestor"]).first() + + if not requestor: + raise Exception(f'requestor "{self.requestor.id}" not found') + + requestor_dict = requestor.to_serializable_dict() + dict["requestor"] = requestor_dict + + if "donation_info" in dict: + donor_id = dict["donation_info"]["donor"] + donor = User.objects(id=donor_id).first() + if not donor: + raise Exception(f'donor "{donor_id}" not found') + dict["donation_info"]["donor"] = donor.to_serializable_dict() + + return MealRequestDTO(**dict) + meta = {"collection": "meal_requests"} diff --git a/backend/app/resources/meal_request_dto.py b/backend/app/resources/meal_request_dto.py index 0aed09f4..bb4dc8de 100644 --- a/backend/app/resources/meal_request_dto.py +++ b/backend/app/resources/meal_request_dto.py @@ -1,6 +1,6 @@ import datetime -from ..models.meal_request import MEAL_STATUSES_STRINGS + from .validate_utils import ( validate_contact, validate_donation_info, @@ -18,7 +18,7 @@ def __init__( drop_off_datetime, drop_off_location, meal_info, - onsite_staff, + onsite_contacts, date_created, date_updated, delivery_instructions=None, @@ -30,7 +30,7 @@ def __init__( self.drop_off_datetime = drop_off_datetime self.drop_off_location = drop_off_location self.meal_info = meal_info - self.onsite_staff = onsite_staff + self.onsite_contacts = onsite_contacts self.date_created = date_created self.date_updated = date_updated self.delivery_instructions = delivery_instructions @@ -42,6 +42,8 @@ def __init__( raise Exception(error_message) def validate(self): + from app.models.meal_request import MEAL_STATUSES_STRINGS + error_list = [] if type(self.id) is not str: @@ -71,8 +73,8 @@ def validate(self): validate_meal_info(self.meal_info, error_list) - for i, staff in enumerate(self.onsite_staff): - validate_contact(staff, f"index {i} of onsite_staff", error_list) + for i, staff in enumerate(self.onsite_contacts): + validate_contact(staff, f"index {i} of onsite_contacts", error_list) if type(self.date_created) is not datetime.datetime: error_list.append("The date_created supplied is not a datetime object.") diff --git a/backend/app/resources/validate_utils.py b/backend/app/resources/validate_utils.py index 91aa9d59..f59e121d 100644 --- a/backend/app/resources/validate_utils.py +++ b/backend/app/resources/validate_utils.py @@ -34,6 +34,12 @@ def validate_role_info(role, role_info, role_info_str, error_list): if role == UserInfoRole.ASP.value: for field in asp_info_fields: role_info = role_info["asp_info"] + if not role_info: + error_list.append( + f"The {role_info_str} supplied does not have asp_info even though user is an ASP!." + ) + return + if field not in role_info: error_list.append( f'The {role_info_str} supplied does not have field "{field}".' @@ -162,6 +168,7 @@ def validate_donation_info(donation_info, error_list): "commitment_date", "meal_description", "additional_info", + "donor_onsite_contacts", ] if not isinstance(donation_info, dict): @@ -188,3 +195,10 @@ def validate_donation_info(donation_info, error_list): error_list.append("The meal_description supplied is not a string.") elif key == "additional_info" and type(val) is not str: error_list.append("The additional_info supplied is not a string.") + elif key == "donor_onsite_contacts" and type(val) is not list: + error_list.append("The donor_onsite_contacts supplied is not list.") + for s in val: + if type(s) is not str: + error_list.append( + "The donor_onsite_contacts supplied is not list of strings." + ) diff --git a/backend/app/services/implementations/meal_request_service.py b/backend/app/services/implementations/meal_request_service.py index 52d84e18..ccec3b22 100644 --- a/backend/app/services/implementations/meal_request_service.py +++ b/backend/app/services/implementations/meal_request_service.py @@ -25,7 +25,7 @@ def create_meal_request( drop_off_time, drop_off_location, delivery_instructions, - onsite_staff: List[str], + onsite_contacts: List[str], ): try: # Create MealRequests @@ -41,9 +41,10 @@ def create_meal_request( drop_off_datetime=datetime.combine(request_date, drop_off_time), drop_off_location=drop_off_location, delivery_instructions=delivery_instructions, - onsite_staff=onsite_staff, + onsite_contacts=onsite_contacts, ) new_meal_request.validate_onsite_contacts() + new_meal_request.save() meal_requests.append(new_meal_request.to_serializable_dict()) except Exception as error: @@ -58,7 +59,7 @@ def update_meal_request( drop_off_datetime, drop_off_location, delivery_instructions, - onsite_staff, + onsite_contacts, meal_request_id, ): original_meal_request: MealRequest = MealRequest.objects( @@ -85,15 +86,10 @@ def update_meal_request( if delivery_instructions is not None: original_meal_request.delivery_instructions = delivery_instructions - if onsite_staff is not None: - original_meal_request.onsite_staff = onsite_staff - - requestor = original_meal_request.requestor + if onsite_contacts is not None: + original_meal_request.onsite_contacts = onsite_contacts - # Does validation, - meal_request_dto = self.convert_meal_request_to_dto( - original_meal_request, requestor - ) + meal_request_dto = original_meal_request.to_dto() original_meal_request.validate_onsite_contacts() original_meal_request.save() @@ -106,6 +102,7 @@ def commit_to_meal_request( meal_request_ids: [str], meal_description: str, additional_info: str, + donor_onsite_contacts: List[str], ) -> List[MealRequestDTO]: try: donor = User.objects(id=donor_id).first() @@ -141,16 +138,13 @@ def commit_to_meal_request( commitment_date=datetime.utcnow(), meal_description=meal_description, additional_info=additional_info, + donor_onsite_contacts=donor_onsite_contacts, ) # Change the meal request's status to "Upcoming" meal_request.status = MealStatus.UPCOMING.value - meal_request_dtos.append( - self.convert_meal_request_to_dto( - meal_request, meal_request.requestor - ) - ) + meal_request_dtos.append(meal_request.to_dto()) meal_request.save() @@ -173,9 +167,7 @@ def cancel_donation(self, meal_request_id: str) -> MealRequestDTO: meal_request.donation_info = None - meal_request_dto = self.convert_meal_request_to_dto( - meal_request, meal_request.requestor - ) + meal_request_dto = meal_request.to_dto() # does validation meal_request.save() @@ -187,15 +179,13 @@ def cancel_donation(self, meal_request_id: str) -> MealRequestDTO: def delete_meal_request(self, meal_request_id: str) -> MealRequestDTO: try: - meal_request = MealRequest.objects(id=meal_request_id).first() + meal_request: MealRequest = MealRequest.objects(id=meal_request_id).first() if not meal_request: raise Exception(f'Meal request "{meal_request_id}" not found') meal_request.delete() - meal_request_dto = self.convert_meal_request_to_dto( - meal_request, meal_request.requestor - ) + meal_request_dto = meal_request.to_dto() return meal_request_dto @@ -203,21 +193,6 @@ def delete_meal_request(self, meal_request_id: str) -> MealRequestDTO: self.logger.error(str(error)) raise error - def convert_meal_request_to_dto( - self, request: MealRequest, requestor: User - ) -> MealRequestDTO: - request_dict = request.to_serializable_dict() - request_dict["requestor"] = requestor.to_serializable_dict() - - if "donation_info" in request_dict: - donor_id = request_dict["donation_info"]["donor"] - donor = User.objects(id=donor_id).first() - if not donor: - raise Exception(f'donor "{donor_id}" not found') - request_dict["donation_info"]["donor"] = donor.to_serializable_dict() - - return MealRequestDTO(**request_dict) - def get_meal_requests_by_requestor_id( self, requestor_id, @@ -258,9 +233,7 @@ def get_meal_requests_by_requestor_id( meal_request_dtos = [] for request in requests: - meal_request_dtos.append( - self.convert_meal_request_to_dto(request, requestor) - ) + meal_request_dtos.append(request.to_dto()) return meal_request_dtos @@ -308,9 +281,7 @@ def get_meal_requests_by_donor_id( meal_request_dtos = [] for request in requests: - meal_request_dtos.append( - self.convert_meal_request_to_dto(request, donor) - ) + meal_request_dtos.append(request.to_dto()) return meal_request_dtos @@ -320,18 +291,12 @@ def get_meal_requests_by_donor_id( def get_meal_request_by_id(self, id: str) -> MealRequestDTO: meal_request = MealRequest.objects(id=id).first() - meal_request_dto = self.convert_meal_request_to_dto( - meal_request, meal_request.requestor - ) - return meal_request_dto + return meal_request.to_dto() def get_meal_requests_by_ids(self, ids: str) -> List[MealRequestDTO]: meal_requests = MealRequest.objects(id__in=ids).all() - meal_request_dtos = [ - self.convert_meal_request_to_dto(meal_request, meal_request.requestor) - for meal_request in meal_requests - ] + meal_request_dtos = [meal_request.to_dto() for meal_request in meal_requests] return meal_request_dtos diff --git a/backend/app/services/implementations/onboarding_request_service.py b/backend/app/services/implementations/onboarding_request_service.py index 4fd39a54..7e3311ad 100644 --- a/backend/app/services/implementations/onboarding_request_service.py +++ b/backend/app/services/implementations/onboarding_request_service.py @@ -1,3 +1,4 @@ +from app.resources.validate_utils import validate_userinfo from ...utilities.location_to_coordinates import getGeocodeFromAddress from ...services.implementations.auth_service import AuthService from ..interfaces.onboarding_request_service import IOnboardingRequestService @@ -24,6 +25,17 @@ def __init__(self, logger, email_service): def create_onboarding_request(self, userInfo: UserInfo): try: + # Users will start out as active + userInfo["active"] = True + userInfo.active = True + + validation_errors = [] + validate_userinfo(userInfo, validation_errors) + if validation_errors: + raise Exception( + f"Error validating user info. Reason = {validation_errors}" + ) + # Create initial UserInfo object user_info = UserInfo( email=userInfo.email, diff --git a/backend/app/services/implementations/onsite_contact_service.py b/backend/app/services/implementations/onsite_contact_service.py index dc981b4d..c9186940 100644 --- a/backend/app/services/implementations/onsite_contact_service.py +++ b/backend/app/services/implementations/onsite_contact_service.py @@ -16,9 +16,9 @@ def get_onsite_contact_by_id( self, id: str, ): - onsite_staff = OnsiteContact.objects(id=id).first() - if onsite_staff: - return onsite_staff.to_dto() + onsite_contacts = OnsiteContact.objects(id=id).first() + if onsite_contacts: + return onsite_contacts.to_dto() else: return None @@ -26,8 +26,8 @@ def get_onsite_contacts_for_user_by_id( self, user_id: str, ) -> List[OnsiteContactDTO]: - onsite_staff = OnsiteContact.objects(organization_id=user_id).all() - return [staff.to_dto() for staff in onsite_staff] + onsite_contacts = OnsiteContact.objects(organization_id=user_id).all() + return [staff.to_dto() for staff in onsite_contacts] def delete_onsite_contact_by_id( self, @@ -71,9 +71,12 @@ def create_onsite_contact( email=email, phone=phone, ) + new_contact.save() + # does validation + dto = new_contact.to_dto() - return new_contact.to_dto() + return dto except Exception as error: self.logger.error(str(error)) diff --git a/backend/app/services/interfaces/meal_request_service.py b/backend/app/services/interfaces/meal_request_service.py index 31e05fe0..4308ffce 100644 --- a/backend/app/services/interfaces/meal_request_service.py +++ b/backend/app/services/interfaces/meal_request_service.py @@ -18,7 +18,7 @@ def create_meal_request( drop_off_time, drop_off_location, delivery_instructions, - onsite_staff: List[str], + onsite_contacts: List[str], ): """Create a new MealRequest object and corresponding MealRequests @@ -38,7 +38,7 @@ def update_meal_request( drop_off_datetime, drop_off_location, delivery_instructions, - onsite_staff: List[str], + onsite_contacts: List[str], meal_request_id, ): pass @@ -50,6 +50,7 @@ def commit_to_meal_request( meal_request_ids: List[str], meal_description: str, additional_info: Union[str, None], + donor_onsite_contacts: List[str], ) -> List[MealRequestDTO]: pass diff --git a/backend/pytest.ini b/backend/pytest.ini new file mode 100644 index 00000000..de19c9f0 --- /dev/null +++ b/backend/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +testpaths = tests \ No newline at end of file diff --git a/backend/tests/graphql/mock_test_data.py b/backend/tests/graphql/mock_test_data.py index cd6eda35..f81edf0d 100644 --- a/backend/tests/graphql/mock_test_data.py +++ b/backend/tests/graphql/mock_test_data.py @@ -136,7 +136,7 @@ # {"name": "ghi", "phone": "135-792-4680", "email": "ghi@uwblueprint.org"}, # {"name": "Jack Doe", "phone": "777-888-999", "email": "com@domain.email"}, # ], - "active": False, + "active": True, } MOCK_INFO3_CAMEL = { @@ -159,7 +159,7 @@ # {"name": "ghi", "phone": "135-792-4680", "email": "ghi@uwblueprint.org"}, # {"name": "Jack Doe", "phone": "777-888-999", "email": "com@domain.email"}, # ], - "active": False, + "active": True, } MOCK_USER1_SNAKE = { diff --git a/backend/tests/graphql/test_all_user_mutations.py b/backend/tests/graphql/test_all_user_mutations.py index 20f6d4b3..46e1fd97 100644 --- a/backend/tests/graphql/test_all_user_mutations.py +++ b/backend/tests/graphql/test_all_user_mutations.py @@ -70,10 +70,13 @@ def test_update_user_by_id(user_setup, mocker): }}""" ) + assert update_to_user_4_info.errors is None + user_result4 = update_to_user_4_info.data["updateUserByID"]["user"] assert user_result4["id"] == str(user_1.id) MOCK_INFO4_CAMEL = deepcopy(MOCK_INFO3_CAMEL) MOCK_INFO4_CAMEL["email"] = "test4@organization.com" + MOCK_INFO4_CAMEL["active"] = False assert user_result4["info"] == MOCK_INFO4_CAMEL update_to_user_1_info = graphql_schema.execute( diff --git a/backend/tests/graphql/test_meal_request.py b/backend/tests/graphql/test_meal_request.py index 644cd64b..1c83624a 100644 --- a/backend/tests/graphql/test_meal_request.py +++ b/backend/tests/graphql/test_meal_request.py @@ -35,7 +35,7 @@ def test_create_meal_request(meal_request_setup, onsite_contact_setup): portions: 40, dietaryRestrictions: "7 gluten free, 7 no beef", }}, - onsiteStaff: ["{asp_onsite_contact.id}", "{asp_onsite_contact2.id}"], + onsiteContacts: ["{asp_onsite_contact.id}", "{asp_onsite_contact2.id}"], requestorId: "{str(asp.id)}", requestDates: [ "2023-06-01", @@ -51,7 +51,7 @@ def test_create_meal_request(meal_request_setup, onsite_contact_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ id name email @@ -86,7 +86,7 @@ def test_create_meal_request(meal_request_setup, onsite_contact_setup): ) created_onsite_contacts = result.data["createMealRequest"]["mealRequests"][0][ - "onsiteStaff" + "onsiteContacts" ] expected_onsite_contacts = ( [asp_onsite_contact, asp_onsite_contact2] @@ -121,7 +121,7 @@ def test_create_meal_request_fails_invalid_onsite_contact( portions: 40, dietaryRestrictions: "7 gluten free, 7 no beef", }}, - onsiteStaff: ["{asp_onsite_contact}, fdsfdja"], + onsiteContacts: ["{asp_onsite_contact}, fdsfdja"], requestorId: "{str(asp.id)}", requestDates: [ "2023-06-01", @@ -137,7 +137,7 @@ def test_create_meal_request_fails_invalid_onsite_contact( portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ id }} }} @@ -161,7 +161,8 @@ def test_commit_to_meal_request(meal_request_setup): requestor: "{str(donor.id)}", mealRequestIds: ["{str(meal_request.id)}"], mealDescription: "Pizza", - additionalInfo: "No nuts" + additionalInfo: "No nuts", + donorOnsiteContacts: [] ) {{ mealRequests {{ @@ -176,7 +177,7 @@ def test_commit_to_meal_request(meal_request_setup): portions dietaryRestrictions }} - onsiteStaff {{ + onsiteContacts {{ name email phone @@ -274,8 +275,11 @@ def test_commit_to_meal_request(meal_request_setup): # Only user's with role "Donor" should be able to commit # to meal requests, otherwise an error is thrown -def test_commit_to_meal_request_fails_for_non_donor(meal_request_setup): +def test_commit_to_meal_request_fails_for_non_donor( + meal_request_setup, onsite_contact_setup +): _, donor, meal_request = meal_request_setup + requestor, _, asp_onsite_contacts, donor_onsite_contact = onsite_contact_setup # All user info roles except for "Donor" INVALID_USERINFO_ROLES = [UserInfoRole.ADMIN.value, UserInfoRole.ASP.value] @@ -291,6 +295,7 @@ def test_commit_to_meal_request_fails_for_non_donor(meal_request_setup): mealRequestIds: ["{str(meal_request.id)}"], mealDescription: "Pizza", additionalInfo: "No nuts" + donorOnsiteContacts: [] ) {{ mealRequests {{ @@ -302,6 +307,10 @@ def test_commit_to_meal_request_fails_for_non_donor(meal_request_setup): result = graphql_schema.execute(mutation) assert result.errors is not None + assert ( + result.errors[0].message + == f'Unexpected error: user "{donor.id}" is not a donor' + ) # A donor can only commit to a meal request if the meal request's @@ -367,7 +376,7 @@ def test_update_meal_request(onsite_contact_setup, meal_request_setup): portions: {updatedMealInfo["portions"]}, dietaryRestrictions: "{updatedMealInfo["dietaryRestrictions"]}", }}, - onsiteStaff: ["{onsite_contact1.id}","{onsite_contact2.id}"] + onsiteContacts: ["{onsite_contact1.id}","{onsite_contact2.id}"] ) {{ mealRequest{{ @@ -379,7 +388,7 @@ def test_update_meal_request(onsite_contact_setup, meal_request_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ id name email @@ -406,7 +415,7 @@ def test_update_meal_request(onsite_contact_setup, meal_request_setup): assert updatedMealRequest["dropOffLocation"] == updatedDropOffLocation assert updatedMealRequest["deliveryInstructions"] == updatedDeliveryInstructions assert updatedMealRequest["mealInfo"] == updatedMealInfo - returned_onsite_contacts = updatedMealRequest["onsiteStaff"] + returned_onsite_contacts = updatedMealRequest["onsiteContacts"] compare_returned_onsite_contact(returned_onsite_contacts[0], onsite_contact1) compare_returned_onsite_contact(returned_onsite_contacts[1], onsite_contact2) @@ -426,7 +435,7 @@ def test_create_meal_request_failure(meal_request_setup): portions: 40, dietaryRestrictions: "7 gluten free, 7 no beef", }}, - onsiteStaff: [ + onsiteContacts: [ {{ name: "John Doe", email: "john.doe@example.com", @@ -482,7 +491,7 @@ def test_get_meal_request_by_requestor_id(meal_request_setup): portions dietaryRestrictions }}, - onsiteStaff {{ + onsiteContacts {{ name email phone @@ -530,7 +539,7 @@ def test_cancel_donation_as_admin(meal_request_setup, user_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ name email phone @@ -579,7 +588,7 @@ def test_cancel_donation_fails_if_no_donation(meal_request_setup, user_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ name email phone @@ -624,7 +633,7 @@ def test_cancel_donation_as_non_admin(meal_request_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ name email phone @@ -668,7 +677,7 @@ def test_delete_meal_request_as_admin(meal_request_setup, user_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ name email phone @@ -711,7 +720,7 @@ def test_delete_meal_request_as_asp(meal_request_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ name email phone @@ -756,7 +765,7 @@ def test_delete_meal_request_as_non_admin_fails_if_donor(meal_request_setup): portions dietaryRestrictions }} - onsiteStaff{{ + onsiteContacts{{ name email phone @@ -782,16 +791,19 @@ def test_delete_meal_request_as_non_admin_fails_if_donor(meal_request_setup): assert MealRequest.objects(id=meal_request.id).first() is not None -def test_get_meal_request_by_donor_id(meal_request_setup): +def test_get_meal_request_by_donor_id(meal_request_setup, onsite_contact_setup): _, donor, meal_request = meal_request_setup + asp, donor, asp_onsite_contact, donor_onsite_contact = onsite_contact_setup + commit = graphql_schema.execute( f"""mutation testCommitToMealRequest {{ commitToMealRequest( requestor: "{str(donor.id)}", mealRequestIds: ["{str(meal_request.id)}"], mealDescription: "Pizza", - additionalInfo: "No nuts" + additionalInfo: "No nuts", + donorOnsiteContacts: ["{str(donor_onsite_contact.id)}"], ) {{ mealRequests {{ @@ -817,7 +829,7 @@ def test_get_meal_request_by_donor_id(meal_request_setup): portions dietaryRestrictions }}, - onsiteStaff {{ + onsiteContacts {{ name email phone @@ -861,7 +873,7 @@ def test_get_meal_requests_by_ids(meal_request_setup): portions: 40, dietaryRestrictions: "7 gluten free, 7 no beef", }}, - onsiteStaff: [], + onsiteContacts: [], requestorId: "{str(asp.id)}", requestDates: [ "2023-06-01", @@ -898,7 +910,7 @@ def test_get_meal_requests_by_ids(meal_request_setup): portions dietaryRestrictions }}, - onsiteStaff {{ + onsiteContacts {{ name email phone diff --git a/backend/tests/graphql/test_reminder_email_service.py b/backend/tests/graphql/test_reminder_email_service.py index bda07fa7..58c43835 100644 --- a/backend/tests/graphql/test_reminder_email_service.py +++ b/backend/tests/graphql/test_reminder_email_service.py @@ -94,6 +94,7 @@ def commit_to_meal_request(donor, meal_request): mealRequestIds: ["{str(meal_request.id)}"], mealDescription: "Pizza", additionalInfo: "No nuts" + donorOnsiteContacts: [] ) {{ mealRequests {{ @@ -108,7 +109,7 @@ def commit_to_meal_request(donor, meal_request): portions dietaryRestrictions }} - onsiteStaff {{ + onsiteContacts {{ name email phone diff --git a/frontend/src/components/asp/requests/CreateMealRequest.tsx b/frontend/src/components/asp/requests/CreateMealRequest.tsx index 201a70f3..4e0e8e19 100644 --- a/frontend/src/components/asp/requests/CreateMealRequest.tsx +++ b/frontend/src/components/asp/requests/CreateMealRequest.tsx @@ -31,7 +31,7 @@ const CreateMealRequest = (): React.ReactElement => { const [deliveryInstructions, setDeliveryInstructions] = useState(""); // This is the selected onsite staff - const [onsiteStaff, setOnsiteStaff] = useState([ + const [onsiteContact, setOnsiteContact] = useState([ { name: "", email: "", @@ -154,8 +154,8 @@ const CreateMealRequest = (): React.ReactElement => { setDietaryRestrictions={setDietaryRestrictions} deliveryInstructions={deliveryInstructions} setDeliveryInstructions={setDeliveryInstructions} - onsiteStaff={onsiteStaff} - setOnsiteStaff={setOnsiteStaff} + onsiteContact={onsiteContact} + setOnsiteContact={setOnsiteContact} availableStaff={availableOnsiteContacts} handleBack={() => {}} // Will be assigned by three step form handleNext={() => {}} // Will be assigned by three step form @@ -168,7 +168,7 @@ const CreateMealRequest = (): React.ReactElement => { numMeals={numMeals} dietaryRestrictions={dietaryRestrictions} deliveryInstructions={deliveryInstructions} - onsiteStaff={onsiteStaff} + onsiteContact={onsiteContact} address={address} userId={userId} handleBack={() => {}} // Will be assigned by three step form diff --git a/frontend/src/components/asp/requests/SchedulingFormMealInfo.tsx b/frontend/src/components/asp/requests/SchedulingFormMealInfo.tsx index 85e2421c..66a2850f 100644 --- a/frontend/src/components/asp/requests/SchedulingFormMealInfo.tsx +++ b/frontend/src/components/asp/requests/SchedulingFormMealInfo.tsx @@ -21,7 +21,7 @@ import { import React, { useState } from "react"; import { Contact } from "../../../types/UserTypes"; -import OnsiteStaffSection from "../../common/OnsiteStaffSection"; +import OnsiteContactSection from "../../common/OnsiteContactSection"; type SchedulingFormMealInfoProps = { address: string; @@ -31,16 +31,14 @@ type SchedulingFormMealInfoProps = { setDietaryRestrictions: (dietaryRestrictions: string) => void; deliveryInstructions: string; setDeliveryInstructions: (deliveryInstructions: string) => void; - onsiteStaff: Contact[]; - setOnsiteStaff: React.Dispatch>; + onsiteContact: Contact[]; + setOnsiteContact: React.Dispatch>; availableStaff: Contact[]; handleBack: () => void; handleNext: () => void; }; -const SchedulingFormMealInfo: React.FunctionComponent< - SchedulingFormMealInfoProps -> = ({ +const SchedulingFormMealInfo: React.FunctionComponent = ({ address, numMeals, setNumMeals, @@ -48,8 +46,8 @@ const SchedulingFormMealInfo: React.FunctionComponent< setDietaryRestrictions, deliveryInstructions, setDeliveryInstructions, - onsiteStaff, - setOnsiteStaff, + onsiteContact, + setOnsiteContact, availableStaff, handleBack, handleNext, @@ -59,8 +57,8 @@ const SchedulingFormMealInfo: React.FunctionComponent< const validateData = () => { if ( numMeals <= 0 || - onsiteStaff.length === 0 || - onsiteStaff.some( + onsiteContact.length === 0 || + onsiteContact.some( (contact) => !contact || contact.name === "" || @@ -185,9 +183,9 @@ const SchedulingFormMealInfo: React.FunctionComponent< - { @@ -112,7 +111,7 @@ const SchedulingFormReviewAndSubmit: React.FunctionComponent staff.id), + onsiteContact: onsiteContact.map((staff: OnsiteContact) => staff.id), // Format the scheduled drop off time with the current time zone scheduledDropOffTime, userId, @@ -232,7 +231,7 @@ const SchedulingFormReviewAndSubmit: React.FunctionComponent - {onsiteStaff.map((staff, index) => + {onsiteContact.map((staff, index) => staff ? ( diff --git a/frontend/src/components/auth/Join.tsx b/frontend/src/components/auth/Join.tsx index 4379a2ab..73993d98 100644 --- a/frontend/src/components/auth/Join.tsx +++ b/frontend/src/components/auth/Join.tsx @@ -39,7 +39,7 @@ import { trimWhiteSpace, } from "../../utils/ValidationUtils"; import useIsWebView from "../../utils/useIsWebView"; -import OnsiteStaffSection from "../common/OnsiteStaffSection"; +import OnsiteContactSection from "../common/OnsiteContactSection"; const PLACEHOLDER_WEB_EXAMPLE_FULL_NAME = "Jane Doe"; const PLACEHOLDER_WEB_EXAMPLE_PHONE_NUMBER = "111-222-3333"; @@ -607,7 +607,8 @@ const Join = (): React.ReactElement => { borderRadius="8px" boxShadow={{ base: "", - lg: "0px 0px 3px rgba(0, 0, 0, 0.1), 0px 4px 20px rgba(0, 0, 0, 0.15)", + lg: + "0px 0px 3px rgba(0, 0, 0, 0.1), 0px 4px 20px rgba(0, 0, 0, 0.15)", }} style={{ backgroundColor: "white", @@ -622,7 +623,7 @@ const Join = (): React.ReactElement => { : getMobileOrganizationSection()} {isWebView && } {isWebView ? getWebContactSection() : getMobileContactSection()} - ); -type OnsiteStaffDropdownProps = { +type onsiteContactDropdownProps = { onsiteInfo: Array; setOnsiteInfo: React.Dispatch>; availableStaff: Array; @@ -127,7 +127,7 @@ const OnsiteDropdownInputRow = ({ availableStaff, index, attemptedSubmit, -}: OnsiteStaffDropdownProps): React.ReactElement => ( +}: onsiteContactDropdownProps): React.ReactElement => ( // Choose the name from a dropdown of available staff, and then fill in the rest of the info based on that @@ -194,7 +194,7 @@ const OnsiteDropdownInputRow = ({ )} ); -type OnsiteStaffSectionProps = { +type OnsiteContactSectionProps = { onsiteInfo: Array; setOnsiteInfo: React.Dispatch>; attemptedSubmit: boolean; @@ -202,13 +202,13 @@ type OnsiteStaffSectionProps = { dropdown?: boolean; }; -const OnsiteStaffSection = ({ +const OnsiteContactSection = ({ onsiteInfo, setOnsiteInfo, attemptedSubmit, availableStaff = [], dropdown = false, -}: OnsiteStaffSectionProps): React.ReactElement => { +}: OnsiteContactSectionProps): React.ReactElement => { const isWebView = useIsWebView(); const [showCreateModal, setShowCreateModal] = useState(false); @@ -431,4 +431,4 @@ const OnsiteStaffSection = ({ ); }; -export default OnsiteStaffSection; +export default OnsiteContactSection; diff --git a/frontend/src/components/meal_donor/donation_form/MealDonationForm.tsx b/frontend/src/components/meal_donor/donation_form/MealDonationForm.tsx index eac390c1..60534b5a 100644 --- a/frontend/src/components/meal_donor/donation_form/MealDonationForm.tsx +++ b/frontend/src/components/meal_donor/donation_form/MealDonationForm.tsx @@ -21,7 +21,7 @@ import TitleSection from "../../common/ThreeStepFormTitleSection"; const MealDonationForm = (): React.ReactElement => { // This is the selected onsite staff - const [onsiteStaff, setOnsiteStaff] = useState([ + const [onsiteContacts, setOnsiteContact] = useState([ { name: "", email: "", @@ -85,7 +85,7 @@ const MealDonationForm = (): React.ReactElement => { portions dietaryRestrictions } - onsiteStaff { + onsiteContacts { name email phone @@ -157,8 +157,8 @@ const MealDonationForm = (): React.ReactElement => { header3="Review & Submit" panel1={ {}} // Leave like this, gets updated by three-step form mealRequestsInformation={ @@ -186,7 +186,7 @@ const MealDonationForm = (): React.ReactElement => { } mealDescription={mealDescription} additionalInfo={additionalInfo} - onsiteStaff={onsiteStaff} + onsiteContact={onsiteContacts} requestorId={requestorId} primaryContact={primaryContact} handleBack={() => {}} // Leave like this, gets updated by three-step form diff --git a/frontend/src/components/meal_donor/donation_form/MealDonationFormContactInfo.tsx b/frontend/src/components/meal_donor/donation_form/MealDonationFormContactInfo.tsx index aaa112db..04206c19 100644 --- a/frontend/src/components/meal_donor/donation_form/MealDonationFormContactInfo.tsx +++ b/frontend/src/components/meal_donor/donation_form/MealDonationFormContactInfo.tsx @@ -22,19 +22,19 @@ import MealDeliveryDetails from "./MealDeliveryDetails"; import { MealRequest } from "../../../types/MealRequestTypes"; import { AuthenticatedUser, Contact } from "../../../types/UserTypes"; -import OnsiteStaffSection from "../../common/OnsiteStaffSection"; +import OnsiteContactSection from "../../common/OnsiteContactSection"; type MealDonationFormContactInfoProps = { - onsiteStaff: Contact[]; - setOnsiteStaff: React.Dispatch>; + onsiteContact: Contact[]; + setOnsiteContact: React.Dispatch>; availableStaff: Contact[]; handleNext: () => void; mealRequestsInformation: Array; }; const MealDonationFormContactInfo: React.FunctionComponent = ({ - onsiteStaff, - setOnsiteStaff, + onsiteContact, + setOnsiteContact, availableStaff, handleNext, mealRequestsInformation, @@ -43,8 +43,8 @@ const MealDonationFormContactInfo: React.FunctionComponent { if ( - onsiteStaff.length === 0 || - onsiteStaff.some( + onsiteContact.length === 0 || + onsiteContact.some( (contact) => !contact || contact.name === "" || @@ -80,9 +80,9 @@ const MealDonationFormContactInfo: React.FunctionComponent Contact Information - ; // From part 1 - onsiteStaff: OnsiteContact[]; + onsiteContact: OnsiteContact[]; // From part 2 mealDescription: string; @@ -78,7 +80,7 @@ type MealDonationFormReviewAndSubmitProps = { const MealDonationFormReviewAndSubmit: React.FunctionComponent = ({ mealRequestsInformation, - onsiteStaff, + onsiteContact, mealDescription, additionalInfo, requestorId, @@ -106,6 +108,7 @@ const MealDonationFormReviewAndSubmit: React.FunctionComponent contact.id), }, }); @@ -222,7 +225,7 @@ const MealDonationFormReviewAndSubmit: React.FunctionComponent - {onsiteStaff.map((staff, index) => + {onsiteContact.map((staff, index) => staff ? ( diff --git a/frontend/src/components/mealrequest/ASPListView.tsx b/frontend/src/components/mealrequest/ASPListView.tsx index fdf2c252..78d5554f 100644 --- a/frontend/src/components/mealrequest/ASPListView.tsx +++ b/frontend/src/components/mealrequest/ASPListView.tsx @@ -70,7 +70,7 @@ const GET_MEAL_REQUESTS_BY_ID = gql` portions dietaryRestrictions } - onsiteStaff { + onsiteContacts { name email phone @@ -81,9 +81,19 @@ const GET_MEAL_REQUESTS_BY_ID = gql` donationInfo { donor { info { + primaryContact { + name + email + phone + } organizationName } } + donorOnsiteContacts { + name + email + phone + } commitmentDate mealDescription additionalInfo @@ -145,9 +155,17 @@ const ASPListView = ({ authId, rowsPerPage = 10 }: ASPListViewProps) => { donor_name: mealRequest.donationInfo?.donor.info?.organizationName, num_meals: mealRequest.mealInfo?.portions, - primary_contact: mealRequest.requestor.info?.primaryContact, - onsite_staff: mealRequest.onsiteStaff, + primary_contact: + mealRequest.donationInfo?.donor?.info?.primaryContact ?? null, + onsite_contacts: mealRequest.onsiteContacts, + donor_onsite_contacts: + mealRequest.donationInfo?.donorOnsiteContacts ?? [], + delivery_notes: mealRequest.deliveryInstructions, + dietary_restrictions: + mealRequest.mealInfo?.dietaryRestrictions ?? "", + meal_description: mealRequest.donationInfo?.mealDescription, + meal_donor_notes: mealRequest.donationInfo?.additionalInfo, delivery_instructions: mealRequest.deliveryInstructions, pending: mealRequest.status === MealStatus.OPEN, _hasContent: false, @@ -241,7 +259,19 @@ const ASPListView = ({ authId, rowsPerPage = 10 }: ASPListViewProps) => { justifyContent="flex-end" onClick={handleExpand(item)} > - {ids.includes(item.id) ? : } + + {ids.includes(item.id) ? ( + + ) : ( + + )} + + + ); } @@ -272,6 +302,7 @@ const ASPListView = ({ authId, rowsPerPage = 10 }: ASPListViewProps) => { p="16px" borderBottom="1px solid" borderColor="gray.400" + flexWrap="wrap" > @@ -280,32 +311,66 @@ const ASPListView = ({ authId, rowsPerPage = 10 }: ASPListViewProps) => { Primary: - {item.primary_contact.name} + {item.primary_contact?.name ?? ""} - {item.primary_contact.email} + {item.primary_contact?.email ?? ""} - {item.primary_contact.phone} + {item.primary_contact?.phone ?? ""} - Onsite: - {item.onsite_staff.map((staff: Contact) => ( + Onsite Contacts + {item.donor_onsite_contacts?.map((staff: Contact) => ( {staff.name} {staff.email} {staff.phone} - ))} + )) ?? []} + + + + + Donor Provided Info: + + + Meal Description + {item.meal_description} + + + Donor Provided Notes: + {item.meal_donor_notes} + + + + + + Your Request: + + + Your Onsite Staff + {item.onsite_contacts?.map((staff: Contact) => ( + + {staff.name} + {staff.email} + {staff.phone} + + )) ?? []} + + + Dietary Restrictions + + {item.dietary_restrictions} + + + + Delivery Instructions + + {item.delivery_instructions} + + - - Meal Description: - {item.meal_description} - - - Meal Donor Notes: - {item.delivery_instructions} - ), @@ -430,7 +495,7 @@ const ASPListView = ({ authId, rowsPerPage = 10 }: ASPListViewProps) => { currentPage={currentPage} setCurrentPage={setCurrentPage} /> - + ); }; diff --git a/frontend/src/components/mealrequest/MealDonorListView.tsx b/frontend/src/components/mealrequest/MealDonorListView.tsx index a5446e92..98fb1874 100644 --- a/frontend/src/components/mealrequest/MealDonorListView.tsx +++ b/frontend/src/components/mealrequest/MealDonorListView.tsx @@ -1,23 +1,28 @@ import { gql, useLazyQuery } from "@apollo/client"; import { ChevronDownIcon, ChevronUpIcon } from "@chakra-ui/icons"; import { - Box, - Button as ChakraButton, - Collapse, - Flex, - HStack, - Menu, - MenuButton, - MenuItemOption, - MenuList, - MenuOptionGroup, - Text, - } from "@chakra-ui/react"; + Box, + Button as ChakraButton, + Collapse, + Flex, + HStack, + Menu, + MenuButton, + MenuItemOption, + MenuList, + MenuOptionGroup, + Text, +} from "@chakra-ui/react"; import * as TABLE_LIBRARY_TYPES from "@table-library/react-table-library/types/table"; - import React, { useEffect, useState } from "react"; -import { MealRequest, MealRequestsData, MealRequestsDonorVariables, MealRequestsVariables, MealStatus } from "../../types/MealRequestTypes"; +import { + MealRequest, + MealRequestsData, + MealRequestsDonorVariables, + MealRequestsVariables, + MealStatus, +} from "../../types/MealRequestTypes"; import { Contact } from "../../types/UserTypes"; import { logPossibleGraphQLError } from "../../utils/GraphQLUtils"; import ListView from "../common/ListView"; @@ -44,7 +49,7 @@ const GET_MEAL_REQUESTS_BY_ID = gql` id requestor { info { - organizationName, + organizationName primaryContact { name email @@ -59,7 +64,7 @@ const GET_MEAL_REQUESTS_BY_ID = gql` portions dietaryRestrictions } - onsiteStaff { + onsiteContacts { name email phone @@ -77,133 +82,144 @@ const GET_MEAL_REQUESTS_BY_ID = gql` } `; -type MealDonorListViewProps = { - completedMealRequests: { - nodes: TABLE_LIBRARY_TYPES.TableNode[] | undefined; - } | undefined; +type MealDonorListViewProps = { + completedMealRequests: + | { + nodes: TABLE_LIBRARY_TYPES.TableNode[] | undefined; + } + | undefined; completedMealRequestsLoading: boolean; - currentPage: number; + currentPage: number; setCurrentPage: React.Dispatch>; -} +}; -const MealDonorListView = ({ completedMealRequests, completedMealRequestsLoading, currentPage, setCurrentPage }: MealDonorListViewProps) => { - const [ids, setIds] = React.useState>( - [], - ); +const MealDonorListView = ({ + completedMealRequests, + completedMealRequestsLoading, + currentPage, + setCurrentPage, +}: MealDonorListViewProps) => { + const [ids, setIds] = React.useState>( + [], + ); - const handleExpand = (item: TABLE_LIBRARY_TYPES.TableNode) => () => { - if (item.pending) return; + const handleExpand = (item: TABLE_LIBRARY_TYPES.TableNode) => () => { + if (item.pending) return; - if (ids.includes(item.id)) { - setIds(ids.filter((id) => id !== item.id)); - } else { - setIds(ids.concat(item.id)); - } - }; + if (ids.includes(item.id)) { + setIds(ids.filter((id) => id !== item.id)); + } else { + setIds(ids.concat(item.id)); + } + }; - const COLUMNS = [ + const COLUMNS = [ { - label: "Date", - renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( + label: "Date", + renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( - {item.date_requested.toLocaleDateString("en-US", { + {item.date_requested.toLocaleDateString("en-US", { year: "numeric", month: "short", day: "numeric", - })} + })} - ), + ), }, { - label: "Time Requested", - renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( + label: "Time Requested", + renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( - {item.time_requested.toLocaleTimeString("en-US", { + {item.time_requested.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", - })} + })} - ), + ), }, { - label: "ASP Name", - renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( + label: "ASP Name", + renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( - {item.asp_name} + {item.asp_name} - ), + ), }, { - label: "# of Meals", - renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( - {item.num_meals} - ), + label: "# of Meals", + renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( + {item.num_meals} + ), }, { - label: "", - renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( - - {ids.includes(item.id) ? : } - - ), - }, - ]; + label: "", + renderCell: (item: TABLE_LIBRARY_TYPES.TableNode) => ( + + {ids.includes(item.id) ? : } + + ), + }, + ]; - const ROW_OPTIONS = { - renderAfterRow: (item: TABLE_LIBRARY_TYPES.TableNode) => ( - - - - Donation Address - {item.donation_address} - Dietary Restrictions - {item.dietary_restrictions} - - - ASP Onsite Staff - {item.onsite_staff.map((staff: Contact) => ( - - {staff.name} - {staff.email} - {staff.phone} - - ))} - - - My Contact Info - {item.contact_info} - Meal Description: - {item.meal_description} - - - - ), - }; + const ROW_OPTIONS = { + renderAfterRow: (item: TABLE_LIBRARY_TYPES.TableNode) => ( + + + + Donation Address + + {item.donation_address} + + Dietary Restrictions + {item.dietary_restrictions} + + + ASP Onsite Staff + {item.onsite_contact.map((staff: Contact) => ( + + {staff.name} + {staff.email} + {staff.phone} + + ))} + + + My Contact Info + + {item.contact_info} + + Meal Description: + {item.meal_description} + + + + ), + }; - return ( - - ) -} + return ( + + ); +}; -export default MealDonorListView; \ No newline at end of file +export default MealDonorListView; diff --git a/frontend/src/pages/EditMealRequestForm.tsx b/frontend/src/pages/EditMealRequestForm.tsx index 8e9797a4..fc6fc70d 100644 --- a/frontend/src/pages/EditMealRequestForm.tsx +++ b/frontend/src/pages/EditMealRequestForm.tsx @@ -27,7 +27,7 @@ import { GraphQLError } from "graphql"; import React, { useContext, useEffect, useState } from "react"; import LoadingSpinner from "../components/common/LoadingSpinner"; -import OnsiteStaffSection from "../components/common/OnsiteStaffSection"; +import OnsiteContactSection from "../components/common/OnsiteContactSection"; import AuthContext from "../contexts/AuthContext"; import { MealRequestsData } from "../types/MealRequestTypes"; import { Contact, OnsiteContact } from "../types/UserTypes"; @@ -55,7 +55,7 @@ const GET_MEAL_REQUEST_BY_ID = gql` portions dietaryRestrictions } - onsiteStaff { + onsiteContacts { id name email @@ -85,7 +85,7 @@ const UPDATE_MEAL_REQUEST = gql` portions: $updatedMealInfoPortions dietaryRestrictions: $updatedMealInfoDietaryRestrictions } - onsiteStaff: $updatedOnsiteContacts + onsiteContacts: $updatedOnsiteContacts ) { mealRequest { id @@ -96,7 +96,7 @@ const UPDATE_MEAL_REQUEST = gql` portions dietaryRestrictions } - onsiteStaff { + onsiteContacts { id name email @@ -153,14 +153,20 @@ const EditMealRequestForm = ({ const [dietaryRestrictions, setDietaryRestrictions] = useState(""); const [deliveryInstructions, setDeliveryInstructions] = useState(""); const [loading, setLoading] = useState(true); + const [onsiteContactsLoading, setOnsiteContactsLoading] = useState(true); + const [isUpcoming, setIsUpcoming] = useState(false); const toast = useToast(); - const [onsiteStaff, setOnsiteStaff] = useState([]); + const [onsiteContact, setOnsiteContact] = useState([]); // This is the list of available onsite staff const [availableOnsiteContacts, setAvailableOnsiteContacts] = useState< Array >([]); - useGetOnsiteContacts(toast, setAvailableOnsiteContacts, setLoading); + useGetOnsiteContacts( + toast, + setAvailableOnsiteContacts, + setOnsiteContactsLoading, + ); const apolloClient = useApolloClient(); @@ -178,9 +184,12 @@ const EditMealRequestForm = ({ setNumberOfMeals(mealRequest.mealInfo.portions); setDietaryRestrictions(mealRequest.mealInfo.dietaryRestrictions); setDeliveryInstructions(mealRequest.deliveryInstructions); + setIsUpcoming(mealRequest.status === "UPCOMING"); // Parse/stringify is to make a deep copy of the onsite staff - setOnsiteStaff(JSON.parse(JSON.stringify(mealRequest.onsiteStaff))); + setOnsiteContact( + JSON.parse(JSON.stringify(mealRequest.onsiteContacts)), + ); setLoading(false); } catch (error) { logPossibleGraphQLError(error as ApolloError); @@ -201,7 +210,7 @@ const EditMealRequestForm = ({ updatedDeliveryInstructions: deliveryInstructions, updatedMealInfoPortions: numberOfMeals, updatedMealInfoDietaryRestrictions: dietaryRestrictions, - updatedOnsiteContacts: onsiteStaff.map((contact) => contact.id), + updatedOnsiteContacts: onsiteContact.map((contact) => contact.id), }, }); const data = response.data; @@ -376,40 +385,39 @@ const EditMealRequestForm = ({ ); return ( - <> - {/* */} - - - - {loading ? ( - - ) : ( - <> + + + + {loading || onsiteContactsLoading ? ( + + ) : ( + <> + + Edit Meal Request + + + - Edit Meal Request + Meal Information - - - - Meal Information - + {!isUpcoming ? ( + ) : null} + {!isUpcoming ? ( setDietaryRestrictions(e.target.value)} /> + ) : null} - - - Delivery Notes - - setDeliveryInstructions(e.target.value)} - /> -
-
- {isWebView && } - -
- - - - - - - )} -
-
- + Delivery Notes + + setDeliveryInstructions(e.target.value)} + /> +
+ + {isWebView && } + + + + + + + + + )} +
+
); }; diff --git a/frontend/src/pages/MealDonorCalendar.tsx b/frontend/src/pages/MealDonorCalendar.tsx index b451dfba..e7429793 100644 --- a/frontend/src/pages/MealDonorCalendar.tsx +++ b/frontend/src/pages/MealDonorCalendar.tsx @@ -75,7 +75,7 @@ const GET_MEAL_REQUESTS_BY_ID = gql` portions dietaryRestrictions } - onsiteStaff { + onsiteContacts { name email phone diff --git a/frontend/src/pages/MealRequestForm.tsx b/frontend/src/pages/MealRequestForm.tsx index b1de5409..d9ffdbfe 100644 --- a/frontend/src/pages/MealRequestForm.tsx +++ b/frontend/src/pages/MealRequestForm.tsx @@ -73,346 +73,393 @@ const MealRequestForm = () => { const initialRef = React.useRef(null); const finalRef = React.useRef(null); - const getMobileOnsiteStaffSection = (): React.ReactElement => ( - - {onsiteInfo.map((info, index) => ( - - - - - {`Additional Onsite Staff (${index + 1})`} - - - ( + + {onsiteInfo.map((info, index) => ( + + + + + {`Additional Onsite Staff (${index + 1})`} + + + + {onsiteInfo.length >= 2 && ( + - {onsiteInfo.length >= 2 && ( - { - onsiteInfo.splice(index, 1); - setOnsiteInfo([...onsiteInfo]); - }} - /> - )} - - {index === 0 && ( - - *Must add at least 1 onsite staff. Maximum of 10. - - )} - - { - onsiteInfo[index].name = e.target.value; + onClick={() => { + onsiteInfo.splice(index, 1); setOnsiteInfo([...onsiteInfo]); }} /> - - - { - onsiteInfo[index].phone = e.target.value; - setOnsiteInfo([...onsiteInfo]); - }} - /> - - - { - onsiteInfo[index].email = e.target.value; - setOnsiteInfo([...onsiteInfo]); - }} - /> - + )} - ))} - {onsiteInfo.length < 10 && ( - { - setOnsiteInfo([ - ...onsiteInfo, - { - name: "", - phone: "", - email: "", - }, - ]); - }} + {index === 0 && ( + + *Must add at least 1 onsite staff. Maximum of 10. + + )} + - + Add another contact - - )} - - ); - - const getWebOnsiteStaffSection = (): React.ReactElement => ( - - - - - 2. Additional onsite staff - + { + onsiteInfo[index].name = e.target.value; + setOnsiteInfo([...onsiteInfo]); + }} + /> + + + { + onsiteInfo[index].phone = e.target.value; + setOnsiteInfo([...onsiteInfo]); + }} + /> + + + { + onsiteInfo[index].email = e.target.value; + setOnsiteInfo([...onsiteInfo]); + }} + /> - - *Must add at least 1 onsite staff. Maximum of 10. - - - - - { + setOnsiteInfo([ + ...onsiteInfo, + { + name: "", + phone: "", + email: "", + }, + ]); + }} + > + + Add another contact + + )} + + ); + + const getWebOnsiteContactSection = (): React.ReactElement => ( + + + + + 2. Additional onsite staff + + + + *Must add at least 1 onsite staff. Maximum of 10. + + + +
+ + + - - - - + + Full Name + + + + + + - - {onsiteInfo.map((info, index) => ( - - - - - + + + + + {onsiteInfo.length >= 2 ? ( + - {onsiteInfo.length >= 2 ? ( - - ) : ( - - ))} - -
- - - Full Name - - - - Phone Number - - - - Email - - -
+ + Phone Number + + + + Email + + +
- - { - onsiteInfo[index].name = e.target.value; - setOnsiteInfo([...onsiteInfo]); - }} - /> - - - - { - onsiteInfo[index].phone = e.target.value; - setOnsiteInfo([...onsiteInfo]); - }} - /> - - - - { - onsiteInfo[index].email = e.target.value; - setOnsiteInfo([...onsiteInfo]); - }} - /> - - - + {onsiteInfo.map((info, index) => ( +
+ + { + onsiteInfo[index].name = e.target.value; + setOnsiteInfo([...onsiteInfo]); + }} + /> + + + + { + onsiteInfo[index].phone = e.target.value; + setOnsiteInfo([...onsiteInfo]); + }} + /> + + + + { + onsiteInfo[index].email = e.target.value; + setOnsiteInfo([...onsiteInfo]); + }} + /> + + + + + { + onsiteInfo.splice(index, 1); + setOnsiteInfo([...onsiteInfo]); + }} + pr={2} /> - { - onsiteInfo.splice(index, 1); - setOnsiteInfo([...onsiteInfo]); - }} - pr={2} - /> - - )} -
-
- {onsiteInfo.length < 10 && ( - { - setOnsiteInfo([ - ...onsiteInfo, - { - name: "", - phone: "", - email: "", - }, - ]); - }} - > - + Add another contact - - )} -
- ); + ) : ( + + )} + + ))} + + + + {onsiteInfo.length < 10 && ( + { + setOnsiteInfo([ + ...onsiteInfo, + { + name: "", + phone: "", + email: "", + }, + ]); + }} + > + + Add another contact + + )} +
+ ); const getMobileContactSection = (): React.ReactElement => ( - - - - Primary Contact - - - + + Primary Contact + + + + setPrimaryContact({ ...primaryContact, name: e.target.value }) + } + placeholder={PLACEHOLDER_MOBILE_EXAMPLE_FULL_NAME} + /> + + + + setPrimaryContact({ + ...primaryContact, + phone: e.target.value, + }) + } + placeholder={PLACEHOLDER_MOBILE_EXAMPLE_PHONE_NUMBER} + /> + + + + setPrimaryContact({ + ...primaryContact, + email: e.target.value, + }) + } + placeholder={PLACEHOLDER_MOBILE_EXAMPLE_EMAIL} + /> + + + + + ); + + const getWebContactSection = (): React.ReactElement => ( + <> + + Contact Information + + + + + + - - setPrimaryContact({ ...primaryContact, name: e.target.value }) - } - placeholder={PLACEHOLDER_MOBILE_EXAMPLE_FULL_NAME} - /> - + 1. Primary contact name + + + setPrimaryContact({ ...primaryContact, name: e.target.value }) + } + /> + + + + + + + Phone number + setPrimaryContact({ ...primaryContact, phone: e.target.value, }) } - placeholder={PLACEHOLDER_MOBILE_EXAMPLE_PHONE_NUMBER} - /> - - - - setPrimaryContact({ - ...primaryContact, - email: e.target.value, - }) - } - placeholder={PLACEHOLDER_MOBILE_EXAMPLE_EMAIL} /> - - - ); - - const getWebContactSection = (): React.ReactElement => ( - <> - - Contact Information - - - + { md: "form-label-bold", }} > - 1. Primary contact name + Email address - setPrimaryContact({ ...primaryContact, name: e.target.value }) + setPrimaryContact({ + ...primaryContact, + email: e.target.value, + }) } /> - - - - - - Phone number - - - setPrimaryContact({ - ...primaryContact, - phone: e.target.value, - }) - } - /> - - - - - - - Email address - - - setPrimaryContact({ - ...primaryContact, - email: e.target.value, - }) - } - /> - - - - - ); + + + ); return ( <> @@ -573,8 +566,8 @@ const MealRequestForm = () => { {isWebView && } {isWebView ? getWebContactSection() : getMobileContactSection()} {isWebView - ? getWebOnsiteStaffSection() - : getMobileOnsiteStaffSection()} + ? getWebOnsiteContactSection() + : getMobileOnsiteContactSection()} diff --git a/frontend/src/pages/Settings.tsx b/frontend/src/pages/Settings.tsx index e002f168..fc94c851 100644 --- a/frontend/src/pages/Settings.tsx +++ b/frontend/src/pages/Settings.tsx @@ -29,7 +29,7 @@ import React, { useContext, useState } from "react"; import { Navigate, useNavigate } from "react-router-dom"; import Logout from "../components/auth/Logout"; -import OnsiteStaffSection from "../components/common/OnsiteStaffSection"; +import OnsiteContactSection from "../components/common/OnsiteContactSection"; import AUTHENTICATED_USER_KEY from "../constants/AuthConstants"; import { LOGIN_PAGE } from "../constants/Routes"; import AuthContext from "../contexts/AuthContext"; @@ -213,9 +213,16 @@ const Settings = (): React.ReactElement => { useGetOnsiteContacts( toast, - (contacts) => { - setOnsiteContacts(contacts); - setServerOnsiteContacts(contacts); + (contacts: OnsiteContact[]) => { + const set1 = contacts.map((contact: OnsiteContact) => + JSON.parse(JSON.stringify(contact)), + ); + const set2 = contacts.map((contact: OnsiteContact) => + JSON.parse(JSON.stringify(contact)), + ); + // const set2 + setOnsiteContacts(set1); + setServerOnsiteContacts(set2); }, setIsLoading, ); @@ -301,7 +308,7 @@ const Settings = (): React.ReactElement => { {userInfo?.email}
- +