diff --git a/backend/app/.gitignore b/backend/app/.gitignore new file mode 100644 index 00000000..bbc6b873 --- /dev/null +++ b/backend/app/.gitignore @@ -0,0 +1,6 @@ +.py[cod] +__pycache__/ +.mypy_cache/ +.pytest_cache/ +.ruff_cache/ +.pdm-python diff --git a/backend/app/models/UserData.py b/backend/app/models/UserData.py index ee8d17f4..aea43a02 100644 --- a/backend/app/models/UserData.py +++ b/backend/app/models/UserData.py @@ -67,6 +67,7 @@ class UserData(Base): # "Other" text fields for custom entries other_ethnic_group = Column(Text, nullable=True) gender_identity_custom = Column(Text, nullable=True) + additional_info = Column(Text, nullable=True) # Flow control fields has_blood_cancer = Column(Text, nullable=True) diff --git a/backend/app/services/implementations/intake_form_processor.py b/backend/app/services/implementations/intake_form_processor.py index 5d32f404..75acad90 100644 --- a/backend/app/services/implementations/intake_form_processor.py +++ b/backend/app/services/implementations/intake_form_processor.py @@ -80,6 +80,9 @@ def process_form_submission(self, user_id: str, form_data: Dict[str, Any]) -> Us }: owning_user.form_status = FormStatus.INTAKE_SUBMITTED + if "additional_info" in form_data: + user_data.additional_info = self._trim_text(form_data.get("additional_info")) + # Commit all changes self.db.commit() self.db.refresh(user_data) diff --git a/backend/docs/intake_api.md b/backend/docs/intake_api.md index b7fd40e2..0f03a324 100644 --- a/backend/docs/intake_api.md +++ b/backend/docs/intake_api.md @@ -55,8 +55,9 @@ Create a new form submission and process it into structured data. "dateOfDiagnosis": "DD/MM/YYYY (optional)", "treatments": ["array of treatment names (optional)"], "experiences": ["array of experience names (optional)"], - } - } + }, + }, + "additional_info": "string (optional)", } } ``` @@ -159,7 +160,8 @@ curl -X POST "/intake/submissions" \ "dateOfDiagnosis": "10/01/2020", "treatments": ["Chemotherapy", "Radiation Therapy"], "experiences": ["Fatigue", "Depression"] - } + }, + "additional_info": "", } }' ``` @@ -194,7 +196,8 @@ curl -X POST "/intake/submissions" \ "treatments": ["Chemotherapy"], "experiences": ["Hair Loss", "Anxiety"] } - } + }, + "additional_info": "string (optional)", } }' ``` diff --git a/backend/migrations/versions/560fa7da2ff1_added_additional_info_col_to_userdata.py b/backend/migrations/versions/560fa7da2ff1_added_additional_info_col_to_userdata.py new file mode 100644 index 00000000..60d21e66 --- /dev/null +++ b/backend/migrations/versions/560fa7da2ff1_added_additional_info_col_to_userdata.py @@ -0,0 +1,31 @@ +"""added additional info col to userdata + +Revision ID: 560fa7da2ff1 +Revises: b56e0bf600a2 +Create Date: 2025-10-02 19:41:51.533804 + +""" + +from typing import Sequence, Union + +import sqlalchemy as sa +from alembic import op + +# revision identifiers, used by Alembic. +revision: str = "560fa7da2ff1" +down_revision: Union[str, None] = "b56e0bf600a2" +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # Add additional_info column to user_data table + op.add_column( + "user_data", + sa.Column("additional_info", sa.Text(), nullable=True), + ) + + +def downgrade() -> None: + # Remove additional_info column from user_data table + op.drop_column("user_data", "additional_info") diff --git a/backend/tests/unit/test_intake_form_processor.py b/backend/tests/unit/test_intake_form_processor.py index 47f6a71e..958d0eb1 100644 --- a/backend/tests/unit/test_intake_form_processor.py +++ b/backend/tests/unit/test_intake_form_processor.py @@ -167,6 +167,7 @@ def test_participant_with_cancer_only(db_session, test_user): "treatments": ["Chemotherapy", "Transfusions"], "experiences": ["Anxiety", "Fatigue"], }, + "additional_info": " I have specific dietary restrictions and prefer morning appointments.", } # Act @@ -196,6 +197,9 @@ def test_participant_with_cancer_only(db_session, test_user): assert user_data.has_blood_cancer == "yes" assert user_data.caring_for_someone == "no" + # Assert - Additional Info + assert user_data.additional_info == "I have specific dietary restrictions and prefer morning appointments." + # Assert - Treatments (many-to-many) treatment_names = [t.name for t in user_data.treatments] assert "Chemotherapy" in treatment_names @@ -252,6 +256,7 @@ def test_volunteer_caregiver_experience_processing(db_session, test_user): "caregiver_experience": { "experiences": ["Anxiety", "Depression"], }, + "additional_info": "I have experience with elderly care and can provide emotional support. I have special requirements \n\t", "loved_one": { "demographics": {"gender_identity": "Male", "age": "45-54"}, "cancer_experience": { @@ -285,6 +290,12 @@ def test_volunteer_caregiver_experience_processing(db_session, test_user): assert "Anxiety" in experience_names assert "Depression" in experience_names + # Assert - Additional Info + assert ( + user_data.additional_info + == "I have experience with elderly care and can provide emotional support. I have special requirements" + ) + # Assert - No personal cancer experience assert user_data.diagnosis is None assert user_data.date_of_diagnosis is None @@ -357,6 +368,7 @@ def test_form_submission_json_structure(db_session, test_user): "experiences": ["Brain Fog", "Feeling Overwhelmed"], }, }, + "additional_info": "I'm only free on weekdays.", } # Act @@ -380,6 +392,8 @@ def test_form_submission_json_structure(db_session, test_user): assert len(user_data.loved_one_treatments) >= 2 # Radiation + Palliative assert len(user_data.loved_one_experiences) >= 2 # Brain Fog + Feeling Overwhelmed + assert user_data.additional_info == "I'm only free on weekdays." + db_session.refresh(test_user) assert test_user.form_status == FormStatus.INTAKE_SUBMITTED @@ -415,6 +429,7 @@ def test_empty_and_minimal_data_handling(db_session, test_user): "marital_status": "", # Empty string "has_kids": "", }, + "additional_info": "", # No cancer_experience, caregiver_experience, or loved_one sections } @@ -439,6 +454,8 @@ def test_empty_and_minimal_data_handling(db_session, test_user): assert len(user_data.experiences) == 0 assert user_data.loved_one_gender_identity is None + assert user_data.additional_info == "" + db_session.refresh(test_user) assert test_user.form_status == FormStatus.INTAKE_SUBMITTED @@ -483,6 +500,7 @@ def test_participant_caregiver_without_cancer(db_session, test_user): "experiences": ["Anxiety", "Communication Challenges"], }, }, + "additional_info": "I need to be in the same city as the loved one", } # Act @@ -520,6 +538,8 @@ def test_participant_caregiver_without_cancer(db_session, test_user): assert "Anxiety" in loved_one_experience_names assert "Communication Challenges" in loved_one_experience_names + assert user_data.additional_info == "I need to be in the same city as the loved one" + db_session.commit() except Exception: @@ -605,6 +625,8 @@ def test_participant_cancer_patient_and_caregiver(db_session, test_user): assert "Other" in user_data.ethnic_group assert user_data.other_ethnic_group == "Mixed European heritage" + assert user_data.additional_info is None + db_session.commit() except Exception: @@ -672,6 +694,8 @@ def test_participant_no_cancer_experience(db_session, test_user): assert len(user_data.loved_one_treatments) == 0 assert len(user_data.loved_one_experiences) == 0 + assert user_data.additional_info is None + db_session.commit() except Exception: @@ -741,6 +765,8 @@ def test_volunteer_cancer_patient_only(db_session, test_user): assert len(user_data.loved_one_treatments) == 0 assert len(user_data.loved_one_experiences) == 0 + assert user_data.additional_info is None + db_session.commit() except Exception: @@ -788,6 +814,7 @@ def test_volunteer_cancer_patient_and_caregiver(db_session, test_user): "experiences": ["Brain Fog", "Fatigue"], }, }, + "additional_info": "N/A", } # Act @@ -818,6 +845,8 @@ def test_volunteer_cancer_patient_and_caregiver(db_session, test_user): assert len(user_data.loved_one_treatments) >= 2 assert len(user_data.loved_one_experiences) >= 2 + assert user_data.additional_info == "N/A" + db_session.commit() except Exception: @@ -884,6 +913,8 @@ def test_volunteer_no_cancer_experience(db_session, test_user): assert len(user_data.loved_one_treatments) == 0 assert len(user_data.loved_one_experiences) == 0 + assert user_data.additional_info is None + db_session.commit() except Exception: @@ -1179,6 +1210,7 @@ def test_unicode_and_special_characters(db_session, test_user): "treatments": ["Chimiothérapie"], "experiences": ["Fatigue"], }, + "additional_info": "émojis 🎉, unicode 中文, and various symbols @#$%^&*()", } user_data = processor.process_form_submission(str(test_user.id), form_data) @@ -1192,6 +1224,8 @@ def test_unicode_and_special_characters(db_session, test_user): assert "हिन्दी" in user_data.other_ethnic_group assert "🌍" in user_data.other_ethnic_group + assert user_data.additional_info == "émojis 🎉, unicode 中文, and various symbols @#$%^&*()" + db_session.commit() except Exception: diff --git a/frontend/src/components/intake/additional-info-form.tsx b/frontend/src/components/intake/additional-info-form.tsx new file mode 100644 index 00000000..d44753b5 --- /dev/null +++ b/frontend/src/components/intake/additional-info-form.tsx @@ -0,0 +1,174 @@ +import React from 'react'; +import { Box, Heading, Button, VStack, HStack, Text, Textarea } from '@chakra-ui/react'; +import { Controller, useForm } from 'react-hook-form'; +import { COLORS } from '@/constants/form'; + +interface AdditionalInfoFormData { + additionalInfo: string; +} + +interface AdditionalInfoFormProps { + formType: 'participant' | 'volunteer'; + onSubmit: (data: AdditionalInfoFormData) => void; + onBack?: () => void; +} + +export function AdditionalInfoForm({ formType, onSubmit, onBack }: AdditionalInfoFormProps) { + const { + control, + handleSubmit, + formState: { errors, isSubmitting }, + } = useForm({ + defaultValues: { + additionalInfo: '', + }, + }); + + const onFormSubmit = (data: AdditionalInfoFormData) => { + onSubmit(data); + }; + + const formTitle = + formType === 'participant' + ? 'First Connection Participant Form' + : 'First Connection Volunteer Form'; + + return ( +
+ {/* Header */} + + {formTitle} + + + {/* Progress Bar */} + + + + + + + + + + + + + + + {/* Additional Information Section */} + + + Additional Information + + + Is there anything else you'd like to share with us? + + + + + + Additional Information (Optional) + + + ( +