From 7bffe1160546076a0f3fa3b3a40e1fba594e1e74 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Fri, 15 May 2026 22:00:46 +0930 Subject: [PATCH 001/105] fix: Remove max height from login form --- client/src/components/authentication/LoginForm.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/components/authentication/LoginForm.js b/client/src/components/authentication/LoginForm.js index 2cc8fc93..a92794a2 100644 --- a/client/src/components/authentication/LoginForm.js +++ b/client/src/components/authentication/LoginForm.js @@ -120,7 +120,6 @@ const LoginForm = () => { xl: "600px", }, minHeight: "auto", - maxHeight: "400px", boxShadow: { xs: "none", sm: 16 }, }} > From 0c9ba75e7c756fb6940d7baf222fcb477490a29b Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Fri, 15 May 2026 22:19:17 +0930 Subject: [PATCH 002/105] Add marital and working status to patient model --- ...8b27_add_marital_and_working_status_to_.py | 34 +++++++++++++++++++ server/models/dbmodels.py | 2 ++ 2 files changed, 36 insertions(+) create mode 100644 server/alembic/versions/d89af5748b27_add_marital_and_working_status_to_.py diff --git a/server/alembic/versions/d89af5748b27_add_marital_and_working_status_to_.py b/server/alembic/versions/d89af5748b27_add_marital_and_working_status_to_.py new file mode 100644 index 00000000..1305ae4f --- /dev/null +++ b/server/alembic/versions/d89af5748b27_add_marital_and_working_status_to_.py @@ -0,0 +1,34 @@ +"""Add marital and working status to patient + +Revision ID: d89af5748b27 +Revises: b1d8c21fe405 +Create Date: 2026-05-15 22:18:17.182065 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'd89af5748b27' +down_revision: Union[str, Sequence[str], None] = 'b1d8c21fe405' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + """Upgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('Patient', sa.Column('MaritalStatus', sa.Integer(), nullable=True)) + op.add_column('Patient', sa.Column('WorkingStatus', sa.Integer(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('Patient', 'WorkingStatus') + op.drop_column('Patient', 'MaritalStatus') + # ### end Alembic commands ### diff --git a/server/models/dbmodels.py b/server/models/dbmodels.py index f2b64627..1968fd6b 100644 --- a/server/models/dbmodels.py +++ b/server/models/dbmodels.py @@ -129,6 +129,8 @@ class Patient(Base): Weight = Column(Numeric(5, 2)) Height = Column(Numeric(5, 2)) DateOfBirth = Column(Date) + MaritalStatus = Column(Integer) + WorkingStatus = Column(Integer) CreatedAt = Column(DateTime, server_default=text('CURRENT_TIMESTAMP')) user = relationship("UserAccount", back_populates="patients") From b48148c11e192285692b8fb754af7becd06b825f Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 14:23:25 +0930 Subject: [PATCH 003/105] Return marital and working status with user information --- server/routers/users.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/routers/users.py b/server/routers/users.py index 040ea2ed..d4c9597f 100644 --- a/server/routers/users.py +++ b/server/routers/users.py @@ -991,7 +991,9 @@ async def get_patient_data(request: Request, db_conn: Session = Depends(get_db)) "weight": float(patient.Weight) if patient.Weight else None, "height": float(patient.Height) if patient.Height else None, "gender": get_gender(patient.Gender), - "age": get_age(patient.DateOfBirth) + "age": get_age(patient.DateOfBirth), + "maritalStatus": patient.get_marital_status_title(), + "workingStatus": patient.get_working_status_title(), } return result From d8ed708bdd000b51a970b8e212e0355c20f1f4d5 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 14:25:27 +0930 Subject: [PATCH 004/105] Update marital and working status on report upload --- server/routers/health_prediction.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/server/routers/health_prediction.py b/server/routers/health_prediction.py index 59b6e829..7abc622b 100644 --- a/server/routers/health_prediction.py +++ b/server/routers/health_prediction.py @@ -107,15 +107,30 @@ async def predict(data: HealthDataInput, request: Request, db_conn: Session = De # Update Weight & Height based on sanitized input. patient.Weight = sanitized_data["weight"] patient.Height = sanitized_data["height"] + patient.set_marital_status(sanitized_data["marital_status"]) + patient.set_working_status(sanitized_data["working_status"]) # Get the CSV patients's ID, otherwise uses the authenticated user's ID. patient_id = csv_patient_id if csv_patient_id is not None else patient.PatientID - healthData = HealthData(patient_id, sanitized_data["age"], sanitized_data["weight"], sanitized_data["height"], gender_map[sanitized_data["gender"]], - sanitized_data["blood_glucose"], sanitized_data["ap_hi"], sanitized_data[ - "ap_lo"], sanitized_data["high_cholesterol"], - sanitized_data["hypertension"], sanitized_data[ - "heart_disease"], sanitized_data["diabetes"], sanitized_data["alcohol"], - smoker_map[sanitized_data["smoker"]], marital_map[sanitized_data["marital_status"]], working_map[sanitized_data["working_status"]], sanitized_data["stroke"]) + healthData = HealthData( + patient_id, + sanitized_data["age"], + sanitized_data["weight"], + sanitized_data["height"], + gender_map[sanitized_data["gender"]], + sanitized_data["blood_glucose"], + sanitized_data["ap_hi"], + sanitized_data["ap_lo"], + sanitized_data["high_cholesterol"], + + sanitized_data["hypertension"], + sanitized_data["heart_disease"], + sanitized_data["diabetes"], + sanitized_data["alcohol"], + smoker_map[sanitized_data["smoker"]], + marital_map[sanitized_data["marital_status"]], + working_map[sanitized_data["working_status"]], + sanitized_data["stroke"]) # Store health data into the database db_conn.add(healthData) From 1183aa5212a4b536b1fa44ba58baad650b037104 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 14:26:43 +0930 Subject: [PATCH 005/105] Add marital and working status to patient model --- server/models/dbmodels.py | 43 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/server/models/dbmodels.py b/server/models/dbmodels.py index 1968fd6b..af208d63 100644 --- a/server/models/dbmodels.py +++ b/server/models/dbmodels.py @@ -2,6 +2,14 @@ from sqlalchemy.orm import declarative_base, relationship import enum +MARITAL_STATUS_OPTIONS = ['Single', 'Married'] +WORKING_STATUS_OPTIONS = [ + 'Unemployed', + 'Private', + 'Student', + 'Public', +] + Base = declarative_base() @@ -129,15 +137,17 @@ class Patient(Base): Weight = Column(Numeric(5, 2)) Height = Column(Numeric(5, 2)) DateOfBirth = Column(Date) + CreatedAt = Column(DateTime, server_default=text('CURRENT_TIMESTAMP')) MaritalStatus = Column(Integer) WorkingStatus = Column(Integer) - CreatedAt = Column(DateTime, server_default=text('CURRENT_TIMESTAMP')) user = relationship("UserAccount", back_populates="patients") health_records = relationship("HealthData", back_populates="patient") user_access = relationship("UserPatientAccess", back_populates="patient") - def __init__(self, user_id, given_names, family_name, gender, weight, height, date_of_birth): + def __init__(self, user_id, given_names, + family_name, gender, weight, height, + date_of_birth, marital_status, working_status): self.UserID = user_id self.GivenNames = given_names self.FamilyName = family_name @@ -145,12 +155,41 @@ def __init__(self, user_id, given_names, family_name, gender, weight, height, da self.Weight = weight self.Height = height self.DateOfBirth = date_of_birth + self.MaritalStatus = marital_status + self.WorkingStatus = working_status def __repr__(self): return f'Patient(PatientID={self.PatientID}, UserID={self.UserID}, givenNames={self.GivenNames}, \ familyName={self.FamilyName}, gender={self.Gender}, weight={self.Weight}, height={self.Height}, \ dateOfBirth={self.DateOfBirth}, Created={self.CreatedAt})' + def get_marital_status_id(self): + return self.MaritalStatus + + def get_marital_status_title(self): + if (self.MaritalStatus is not None + and self.MaritalStatus < len(MARITAL_STATUS_OPTIONS)): + return MARITAL_STATUS_OPTIONS[self.MaritalStatus] + return None + + def get_working_status_title(self): + if (self.WorkingStatus is not None + and self.WorkingStatus < len(WORKING_STATUS_OPTIONS)): + return WORKING_STATUS_OPTIONS[self.WorkingStatus] + return None + + def set_marital_status(self, status: int | str): + if isinstance(status, str): + self.MaritalStatus = MARITAL_STATUS_OPTIONS.index(status) + else: + self.MaritalStatus = status + + def set_working_status(self, status: int | str): + if isinstance(status, str): + self.WorkingStatus = WORKING_STATUS_OPTIONS.index(status) + else: + self.WorkingStatus = status + class UserPatientAccess(Base): __tablename__ = 'UserPatientAccess' From 2eef9f2cc7820c8bfe7f7f375aeb56931a9a71bc Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 14:27:48 +0930 Subject: [PATCH 006/105] Update marital and working status on report form --- client/src/components/GenerateReportForm.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/components/GenerateReportForm.js b/client/src/components/GenerateReportForm.js index abd69ee1..8b46aaa1 100644 --- a/client/src/components/GenerateReportForm.js +++ b/client/src/components/GenerateReportForm.js @@ -66,10 +66,10 @@ const GenerateReportForm = () => { const [alertApLowRequired, setAlertApLowRequired] = useState(false); const [apHigh, setApHigh] = useState(null); const [alertApHighRequired, setAlertApHighRequired] = useState(false); - const [maritalStatus, setMaritalStatus] = useState(null); + const [maritalStatus, setMaritalStatus] = useState(""); const [alertMaritalStatusRequired, setAlertMaritalStatusRequired] = useState(false); - const [workingStatus, setWorkingStatus] = useState(null); + const [workingStatus, setWorkingStatus] = useState(""); const [alertWorkingStatusRequired, setAlertWorkingStatusRequired] = useState(false); const [bloodGlucoseInput, setBloodGlucoseInput] = useState(""); @@ -92,6 +92,8 @@ const GenerateReportForm = () => { setHeight({ isValid: true, value: data.height }); setGender(data.gender); setAge({ isValid: true, value: data.age }); + setMaritalStatus(data.maritalStatus); + setWorkingStatus(data.workingStatus); } catch (err) { console.log("Failed to fetch patient data."); } From 3d19fe0bde96abbca976abd5e1d6a0814350e762 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 17:09:00 +0930 Subject: [PATCH 007/105] Add docstring to model methods --- server/models/dbmodels.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/server/models/dbmodels.py b/server/models/dbmodels.py index af208d63..0f42f46d 100644 --- a/server/models/dbmodels.py +++ b/server/models/dbmodels.py @@ -167,24 +167,30 @@ def get_marital_status_id(self): return self.MaritalStatus def get_marital_status_title(self): + """Returns marital status as a string.""" if (self.MaritalStatus is not None and self.MaritalStatus < len(MARITAL_STATUS_OPTIONS)): return MARITAL_STATUS_OPTIONS[self.MaritalStatus] return None def get_working_status_title(self): + """Returns working status as a string.""" if (self.WorkingStatus is not None and self.WorkingStatus < len(WORKING_STATUS_OPTIONS)): return WORKING_STATUS_OPTIONS[self.WorkingStatus] return None def set_marital_status(self, status: int | str): + """Sets the value of the marital status and will convert a string + to the matching integer.""" if isinstance(status, str): self.MaritalStatus = MARITAL_STATUS_OPTIONS.index(status) else: self.MaritalStatus = status def set_working_status(self, status: int | str): + """Sets the value of the working status and will convert a string + to the matching integer.""" if isinstance(status, str): self.WorkingStatus = WORKING_STATUS_OPTIONS.index(status) else: From 65c6d225ac779e295f8aa2ab341066b0664621da Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 17 May 2026 18:28:01 +0930 Subject: [PATCH 008/105] Update create_patient to check for existing records --- server/routers/users.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/server/routers/users.py b/server/routers/users.py index 040ea2ed..effedccc 100644 --- a/server/routers/users.py +++ b/server/routers/users.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import Session from html_sanitizer import Sanitizer import re +from sqlalchemy import func from ..utils.database import get_db from ..utils.audit_log import write_audit_log @@ -657,10 +658,30 @@ async def create_patient(patient: PatientCreationDetails, request: Request, db_c raise HTTPException( status_code=status.HTTP_422_UNPROCESSABLE_ENTITY) + # Sanitize name input and remove whitespace + sanitizer = Sanitizer() + sanitized_given_names = sanitizer.sanitize(patient.given_names).strip() + sanitized_family_name = sanitizer.sanitize(patient.family_name).strip() + + # Check if the patient already exists + existing_patient = db_conn.query(Patient).filter( + func.lower(Patient.GivenNames) == sanitized_given_names.lower(), + func.lower(Patient.FamilyName) == sanitized_family_name.lower(), + Patient.DateOfBirth == patient.date_of_birth, + Patient.Gender == gender_map[patient.gender] + ).first() + + if existing_patient: + raise HTTPException( + status_code=status.HTTP_409_CONFLICT, + detail="Patient already exists." + ) + # Create new patient new_patient = Patient(user_id=None, - given_names=patient.given_names, - family_name=patient.family_name, gender=gender_map[patient.gender], + given_names=sanitized_given_names, + family_name=sanitized_family_name, + gender=gender_map[patient.gender], date_of_birth=patient.date_of_birth, weight=patient.weight, height=patient.height) From 8c2f1cee2c61fef3959a230f9a4422634b45a982 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 17 May 2026 19:36:23 +0930 Subject: [PATCH 009/105] Implement test for checking duplicate patient creation --- server/tests/routers/test_users.py | 83 ++++++++++++++++++++++++++++-- 1 file changed, 80 insertions(+), 3 deletions(-) diff --git a/server/tests/routers/test_users.py b/server/tests/routers/test_users.py index 355b68da..089218e3 100644 --- a/server/tests/routers/test_users.py +++ b/server/tests/routers/test_users.py @@ -126,6 +126,19 @@ def setup_once_for_all_tests(): db_conn.commit() +# Removes patient record after a test +@pytest.fixture +def cleanup_patient(): + db_conn = next(get_db()) + created_ids = [] + yield created_ids + for patient_id in created_ids: + db_conn.query(UserPatientAccess).filter( + UserPatientAccess.PatientID == patient_id).delete() + db_conn.query(Patient).filter(Patient.PatientID == patient_id).delete() + db_conn.commit() + + def test_delete_requires_authentication(): fresh = TestClient(app) res = fresh.delete("/users/") @@ -244,7 +257,7 @@ def test_merchant_cannot_access_other_patients(): assert response.status_code == status.HTTP_404_NOT_FOUND -def test_create_patient(): +def test_create_patient(cleanup_patient): # Login as Merchant credentials = {"email": "service@example.com", "password": "thisismypassword"} @@ -265,6 +278,7 @@ def test_create_patient(): assert response.status_code == status.HTTP_200_OK assert response.json()["message"] == 'Patient successfully created.' assert isinstance(response.json()["patient_id"], int) + cleanup_patient.append(response.json()["patient_id"]) def test_create_patient_non_merchant(): @@ -359,7 +373,7 @@ def test_create_patient_invalid_height(): assert response.status_code == status.HTTP_422_UNPROCESSABLE_ENTITY -def test_remove_patient(): +def test_remove_patient(cleanup_patient): # Login as Merchant credentials = {"email": "service@example.com", "password": "thisismypassword"} @@ -368,7 +382,7 @@ def test_remove_patient(): assert login_res.json() == {'message': f'Successfully logged in.'} # Patient Details patient = { - "given_names": "Timmy", + "given_names": "John", "family_name": "Smith", "date_of_birth": "1988-04-04", "gender": "Male", @@ -380,6 +394,7 @@ def test_remove_patient(): assert response.status_code == status.HTTP_200_OK assert response.json()["message"] == "Patient successfully created." assert isinstance(response.json()["patient_id"], int) + cleanup_patient.append(response.json()["patient_id"]) response = client.delete( f"/remove-patient/{response.json()["patient_id"]}") @@ -387,6 +402,68 @@ def test_remove_patient(): assert response.json() == {'message': f'Patient successfully removed.'} +def test_create_duplicate_patient(cleanup_patient): + # Login as Merchant + credentials = {"email": "service@example.com", + "password": "thisismypassword"} + login_res = client.post("/login/", json=credentials) + assert login_res.status_code == status.HTTP_200_OK + assert login_res.json() == {'message': f'Successfully logged in.'} + # Patient Details + patient = { + "given_names": "Timmy", + "family_name": "Smith", + "date_of_birth": "1988-04-04", + "gender": "Male", + "weight": 80, + "height": 180 + } + # Create Patient + response = client.post("/create-patient", json=patient) + assert response.status_code == status.HTTP_200_OK + assert isinstance(response.json()["patient_id"], int) + cleanup_patient.append(response.json()["patient_id"]) + + # Attempt to create the patient record again + response = client.post("/create-patient", json=patient) + assert response.status_code == status.HTTP_409_CONFLICT + + +def test_create_duplicate_patient_whitespace(cleanup_patient): + # Login as Merchant + credentials = {"email": "service@example.com", + "password": "thisismypassword"} + login_res = client.post("/login/", json=credentials) + assert login_res.status_code == status.HTTP_200_OK + assert login_res.json() == {'message': f'Successfully logged in.'} + # Patient Details + patient = { + "given_names": "Timmy", + "family_name": "Smith", + "date_of_birth": "1988-04-04", + "gender": "Male", + "weight": 80, + "height": 180 + } + whitespace_patient = { + "given_names": " Timmy", + "family_name": " Smith", + "date_of_birth": "1988-04-04", + "gender": "Male", + "weight": 80, + "height": 180 + } + # Create Patient + response = client.post("/create-patient", json=patient) + assert response.status_code == status.HTTP_200_OK + assert isinstance(response.json()["patient_id"], int) + cleanup_patient.append(response.json()["patient_id"]) + + # Attempt to create the patient record again using the same patient names with whitespace + response = client.post("/create-patient", json=whitespace_patient) + assert response.status_code == status.HTTP_409_CONFLICT + + class TestPatientAccessRequestFlow(): def test_successful_request(self): From e905b549861dec6f95feed07d4a13261c89cb32e Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 17 May 2026 19:45:12 +0930 Subject: [PATCH 010/105] Fix create patient form always return success --- client/src/components/authentication/CreatePatientForm.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/client/src/components/authentication/CreatePatientForm.js b/client/src/components/authentication/CreatePatientForm.js index d5c9473a..cfb31db7 100644 --- a/client/src/components/authentication/CreatePatientForm.js +++ b/client/src/components/authentication/CreatePatientForm.js @@ -168,12 +168,12 @@ const CreatePatientForm = () => { .then((response) => { if (!response.ok) { setShowFailMessage(true); + setShowSuccessMessage(false); + return } - return response.json(); - }) - .then((data) => { setShowSuccessMessage(true); setShowFailMessage(false); + return response.json(); }) .catch((err) => { console.log("An error has occurred"); From 8fc3312cf559bcbcea888fe9d4448b27d17b716c Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 20:12:08 +0930 Subject: [PATCH 011/105] Change status types to dictionaries --- server/models/dbmodels.py | 41 ++++++++++++++++++--------------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/server/models/dbmodels.py b/server/models/dbmodels.py index 0f42f46d..668a3aeb 100644 --- a/server/models/dbmodels.py +++ b/server/models/dbmodels.py @@ -2,13 +2,17 @@ from sqlalchemy.orm import declarative_base, relationship import enum -MARITAL_STATUS_OPTIONS = ['Single', 'Married'] -WORKING_STATUS_OPTIONS = [ - 'Unemployed', - 'Private', - 'Student', - 'Public', -] +MARITAL_STATUS_OPTIONS = { + 0: 'Single', + 1: 'Married', +} + +WORKING_STATUS_OPTIONS = { + 0: 'Unemployed', + 1: 'Private', + 2: 'Student', + 3: 'Public', +} Base = declarative_base() @@ -163,28 +167,20 @@ def __repr__(self): familyName={self.FamilyName}, gender={self.Gender}, weight={self.Weight}, height={self.Height}, \ dateOfBirth={self.DateOfBirth}, Created={self.CreatedAt})' - def get_marital_status_id(self): - return self.MaritalStatus - - def get_marital_status_title(self): + def get_marital_status(self): """Returns marital status as a string.""" - if (self.MaritalStatus is not None - and self.MaritalStatus < len(MARITAL_STATUS_OPTIONS)): - return MARITAL_STATUS_OPTIONS[self.MaritalStatus] - return None + return MARITAL_STATUS_OPTIONS.get(self.MaritalStatus) - def get_working_status_title(self): + def get_working_status(self): """Returns working status as a string.""" - if (self.WorkingStatus is not None - and self.WorkingStatus < len(WORKING_STATUS_OPTIONS)): - return WORKING_STATUS_OPTIONS[self.WorkingStatus] - return None + return WORKING_STATUS_OPTIONS.get(self.WorkingStatus) def set_marital_status(self, status: int | str): """Sets the value of the marital status and will convert a string to the matching integer.""" if isinstance(status, str): - self.MaritalStatus = MARITAL_STATUS_OPTIONS.index(status) + self.MaritalStatus = next( + (k for k, v in MARITAL_STATUS_OPTIONS.items() if v == status), None) else: self.MaritalStatus = status @@ -192,7 +188,8 @@ def set_working_status(self, status: int | str): """Sets the value of the working status and will convert a string to the matching integer.""" if isinstance(status, str): - self.WorkingStatus = WORKING_STATUS_OPTIONS.index(status) + self.WorkingStatus = next( + (k for k, v in WORKING_STATUS_OPTIONS.items() if v == status), None) else: self.WorkingStatus = status From cc2e76bcbc2f2e38740ea47c7e52d93cbf66961e Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 20:12:59 +0930 Subject: [PATCH 012/105] Return marital and working status for merchant patients --- server/routers/users.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/server/routers/users.py b/server/routers/users.py index d4c9597f..43e1cde7 100644 --- a/server/routers/users.py +++ b/server/routers/users.py @@ -992,8 +992,8 @@ async def get_patient_data(request: Request, db_conn: Session = Depends(get_db)) "height": float(patient.Height) if patient.Height else None, "gender": get_gender(patient.Gender), "age": get_age(patient.DateOfBirth), - "maritalStatus": patient.get_marital_status_title(), - "workingStatus": patient.get_working_status_title(), + "maritalStatus": patient.get_marital_status(), + "workingStatus": patient.get_working_status(), } return result @@ -1031,7 +1031,9 @@ async def get_merchant_patient_data(patient_id: int, request: Request, db_conn: "weight": float(patient.Weight) if patient.Weight else None, "height": float(patient.Height) if patient.Height else None, "gender": get_gender(patient.Gender), - "age": get_age(patient.DateOfBirth) + "age": get_age(patient.DateOfBirth), + "maritalStatus": patient.get_marital_status(), + "workingStatus": patient.get_working_status(), } return result From 6c1902b3b3a2e7e4b264c83c0fabec03950bcf80 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 20:13:42 +0930 Subject: [PATCH 013/105] Update marital and working status for merchant reports --- client/src/components/MerchantReportForm.js | 6 ++++-- server/routers/health_prediction.py | 2 ++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/components/MerchantReportForm.js b/client/src/components/MerchantReportForm.js index f34fdfcd..94c62652 100644 --- a/client/src/components/MerchantReportForm.js +++ b/client/src/components/MerchantReportForm.js @@ -82,10 +82,10 @@ const MerchantReportForm = () => { const [alertApLowRequired, setAlertApLowRequired] = useState(false); const [apHigh, setApHigh] = useState(null); const [alertApHighRequired, setAlertApHighRequired] = useState(false); - const [maritalStatus, setMaritalStatus] = useState(null); + const [maritalStatus, setMaritalStatus] = useState(""); const [alertMaritalStatusRequired, setAlertMaritalStatusRequired] = useState(false); - const [workingStatus, setWorkingStatus] = useState(null); + const [workingStatus, setWorkingStatus] = useState(""); const [alertWorkingStatusRequired, setAlertWorkingStatusRequired] = useState(false); const [alertPatientRequired, setAlertPatientRequired] = useState(false); @@ -142,6 +142,8 @@ const MerchantReportForm = () => { setHeight({ isValid: true, value: data.height }); setGender(data.gender); setAge({ isValid: true, value: data.age }); + setMaritalStatus(data.maritalStatus); + setWorkingStatus(data.workingStatus); } catch (err) { console.log("Failed to fetch patient data."); } diff --git a/server/routers/health_prediction.py b/server/routers/health_prediction.py index 7abc622b..4f9cfc80 100644 --- a/server/routers/health_prediction.py +++ b/server/routers/health_prediction.py @@ -392,6 +392,8 @@ async def merchant_predict(data: MerchantHealthDataInput, request: Request, db_c # Update Weight & Height based on sanitized input. patient.Weight = sanitized_data["weight"] patient.Height = sanitized_data["height"] + patient.set_marital_status(sanitized_data["marital_status"]) + patient.set_working_status(sanitized_data["working_status"]) # Check if the merchant has permission to view the patients record if (merchant_view_patient(merchant.UserID, patient.PatientID, db_conn) == False): From 2e2a6f6523d6f60da4664740f3f0370f7edcaaf6 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 20:37:56 +0930 Subject: [PATCH 014/105] Give marital and work status default values --- server/models/dbmodels.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/server/models/dbmodels.py b/server/models/dbmodels.py index 668a3aeb..38fe314d 100644 --- a/server/models/dbmodels.py +++ b/server/models/dbmodels.py @@ -149,9 +149,18 @@ class Patient(Base): health_records = relationship("HealthData", back_populates="patient") user_access = relationship("UserPatientAccess", back_populates="patient") - def __init__(self, user_id, given_names, - family_name, gender, weight, height, - date_of_birth, marital_status, working_status): + def __init__( + self, + user_id, + given_names, + family_name, + gender, + weight, + height, + date_of_birth, + marital_status=None, + working_status=None + ): self.UserID = user_id self.GivenNames = given_names self.FamilyName = family_name From c35552649c1d5aead135442146736d64168e667e Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Sun, 17 May 2026 20:38:31 +0930 Subject: [PATCH 015/105] test: Fix tests checking patient keys --- server/tests/routers/test_users.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/server/tests/routers/test_users.py b/server/tests/routers/test_users.py index 355b68da..aa7f76ea 100644 --- a/server/tests/routers/test_users.py +++ b/server/tests/routers/test_users.py @@ -189,7 +189,14 @@ def test_get_patient_data_returns_correct_fields(): data = response.json() assert response.status_code == status.HTTP_200_OK - assert data.keys() == {"weight", "height", "gender", "age"} + assert data.keys() == { + "workingStatus", + "maritalStatus", + "weight", + "height", + "gender", + "age" + } assert data["gender"] == "Male" assert data["age"] == datetime.today().year - 1980 - \ ((datetime.today().month, datetime.today().day) < (5, 24)) @@ -227,7 +234,14 @@ def test_merchant_get_patient_data_returns_correct_fields(): data = response.json() assert response.status_code == status.HTTP_200_OK - assert data.keys() == {"weight", "height", "gender", "age"} + assert data.keys() == { + "workingStatus", + "maritalStatus", + "weight", + "height", + "gender", + "age" + } assert data["gender"] == "Male" assert data["age"] == datetime.today().year - 1980 - \ ((datetime.today().month, datetime.today().day) < (5, 24)) From 80402a4f2a125b7d512370ae19643e256a8c1e31 Mon Sep 17 00:00:00 2001 From: wyl2003 <108109393+wyl2003@users.noreply.github.com> Date: Sun, 17 May 2026 22:49:59 +0930 Subject: [PATCH 016/105] remove some text and tab in setting --- client/src/routes/UserSettings.js | 140 +----------------------------- 1 file changed, 1 insertion(+), 139 deletions(-) diff --git a/client/src/routes/UserSettings.js b/client/src/routes/UserSettings.js index e69a95b0..753b7655 100644 --- a/client/src/routes/UserSettings.js +++ b/client/src/routes/UserSettings.js @@ -6,10 +6,7 @@ import { ListItem, ListItemText, TextField, - Divider, Button, - Switch, - FormControlLabel, FormControl, InputLabel, Select, @@ -59,16 +56,6 @@ const UserSettings = () => { const [passwordChanged, setPasswordChanged] = useState(false); - // Notification state - const [notifications, setNotifications] = useState({ - emailNotifications: true, - smsNotifications: false, - pushNotifications: true, - healthReminders: true, - reportUpdates: true, - systemAlerts: false, - }); - const [saveMessage, setSaveMessage] = useState(""); const [saveError, setSaveError] = useState(""); const [profileErrors, setProfileErrors] = useState({}); @@ -161,12 +148,6 @@ const UserSettings = () => { const updateForm = (k, v) => setFormData((p) => ({ ...p, [k]: v })); const updatePwd = (k, v) => setPasswordData((p) => ({ ...p, [k]: v })); - const updateNotify = (k, v) => setNotifications((p) => ({ ...p, [k]: v })); - - const handleSave = (section) => { - setSaveMessage(`${section} saved successfully!`); - setTimeout(() => setSaveMessage(""), 2500); - }; const validateProfile = () => { const errors = {}; @@ -324,13 +305,6 @@ const UserSettings = () => { flexWrap: "wrap", }} > - - Danger Zone - - - - - ); - const handleTabChange = (event, newValue) => { setSelectedSection(newValue); }; @@ -715,8 +580,6 @@ const UserSettings = () => { return Profile(); case "Password": return Password(); - case "Notifications": - return Notifications(); default: return null; } @@ -758,7 +621,6 @@ const UserSettings = () => { - ) : ( @@ -776,7 +638,7 @@ const UserSettings = () => { - {["Account Details", "Profile", "Password", "Notifications"].map( + {["Account Details", "Profile", "Password"].map( (item) => ( Date: Sun, 17 May 2026 23:01:27 +0930 Subject: [PATCH 017/105] fix: user dashboard percentage card displaying newest --- client/src/routes/UserLanding.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/routes/UserLanding.js b/client/src/routes/UserLanding.js index 82c2ae87..8e0d0fef 100644 --- a/client/src/routes/UserLanding.js +++ b/client/src/routes/UserLanding.js @@ -157,7 +157,7 @@ const UserLanding = ({}) => { lineHeight: 1.2, }} > - {data?.risks?.[key]?.slice(-1)[0] ?? 0}% + {data?.risks?.[key]?.[0] ?? 0}% Date: Sun, 17 May 2026 23:05:03 +0930 Subject: [PATCH 018/105] fix: display last data in Patient Details page --- client/src/routes/PatientDetails.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/routes/PatientDetails.js b/client/src/routes/PatientDetails.js index d56ca32d..b6b99d1e 100644 --- a/client/src/routes/PatientDetails.js +++ b/client/src/routes/PatientDetails.js @@ -218,7 +218,7 @@ const PatientDetails = () => { {key.toUpperCase()} - {patientData?.risks?.[key]?.slice(-1)[0] ?? 0}% + {patientData?.risks?.[key]?.[0] ?? 0}% Date: Sun, 17 May 2026 23:30:31 +0930 Subject: [PATCH 019/105] Add authentication to the backend API for changing user roles Log unauthorized user role change attempts Prevent administrators from changing their own roles to avoid losing access privileges --- server/routers/admin.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/server/routers/admin.py b/server/routers/admin.py index 76396b3e..67412aa1 100644 --- a/server/routers/admin.py +++ b/server/routers/admin.py @@ -189,6 +189,32 @@ async def get_users( async def update_user_role(user_email: str, role_id: int, request: Request, db_conn: Session = Depends(get_db)): """Update a user's role""" + # Authenticate the requesting user. + current_user_data = get_current_user(request, db_conn) + requesting_user_email = current_user_data.get('email') + + # Verify the requesting user is an administrator. + admin_user = db_conn.query(UserAccount).filter( + UserAccount.Email == requesting_user_email).first() + if not admin_user: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not identify the requesting user.") + + admin_role = db_conn.query(AccountRole).join( + UserAccountRole, AccountRole.RoleID == UserAccountRole.RoleID + ).filter(UserAccountRole.UserID == admin_user.UserID).first() + + if not admin_role or admin_role.RoleName.lower() != 'admin': + write_audit_log(db_conn, + eventType=LogEventType.ROLE_CHANGED, + success=False, + userEmail=requesting_user_email, + device=request.headers.get("user-agent"), + ipAddress=request.client.host, + description=f"Unauthorized role change attempt for account: {user_email}.") + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, + detail="You do not have permission to change user roles.") + # Check if both the user and role exist. user = db_conn.query(UserAccount).filter( UserAccount.Email == user_email).first() @@ -196,6 +222,11 @@ async def update_user_role(user_email: str, role_id: int, request: Request, raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="User not found.") + # Prevent admin from changing their own role. + if admin_user.UserID == user.UserID: + raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, + detail="Administrators cannot change their own role.") + role = db_conn.query(AccountRole).filter( AccountRole.RoleID == role_id).first() if not role: @@ -214,16 +245,10 @@ async def update_user_role(user_email: str, role_id: int, request: Request, db_conn.commit() - actor_email = None - try: # After resolving the authentication issue for this endpoint, this exception handling should be removed - actor_email = get_current_user(request, db_conn).get('email') - except Exception: - pass - write_audit_log(db_conn, eventType=LogEventType.ROLE_CHANGED, success=True, - userEmail=actor_email, + userEmail=requesting_user_email, device=request.headers.get("user-agent"), ipAddress=request.client.host, description=f"Role changed for {user_email} to {role.RoleName}.") From b009aabc5a19f89b91e578aec29dc3ca9218080c Mon Sep 17 00:00:00 2001 From: wyl2003 <108109393+wyl2003@users.noreply.github.com> Date: Mon, 18 May 2026 00:53:53 +0930 Subject: [PATCH 020/105] fix: role change frontend add include credentials --- client/src/components/administrator/UserManagementTable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/components/administrator/UserManagementTable.js b/client/src/components/administrator/UserManagementTable.js index ca06daca..d3c72852 100644 --- a/client/src/components/administrator/UserManagementTable.js +++ b/client/src/components/administrator/UserManagementTable.js @@ -130,6 +130,7 @@ const UserManagementTable = () => { { method: "PATCH", headers: { "Content-Type": "application/json" }, + credentials: "include", }, ); if (!response.ok) throw new Error(response.status); From 6f437bc6327dc545f1ed4076554cb630786c0fd0 Mon Sep 17 00:00:00 2001 From: wyl2003 <108109393+wyl2003@users.noreply.github.com> Date: Mon, 18 May 2026 23:56:47 +0930 Subject: [PATCH 021/105] Remove FALLBACK text from prediction text --- server/services/health_recommendation_service.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/services/health_recommendation_service.py b/server/services/health_recommendation_service.py index c67afb1d..8c0b1041 100644 --- a/server/services/health_recommendation_service.py +++ b/server/services/health_recommendation_service.py @@ -172,7 +172,7 @@ def _fallback_recommendations(ctx: Dict): if "high cholesterol" in conditions: diet += "Increase soluble fiber (oats, legumes) and healthy fats (olive oil, nuts); reduce saturated fats. " - lifestyle = "[FALLBACK] Sleep 7–9 hours nightly, manage stress with short daily breathing or mindfulness. Hydrate adequately. " + lifestyle = "Sleep 7–9 hours nightly, manage stress with short daily breathing or mindfulness. Hydrate adequately. " if "smoking" in conditions: lifestyle += "Begin a smoking cessation plan (nicotine replacement or counseling). " if "alcohol use" in conditions: From 9adcd6429c7d2d7e9f27f132e0e7a5614ed4efa3 Mon Sep 17 00:00:00 2001 From: wyl2003 <108109393+wyl2003@users.noreply.github.com> Date: Tue, 19 May 2026 01:03:19 +0930 Subject: [PATCH 022/105] doc: add docstring for health_prediction.py --- server/routers/health_prediction.py | 42 ++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/server/routers/health_prediction.py b/server/routers/health_prediction.py index 4f9cfc80..c869c577 100644 --- a/server/routers/health_prediction.py +++ b/server/routers/health_prediction.py @@ -76,6 +76,16 @@ class MerchantHealthDataInput(CamelModel): def build_model_input_df(model, values): + """ + Construct a single-row pandas DataFrame from model features and input values. + + Uses the model's ``feature_names_in_`` attribute as column headers when available, + falling back to a positional layout if the attribute is absent. + + :param model: A fitted scikit-learn model (expected to expose ``feature_names_in_``). + :param values: A list of numeric values corresponding to the model's feature set. + :return: A pandas DataFrame with one row suitable for ``model.predict_proba()``. + """ feature_names = getattr(model, "feature_names_in_", None) if feature_names is not None and len(feature_names) == len(values): return pd.DataFrame([values], columns=list(feature_names)) @@ -85,7 +95,21 @@ def build_model_input_df(model, values): @router.post("/health-prediction/") async def predict(data: HealthDataInput, request: Request, db_conn: Session = Depends(get_db), csv_patient_id: Optional[int] = None): - + """ + Generate cardio, stroke, and diabetes risk predictions for the authenticated user. + + Accepts health metrics via ``HealthDataInput``, sanitises and validates the data, + calculates BMI, runs three ML prediction models, persists health data and predictions + to the database, and generates AI-powered health recommendations (best-effort). + + :param data: Health metrics including vitals, lifestyle choices, and medical history. + :param request: The HTTP request object (used for audit logging and user extraction). + :param db_conn: Database session provided by the FastAPI dependency. + :param csv_patient_id: Optional patient ID forwarded from CSV bulk upload flow. + :return: JSON with ``cardioProbability``, ``strokeProbability``, ``diabetesProbability``, + and a ``recommendations`` block. + :raises HTTPException 422: If sanitised health data fails validation. + """ # Sanitize and normalize health data sanitized_data = sanitize_health_data(data) @@ -541,66 +565,82 @@ async def merchant_predict(data: MerchantHealthDataInput, request: Request, db_c def is_age_valid(age: int): + """Check whether ``age`` falls within the valid physiological range (0–100).""" return age >= 0 and age <= 100 def is_weight_valid(weight: float): + """Check whether ``weight`` (kg) falls within the valid range (0.0–200.0).""" return weight >= 0.0 and weight <= 200.0 def is_height_valid(height: float): + """Check whether ``height`` (cm) falls within the valid range (0.0–300.0).""" return height >= 0.0 and height <= 300 def is_gender_valid(gender: int): + """Check whether ``gender`` is present in the predefined mapping.""" return gender in gender_map def is_blood_glucose_valid(blood_glucose: float): + """Check whether ``blood_glucose`` (mmol/L) falls within the valid range (0.0–20.0).""" return blood_glucose >= 0.0 and blood_glucose <= 20.0 def is_ap_hi_valid(ap_hi: float): + """Check whether systolic blood pressure ``ap_hi`` (mmHg) is valid (0.0–200.0).""" return ap_hi >= 0.0 and ap_hi <= 200.0 def is_ap_lo_valid(ap_lo: float): + """Check whether diastolic blood pressure ``ap_lo`` (mmHg) is valid (0.0–200.0).""" return ap_lo >= 0.0 and ap_lo <= 200.0 def is_high_cholesterol_valid(high_cholesterol: int): + """Check whether ``high_cholesterol`` is a binary flag (0 or 1).""" return high_cholesterol == 0 or high_cholesterol == 1 def is_hyper_tension_valid(hypertension: int): + """Check whether ``hypertension`` is a binary flag (0 or 1).""" return hypertension == 0 or hypertension == 1 def is_heart_disease_valid(heart_disease: int): + """Check whether ``heart_disease`` is a binary flag (0 or 1).""" return heart_disease == 0 or heart_disease == 1 def is_diabetes_valid(diabetes: int): + """Check whether ``diabetes`` is a binary flag (0 or 1).""" return diabetes == 0 or diabetes == 1 def is_alcohol_valid(alcohol: int): + """Check whether ``alcohol`` is a binary flag (0 or 1).""" return alcohol == 0 or alcohol == 1 def is_smoker_valid(smoker: str): + """Check whether ``smoker`` is present in the predefined smoking-status mapping.""" return smoker in smoker_map def is_marital_status_valid(marital_status: str): + """Check whether ``marital_status`` is present in the predefined mapping.""" return marital_status in marital_map def is_working_status_valid(working_status: str): + """Check whether ``working_status`` is present in the predefined mapping.""" return working_status in working_map def is_stroke_valid(stroke: int): + """Check whether ``stroke`` is a binary flag (0 or 1).""" return stroke == 0 or stroke == 1 From 6804cb72ef677df038cb16ddd65e3167f251530b Mon Sep 17 00:00:00 2001 From: wyl2003 <108109393+wyl2003@users.noreply.github.com> Date: Tue, 19 May 2026 02:10:01 +0930 Subject: [PATCH 023/105] doc: add docstring for admin.py --- server/routers/admin.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/server/routers/admin.py b/server/routers/admin.py index 67412aa1..174d921f 100644 --- a/server/routers/admin.py +++ b/server/routers/admin.py @@ -536,6 +536,7 @@ async def get_logs( @router.get("/admin-dashboard/active-account-analytics") async def get_active_account_analytics(request: Request, db_conn: Session = Depends(get_db)): + """Return counts of active standard-user accounts in the past month and week.""" _confirm_admin(request, db_conn) prev_month = datetime.now() - timedelta(days=30) prev_week = datetime.now() - timedelta(days=7) @@ -570,6 +571,7 @@ async def get_active_account_analytics(request: Request, db_conn: Session = Depe @router.get("/admin-dashboard/active-merchant-analytics") async def get_active_merchant_analytics(request: Request, db_conn: Session = Depends(get_db)): + """Return counts of active merchant accounts in the past month and week.""" _confirm_admin(request, db_conn) prev_month = datetime.now() - timedelta(days=30) prev_week = datetime.now() - timedelta(days=7) @@ -604,6 +606,7 @@ async def get_active_merchant_analytics(request: Request, db_conn: Session = Dep @router.get("/admin-dashboard/recent-reports-generated-analytics") async def get_reports_generated_analytics(request: Request, db_conn: Session = Depends(get_db)): + """Return counts of health reports generated in the past month and week.""" _confirm_admin(request, db_conn) prev_month = datetime.now() - timedelta(days=30) prev_week = datetime.now() - timedelta(days=7) @@ -628,6 +631,7 @@ async def get_reports_generated_analytics(request: Request, db_conn: Session = D @router.get("/admin-dashboard/pending-merchants-analytics") async def get_pending_merchant_analytics(request: Request, db_conn: Session = Depends(get_db)): + """Return the number of merchant accounts awaiting validation.""" _confirm_admin(request, db_conn) pending_merchants = ( @@ -649,6 +653,7 @@ async def get_pending_merchant_analytics(request: Request, db_conn: Session = De @router.get("/admin-dashboard/predictions-distinct-years") async def get_predictions_distinct_years(request: Request, db_conn: Session = Depends(get_db)): + """Return a sorted list of distinct years in which predictions exist.""" _confirm_admin(request, db_conn) query = db_conn.query( extract("year", Prediction.CreatedAt).label("year") @@ -658,6 +663,15 @@ async def get_predictions_distinct_years(request: Request, db_conn: Session = De @router.get("/admin-dashboard/ave-risk-series/{year}") async def get_average_risk_series(year: int, request: Request, db_conn: Session = Depends(get_db)): + """ + Return monthly average risk probabilities (stroke, diabetes, CVD) for a given year. + + :param year: The calendar year to aggregate risk data for (e.g. 2025). + :param request: The HTTP request object (used for admin authentication). + :param db_conn: Database session provided by the FastAPI dependency. + :return: A list of dicts, each with ``date`` (YYYY-MM) and average ``stroke``, + ``diabetes``, ``cvd`` values. + """ _confirm_admin(request, db_conn) year_start = datetime(year, 1, 1) @@ -678,6 +692,7 @@ async def get_average_risk_series(year: int, request: Request, db_conn: Session @router.get("/admin-dashboard/login-activity/{timespanInDays}") async def get_login_activity(timespanInDays: int, request: Request, db_conn: Session = Depends(get_db)): + """Return daily login event counts over the specified number of days.""" _confirm_admin(request, db_conn) start_date = datetime.now() - timedelta(days=timespanInDays) @@ -695,6 +710,7 @@ async def get_login_activity(timespanInDays: int, request: Request, db_conn: Ses @router.get("/admin-dashboard/unvalidated-account-analytics") async def get_unvalidated_account_analytics(request: Request, db_conn: Session = Depends(get_db)): + """Return the number of standard-user accounts that have not been validated.""" _confirm_admin(request, db_conn) unvalidated_accounts = ( @@ -715,6 +731,7 @@ async def get_unvalidated_account_analytics(request: Request, db_conn: Session = @router.get("/admin-dashboard/user-analytics") async def get_user_analytics(request: Request, db_conn: Session = Depends(get_db)): + """Return aggregated counts of total, standard, patient-only, and merchant accounts.""" _confirm_admin(request, db_conn) account_total = db_conn.query(UserAccount).filter( @@ -741,6 +758,7 @@ async def get_user_analytics(request: Request, db_conn: Session = Depends(get_db def _confirm_admin(request: Request, db_conn: Session): + """Verify the requesting user has an admin role; raises 403/404 otherwise.""" user = get_current_user(request, db_conn) admin = db_conn.query(UserAccount).filter( UserAccount.Email == user["email"]).first() From 069922357af03838f65ef2dd6ea6f8e6a0ded552 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Tue, 19 May 2026 13:14:52 +0930 Subject: [PATCH 024/105] Add value property to display existing phone numbers --- client/src/components/authentication/PhoneInputField.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/client/src/components/authentication/PhoneInputField.js b/client/src/components/authentication/PhoneInputField.js index 500ecdb2..5f593e81 100644 --- a/client/src/components/authentication/PhoneInputField.js +++ b/client/src/components/authentication/PhoneInputField.js @@ -14,8 +14,10 @@ import { * @param {function} [props.onChange] - Callback function called when input * is changed. */ -const PhoneInputField = ({ onChange }) => { - const [rawPhoneNumber, setRawPhoneNumber] = useState(""); +const PhoneInputField = ({ onChange, value }) => { + const [rawPhoneNumber, setRawPhoneNumber] = useState( + value === null ? value : "", + ); const [selectedDialingCode, setSelectedDialingCode] = useState(""); const [isValid, setIsValid] = useState(true); const [isValidDialingCode, setIsValidDialingCode] = useState(true); @@ -82,6 +84,7 @@ const PhoneInputField = ({ onChange }) => { onChange?.({ phone: outputPhoneNumber, isValid: true, + rawDigits: phoneNumber, }); } else { setIsValid(false); @@ -89,6 +92,7 @@ const PhoneInputField = ({ onChange }) => { onChange?.({ phone: null, isValid: false, + rawDigits: phoneNumber, }); } } @@ -128,6 +132,7 @@ const PhoneInputField = ({ onChange }) => { id="outlined-input" name="phone" label="Phone" + value={value} onChange={updatePhoneNumber} sx={{ width: "100%" }} > From 15f58d12fb421aba81ee4ae4123ce13d1bccb0a2 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Tue, 19 May 2026 13:17:25 +0930 Subject: [PATCH 025/105] Add clarification to component parameters --- client/src/components/authentication/PhoneInputField.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/components/authentication/PhoneInputField.js b/client/src/components/authentication/PhoneInputField.js index 5f593e81..ce9358ab 100644 --- a/client/src/components/authentication/PhoneInputField.js +++ b/client/src/components/authentication/PhoneInputField.js @@ -11,8 +11,9 @@ import { * selection for a dialling code. * * @param {Object} props - * @param {function} [props.onChange] - Callback function called when input - * is changed. + * @param {function} [props.onChange] - Callback function to call when input is changed. + * Event parameters: phone, isValid, and rawDigits + * @param {String} [props.value] - Phone number */ const PhoneInputField = ({ onChange, value }) => { const [rawPhoneNumber, setRawPhoneNumber] = useState( From 71ad55f140645caf5ad2a096a1f69eca66967f4b Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Tue, 19 May 2026 13:22:04 +0930 Subject: [PATCH 026/105] Replace TextField for PhoneInputField component --- client/src/routes/UserSettings.js | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/client/src/routes/UserSettings.js b/client/src/routes/UserSettings.js index 753b7655..7cd18140 100644 --- a/client/src/routes/UserSettings.js +++ b/client/src/routes/UserSettings.js @@ -21,6 +21,7 @@ import { import ConfirmationDialog from "../components/confirmationDialog"; import { useNavigate } from "react-router-dom"; import { UserContext } from "../utils/UserContext"; +import PhoneInputField from "../components/authentication/PhoneInputField"; const UserSettings = () => { const navigate = useNavigate(); @@ -65,6 +66,9 @@ const UserSettings = () => { const [deleteBusy, setDeleteBusy] = useState(false); const [deleteError, setDeleteError] = useState(""); + const [isPhoneValid, setIsPhoneValid] = useState(true); + const [rawPhoneDigits, setRawPhoneDigits] = useState(""); + // Resolve API base from environment variable const API_BASE = useMemo( () => process.env.REACT_APP_API_URL || "http://localhost:8000", @@ -90,6 +94,7 @@ const UserSettings = () => { height: user.height ?? "", weight: user.weight ?? "", })); + setRawPhoneDigits(user.phone_number || ""); }, [user]); const clearMessages = () => { @@ -190,6 +195,11 @@ const UserSettings = () => { const handleAccountSave = async () => { clearMessages(); + if (!isPhoneValid) { + setSaveError("Please provide a valid phone number."); + return; + } + setAccountSaving(true); try { const phone = formData.phone ?? ""; @@ -276,12 +286,14 @@ const UserSettings = () => { helperText="Email update is currently not supported" fullWidth /> - updateForm("phone", e.target.value)} - helperText="Only digits will be stored" - fullWidth + { + updateForm("phone", e.phone); + setIsPhoneValid(e.isValid); + setRawPhoneDigits(e.rawDigits); + console.log(e); + }} + value={rawPhoneDigits} /> From 45d1146fc24c84decc53a8ed2f48f502c8176d8e Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Tue, 19 May 2026 13:35:52 +0930 Subject: [PATCH 027/105] Fix information leakage issue with logging error --- client/src/routes/UserSettings.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/routes/UserSettings.js b/client/src/routes/UserSettings.js index 7cd18140..82f6fe0a 100644 --- a/client/src/routes/UserSettings.js +++ b/client/src/routes/UserSettings.js @@ -147,7 +147,7 @@ const UserSettings = () => { return response.json(); }) .catch((error) => { - console.log(error); + console.log("Failed to change password"); }); } From b5da5e8b93fbd4f2c8686c022cfad1700d6d835e Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Tue, 19 May 2026 13:38:17 +0930 Subject: [PATCH 028/105] Fix additional information leakage issue --- client/src/routes/UserSettings.js | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/routes/UserSettings.js b/client/src/routes/UserSettings.js index 82f6fe0a..d9bb3f41 100644 --- a/client/src/routes/UserSettings.js +++ b/client/src/routes/UserSettings.js @@ -291,7 +291,6 @@ const UserSettings = () => { updateForm("phone", e.phone); setIsPhoneValid(e.isValid); setRawPhoneDigits(e.rawDigits); - console.log(e); }} value={rawPhoneDigits} /> From dfd61ddbf20dc4db52edf58e01ab1202ad4aa963 Mon Sep 17 00:00:00 2001 From: A-Axisa Date: Tue, 19 May 2026 13:51:01 +0930 Subject: [PATCH 029/105] fix: Update routing to use revised route name --- client/src/routes/HealthAnalytics.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/routes/HealthAnalytics.js b/client/src/routes/HealthAnalytics.js index 39fc7d4b..4126d03f 100644 --- a/client/src/routes/HealthAnalytics.js +++ b/client/src/routes/HealthAnalytics.js @@ -456,7 +456,7 @@ const HealthAnalytics = () => { > + ); }; -export default DisclaimerPolicy; \ No newline at end of file +export default DisclaimerPolicy; diff --git a/client/src/components/DownloadReportButton.js b/client/src/components/DownloadReportButton.js index 43e9b5fa..d7c14104 100644 --- a/client/src/components/DownloadReportButton.js +++ b/client/src/components/DownloadReportButton.js @@ -1,8 +1,8 @@ -import { pdf } from '@react-pdf/renderer'; -import { saveAs } from 'file-saver'; -import HealthReportPDFFlat from './HealthReportPDFFlat'; -import { Button } from '@mui/material'; -import DownloadIcon from '@mui/icons-material/Download'; +import { pdf } from "@react-pdf/renderer"; +import { saveAs } from "file-saver"; +import HealthReportPDFFlat from "./HealthReportPDFFlat"; +import { Button } from "@mui/material"; +import DownloadIcon from "@mui/icons-material/Download"; /** * A simple button component to trigger a PDF report download for a specific health data ID. @@ -16,9 +16,14 @@ import DownloadIcon from '@mui/icons-material/Download'; * @param {object} [props.flatReportData] - Already-fetched flat report data from /getReportData/{id}. * @param {object} [props.meta] - Additional metadata: { date, healthDataID, fileNameHint }. * @param {function} props.onError - Callback function to handle errors. + * @returns {@mui.material.Button} */ -const DownloadReportButton = ({ healthDataId, flatReportData, meta, onError }) => { - +const DownloadReportButton = ({ + healthDataId, + flatReportData, + meta, + onError, +}) => { const handleDownload = async () => { if (!flatReportData && !healthDataId) { onError?.("Health Data ID is required."); @@ -28,24 +33,28 @@ const DownloadReportButton = ({ healthDataId, flatReportData, meta, onError }) = try { // Build local date string for filename to avoid timezone shift const buildLocalDatePart = (value) => { - if (!value) return 'report'; + if (!value) return "report"; const d = new Date(value); - if (Number.isNaN(d.getTime())) return 'report'; + if (Number.isNaN(d.getTime())) return "report"; const y = d.getFullYear(); - const m = String(d.getMonth() + 1).padStart(2, '0'); - const day = String(d.getDate()).padStart(2, '0'); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; }; // Generate from flat data (page already has it) - const fileName = meta?.fileNameHint - || `HealthReport_${String(meta?.healthDataID ?? healthDataId)}_${buildLocalDatePart(meta?.date)}.pdf`; + const fileName = + meta?.fileNameHint || + `HealthReport_${String(meta?.healthDataID ?? healthDataId)}_${buildLocalDatePart(meta?.date)}.pdf`; const blob = await pdf( - + , ).toBlob(); saveAs(blob, fileName); if (onError) onError?.(null); // Clear previous errors on success - } catch (error) { console.error("Failed to download report:", error); if (onError) onError(error.message); @@ -53,7 +62,12 @@ const DownloadReportButton = ({ healthDataId, flatReportData, meta, onError }) = }; return ( - ); diff --git a/client/src/components/GenerateReportForm.js b/client/src/components/GenerateReportForm.js index 8b46aaa1..59987d78 100644 --- a/client/src/components/GenerateReportForm.js +++ b/client/src/components/GenerateReportForm.js @@ -1,5 +1,4 @@ import { useState, useEffect } from "react"; - import { useNavigate } from "react-router-dom"; import { Card, @@ -17,14 +16,18 @@ import { Select, FormHelperText, } from "@mui/material"; - import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; import CheckBoxIcon from "@mui/icons-material/CheckBox"; - import BloodReportUpload from "./BloodReportUpload"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A form that can be filled in by a standard user with their health + * information to generate a health report. + * + * @returns {@mui.material.Card} + */ const GenerateReportForm = () => { const navigate = useNavigate(); diff --git a/client/src/components/HealthReportPDFFlat.js b/client/src/components/HealthReportPDFFlat.js index 48e0b6bf..0312fe5b 100644 --- a/client/src/components/HealthReportPDFFlat.js +++ b/client/src/components/HealthReportPDFFlat.js @@ -1,129 +1,140 @@ -import { Page, Text, View, Document, StyleSheet, Image } from '@react-pdf/renderer'; -import WellAiLogo from '../assets/WellAiLogoTR.png'; +import { + Page, + Text, + View, + Document, + StyleSheet, + Image, +} from "@react-pdf/renderer"; +import WellAiLogo from "../assets/WellAiLogoTR.png"; + +/** + * The document used for converting a health report to a PDF File. + */ // Styles reused for flat report PDF const styles = StyleSheet.create({ - page: { - flexDirection: 'column', - backgroundColor: '#FFFFFF', - paddingTop: 100, // Space for fixed header - paddingBottom: 70, // Space for fixed footer + page: { + flexDirection: "column", + backgroundColor: "#FFFFFF", + paddingTop: 100, // Space for fixed header + paddingBottom: 70, // Space for fixed footer paddingLeft: 40, paddingRight: 40, - fontFamily: 'Helvetica' + fontFamily: "Helvetica", }, watermarkContainer: { - position: 'absolute', + position: "absolute", top: 250, left: 80, right: 80, opacity: 0.08, - justifyContent: 'center', - alignItems: 'center', - zIndex: -1 + justifyContent: "center", + alignItems: "center", + zIndex: -1, }, watermarkImage: { width: 400, height: 400, - objectFit: 'contain' + objectFit: "contain", }, headerContainer: { - position: 'absolute', + position: "absolute", top: 30, left: 40, right: 40, }, headerTop: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-end', + flexDirection: "row", + justifyContent: "space-between", + alignItems: "flex-end", marginBottom: 5, }, headerLeftText: { fontSize: 10, - color: '#888888', - marginBottom: 5 + color: "#888888", + marginBottom: 5, }, logo: { - width: 120, + width: 120, height: 35, - objectFit: 'contain' + objectFit: "contain", }, headerLineContainer: { - position: 'relative', + position: "relative", height: 8, marginBottom: 10, }, headerLine: { - position: 'absolute', + position: "absolute", top: 4, left: 0, right: 0, height: 1, - backgroundColor: '#888888' + backgroundColor: "#888888", }, headerBottom: { - flexDirection: 'row', - justifyContent: 'space-between', + flexDirection: "row", + justifyContent: "space-between", marginTop: 5, }, metaText: { fontSize: 10, - color: '#000', + color: "#000", }, content: { flex: 1, }, reportTitle: { - fontFamily: 'Helvetica-Bold', + fontFamily: "Helvetica-Bold", fontSize: 20, - textAlign: 'center', - textDecoration: 'underline', + textAlign: "center", + textDecoration: "underline", marginBottom: 15, }, sessionTitle: { - fontFamily: 'Helvetica-Oblique', + fontFamily: "Helvetica-Oblique", fontSize: 12, - textDecoration: 'underline', + textDecoration: "underline", marginBottom: 5, }, table: { borderWidth: 1, - borderColor: '#000', + borderColor: "#000", marginBottom: 20, }, tableHeader: { - flexDirection: 'row', - backgroundColor: '#EBEBEB', + flexDirection: "row", + backgroundColor: "#EBEBEB", borderBottomWidth: 1, - borderBottomColor: '#000', + borderBottomColor: "#000", }, tableHeaderCell: { padding: 5, - justifyContent: 'center', - alignItems: 'center', + justifyContent: "center", + alignItems: "center", }, tableHeaderCellText: { - fontFamily: 'Helvetica-Bold', + fontFamily: "Helvetica-Bold", fontSize: 10, - textAlign: 'center', + textAlign: "center", }, tableRow: { - flexDirection: 'row', + flexDirection: "row", borderBottomWidth: 1, - borderBottomColor: '#000', + borderBottomColor: "#000", }, tableCell: { padding: 5, fontSize: 10, - justifyContent: 'center', - alignItems: 'center', - textAlign: 'center', + justifyContent: "center", + alignItems: "center", + textAlign: "center", }, tableCellTitle: { - fontFamily: 'Helvetica-Bold', + fontFamily: "Helvetica-Bold", fontSize: 10, - textAlign: 'center', + textAlign: "center", }, recommendationSection: { marginBottom: 12, @@ -131,10 +142,10 @@ const styles = StyleSheet.create({ recommendationLabel: { fontSize: 10, marginBottom: 2, - fontFamily: 'Helvetica', + fontFamily: "Helvetica", }, bulletRow: { - flexDirection: 'row', + flexDirection: "row", marginBottom: 3, paddingLeft: 20, paddingRight: 10, @@ -142,7 +153,7 @@ const styles = StyleSheet.create({ bulletSign: { width: 12, fontSize: 10, - fontFamily: 'Helvetica', + fontFamily: "Helvetica", }, bulletContent: { flex: 1, @@ -152,187 +163,301 @@ const styles = StyleSheet.create({ disclaimer: { fontSize: 9, lineHeight: 1.3, - textAlign: 'justify', + textAlign: "justify", }, privacyText: { fontSize: 9, }, link: { - color: '#0066cc', - textDecoration: 'underline', + color: "#0066cc", + textDecoration: "underline", }, footerContainer: { - position: 'absolute', + position: "absolute", bottom: 30, left: 40, right: 40, }, footerLine: { height: 1, - backgroundColor: '#888888', + backgroundColor: "#888888", marginBottom: 5, }, footerContent: { - flexDirection: 'row', - justifyContent: 'space-between', + flexDirection: "row", + justifyContent: "space-between", }, footerText: { fontSize: 9, - color: '#888', - } + color: "#888", + }, }); const getRisk = (val) => { const percentage = Number(val || 0); - if (percentage >= 50) return { label: 'HIGH', color: '#FF0000' }; - if (percentage >= 30) return { label: 'MEDIUM', color: '#FFB800' }; - return { label: 'LOW', color: '#00B050' }; + if (percentage >= 50) return { label: "HIGH", color: "#FF0000" }; + if (percentage >= 30) return { label: "MEDIUM", color: "#FFB800" }; + return { label: "LOW", color: "#00B050" }; }; const pct = (v) => `${Number(v || 0).toFixed(1)}%`; const TableRow = ({ index, disease, chance, risk, noBorderBottom }) => ( - - - {index} + + + {index} - - {disease} + + {disease} - - {pct(chance)} + + {pct(chance)} - - {risk.label} + + + {risk.label} + ); const renderParagraph = (text) => { - if (!text) return ( - - - - N/A - - ); - - const lines = text.replace(/\r/g, '').split('\n'); + if (!text) + return ( + + - + N/A + + ); + + const lines = text.replace(/\r/g, "").split("\n"); return lines.map((line, idx) => { let t = line.trim(); if (!t) return null; - if (t.startsWith('-') || t.startsWith('•') || t.startsWith('*')) { + if (t.startsWith("-") || t.startsWith("•") || t.startsWith("*")) { t = t.substring(1).trim(); } return ( - - - {t} + - + {t} ); }); }; -// PDF for flat report data returned from /getReportData/{id} -// Props: { data, metaId, metaDate } +/** + * A document representing a user's health report in a PDF format. + * + * @param {Object} props + * @param {Object} [props.data] - Health data from health report + * @param {Object} [props.metaId] - Health report ID + * @param {Object} [props.metaDate] - Date the health report was generated + * @returns {@react-pdf.renderer.Document} + */ const HealthReportPDFFlat = ({ data, metaId, metaDate }) => { const dateObj = metaDate ? new Date(metaDate) : new Date(); - const dateStr = dateObj.toLocaleDateString('en-GB', { - day: 'numeric', month: 'long', year: 'numeric' + const dateStr = dateObj.toLocaleDateString("en-GB", { + day: "numeric", + month: "long", + year: "numeric", }); - const patientName = (data.patientName || '').trim() || 'Patient'; + const patientName = (data.patientName || "").trim() || "Patient"; return ( {/* Background Watermark */} - + {/* Fixed Header */} - - Predictive Health Report for Early Detection - - - - - - - Name: {patientName} - Report date: {dateStr} - + + + Predictive Health Report for Early Detection + + + + + + + + Name: {patientName} + Report date: {dateStr} + {/* Fixed Footer */} - - - All Copyright Reserved © {dateObj.getFullYear()} - `Page ${pageNumber} of ${totalPages}`} /> - + + + + All Copyright Reserved © {dateObj.getFullYear()} + + + `Page ${pageNumber} of ${totalPages}` + } + /> + {/* Body Content */} - {/* Title */} - Health Prediction Report + {/* Title */} + Health Prediction Report + + {/* Session 1 */} + Session 1 - Analysis Result + + {/* Table Header */} + + + No. + + + + Disease Description + + + + + AI-Powered{"\n"}Prediction Result + + + + + Risk{"\n"}Category + + + + + {/* Rows */} + + + + + + {/* Session 2 */} + + Session 2 – Personal Health Recommendations + - {/* Session 1 */} - Session 1 - Analysis Result - - {/* Table Header */} - - No. - Disease Description - AI-Powered{'\n'}Prediction Result - Risk{'\n'}Category - - - {/* Rows */} - - - - + + Lifestyle: + {renderParagraph( + data.lifestyleRecommendation || data.exerciseRecommendation, + )} + + + + Food Intake: + {renderParagraph(data.dietRecommendation)} + + + + Food to Avoid: + {renderParagraph(data.dietToAvoidRecommendation)} + - {/* Session 2 */} - Session 2 – Personal Health Recommendations - - - Lifestyle: - {renderParagraph(data.lifestyleRecommendation || data.exerciseRecommendation)} - - - - Food Intake: - {renderParagraph(data.dietRecommendation)} - - - - Food to Avoid: - {renderParagraph(data.dietToAvoidRecommendation)} - - - - Exercise Recommendations: - {renderParagraph(data.exerciseRecommendation)} - + + + Exercise Recommendations: + + {renderParagraph(data.exerciseRecommendation)} + {/* Break hint: we want disclaimer to either stay at the end or push to next page. */} - Disclaimer: The information provided by this health prediction report is intended for informational - purposes only and should not be construed as medical advice. Always consult with a qualified - healthcare professional regarding any questions or concerns you may have about your health. Kindly - note that the predictions may not be completely accurate and may not take into account all relevant - factors, and results may vary depending on your individual health history and circumstances. By using - this prediction report, you agree that you understand and accept the limitations of the information - provided. + Disclaimer: The information provided by this health prediction + report is intended for informational purposes only and should not be + construed as medical advice. Always consult with a qualified + healthcare professional regarding any questions or concerns you may + have about your health. Kindly note that the predictions may not be + completely accurate and may not take into account all relevant + factors, and results may vary depending on your individual health + history and circumstances. By using this prediction report, you + agree that you understand and accept the limitations of the + information provided. - + - For our privacy notice, please refer to https://wellai.app/privacy-notice/. + For our privacy notice, please refer to{" "} + + https://wellai.app/privacy-notice/ + + . diff --git a/client/src/components/MerchantReportForm.js b/client/src/components/MerchantReportForm.js index 94c62652..015c328f 100644 --- a/client/src/components/MerchantReportForm.js +++ b/client/src/components/MerchantReportForm.js @@ -11,7 +11,6 @@ import { Button, Autocomplete, } from "@mui/material"; - import OutlinedInput from "@mui/material/OutlinedInput"; import InputLabel from "@mui/material/InputLabel"; import MenuItem from "@mui/material/MenuItem"; @@ -23,11 +22,16 @@ import CheckBoxIcon from "@mui/icons-material/CheckBox"; import FormHelperText from "@mui/material/FormHelperText"; import FileUploadIcon from "@mui/icons-material/FileUpload"; import { styled } from "@mui/material/styles"; - import BloodReportUpload from "./BloodReportUpload"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A form that can be filled in by a merchant with a patients health + * information to generate health report for them. + * + * @returns {@mui.material.Card} + */ const MerchantReportForm = () => { const navigate = useNavigate(); const location = useLocation(); diff --git a/client/src/components/Navbar.js b/client/src/components/Navbar.js index 0e4d35a9..3a9843b6 100644 --- a/client/src/components/Navbar.js +++ b/client/src/components/Navbar.js @@ -38,6 +38,16 @@ import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A navigation bar located on the side of the page that allows quick navigation + * of the service. Different tabs will be displayed based on the current + * user's role and provide access to the appropriate pages. + * The service's disclaimer policy and privacy notice are also located at the bottom. + * + * @param {Object} props + * @param {string} [props.role] - Renders different tabs based on a user's role. + * @returns {<>} + */ const NavBar = ({ role }) => { // Maps each route to the corrosponding page title. const routePageMap = { diff --git a/client/src/components/PrivacyNotice.js b/client/src/components/PrivacyNotice.js index 450d0daa..d28f6c2e 100644 --- a/client/src/components/PrivacyNotice.js +++ b/client/src/components/PrivacyNotice.js @@ -4,122 +4,254 @@ import { DialogContent, DialogTitle, Box, - Button + Button, } from "@mui/material"; - +/** + * A dialog box displaying the privacy notice for the service. + * + * @param {Object} props + * @param {boolean} [props.open] + * @param {function} [props.onClose] - Callback function for closing dialog + * @returns {@mui.material.Dialog} + */ const PrivacyNotice = ({ open, onClose }) => { return ( Privacy Notice - This Privacy Notice is issued by WellAI Sdn Bhd (“WellAI”) to regulate the collection and processing of your Personal Data (as herein defined) in strict accordance with the requirements of the Personal Data Protection Act, Malaysia, 2010 (PDPA). - By interacting with us, submitting information to us, or signing up for any products and services offered by us, you agree and consent to WellAI, as well as its representatives and/or agents collecting, using, disclosing and sharing amongst themselves your Personal Data, and disclosing such Personal Data to our authorised service providers and relevant third parties in the manners set forth in this Privacy Notice. - This Privacy Notice supplements but does not supersede nor replace any other consents you may have previously provided to WellAI in respect of your Personal Data, and your consents herein are additional to any rights which any member of WellAI may have at law to collect, use, or disclose your Personal Data. - Please note that WellAI may amend this Privacy Notice at any time without prior notice and will notify you of any such amendment via our website or by email. + This Privacy Notice is issued by WellAI Sdn Bhd (“WellAI”) to regulate + the collection and processing of your Personal Data (as herein + defined) in strict accordance with the requirements of the Personal + Data Protection Act, Malaysia, 2010 (PDPA). By interacting with us, + submitting information to us, or signing up for any products and + services offered by us, you agree and consent to WellAI, as well as + its representatives and/or agents collecting, using, disclosing and + sharing amongst themselves your Personal Data, and disclosing such + Personal Data to our authorised service providers and relevant third + parties in the manners set forth in this Privacy Notice. This Privacy + Notice supplements but does not supersede nor replace any other + consents you may have previously provided to WellAI in respect of your + Personal Data, and your consents herein are additional to any rights + which any member of WellAI may have at law to collect, use, or + disclose your Personal Data. Please note that WellAI may amend this + Privacy Notice at any time without prior notice and will notify you of + any such amendment via our website or by email. 1. Definitions - 1.1. WellAI: Refers to WellAI Sdn Bhd, its affiliates, subsidiaries, associated entities, including but not limited to any of their branches and offices. - 1.2. Personal Data: Includes information such as name, age, gender, identity card number, passport number, date of birth, education, race, ethnic origin, nationality, contact details, family information, occupation details, medical and personal health information, demographic information, device information, device location, payment card number and expiry date, billing address, loyalty program membership details, photographs, video and audio, CCTV recordings and other images, preferences and interests, financial details, and any other information relevant to services provided. + 1.1. WellAI: Refers to WellAI Sdn Bhd, its affiliates, subsidiaries, + associated entities, including but not limited to any of their + branches and offices. 1.2. Personal Data: Includes information such as + name, age, gender, identity card number, passport number, date of + birth, education, race, ethnic origin, nationality, contact details, + family information, occupation details, medical and personal health + information, demographic information, device information, device + location, payment card number and expiry date, billing address, + loyalty program membership details, photographs, video and audio, CCTV + recordings and other images, preferences and interests, financial + details, and any other information relevant to services provided. - 2. Information Collection + + 2. Information Collection + - 2.1. We may collect Personal Data directly from you or from third parties, including but not limited to online forms, mobile applications, website, email, agreements, name cards, browser, files in media or storage, social medias, or any identity materials that you have distributed voluntarily. - 2.2. Information collected may include personal details, contact details, medical history, device information, and more. Providing obligatory information is necessary for the provision of services. + 2.1. We may collect Personal Data directly from you or from third + parties, including but not limited to online forms, mobile + applications, website, email, agreements, name cards, browser, files + in media or storage, social medias, or any identity materials that you + have distributed voluntarily. 2.2. Information collected may include + personal details, contact details, medical history, device + information, and more. Providing obligatory information is necessary + for the provision of services. - 3. Use of Information + + 3. Use of Information + - 3.1. The collected data may be used for various purposes including but not limited to processing medical services, administering services and events, processing payments, internal analysis, customer loyalty programmes, internal investigations, audit or security purposes, compliance with legal obligations, marketing and communication of relevant information. + 3.1. The collected data may be used for various purposes including but + not limited to processing medical services, administering services and + events, processing payments, internal analysis, customer loyalty + programmes, internal investigations, audit or security purposes, + compliance with legal obligations, marketing and communication of + relevant information. - 4. Disclosure of Information + + 4. Disclosure of Information + - 4.1. Personal Data will be kept confidential but may be disclosed to relevant third parties, including other companies within the WellAI and relevant third parties (in or outside of Malaysia) as required under law, pursuant to the relevant contractual relationship or for purposes mentioned in paragraph 3 above. - 4.2. WellAI may also disclose or transfer Personal Data in the event of any restructuring, sale, or acquisition of the respective companies. - 4.3 Where WellAI deals with third parties, specific security and confidentiality safeguards will be put in place to ensure the personal data protection rights remain unaffected. + 4.1. Personal Data will be kept confidential but may be disclosed to + relevant third parties, including other companies within the WellAI + and relevant third parties (in or outside of Malaysia) as required + under law, pursuant to the relevant contractual relationship or for + purposes mentioned in paragraph 3 above. 4.2. WellAI may also disclose + or transfer Personal Data in the event of any restructuring, sale, or + acquisition of the respective companies. 4.3 Where WellAI deals with + third parties, specific security and confidentiality safeguards will + be put in place to ensure the personal data protection rights remain + unaffected. 5. Your Rights - 5.1. You have the right to request access, correction, updating, or deletion of your Personal Data. Requests can be made through the designated contact points provided including but not limited to online registered account or request to pdpa@wellai.app. - 5.2. WellAI reserve the right to charge a fee for processing data access requests in accordance with PDPA. + 5.1. You have the right to request access, correction, updating, or + deletion of your Personal Data. Requests can be made through the + designated contact points provided including but not limited to online + registered account or request to pdpa@wellai.app. 5.2. WellAI reserve + the right to charge a fee for processing data access requests in + accordance with PDPA. - 6. Withdrawing Consent + + 6. Withdrawing Consent + - 6.1. You are entitled to limit our processing of your personal data by expressly withdrawing in full, your consent given previously, in each case, including for direct marketing purposes subject to any applicable legal restrictions, contractual conditions, and within a reasonable amount of time period. You may opt out of receiving any communications from us at any time by: - 6.1.1 following the opt-out instructions or by clicking on the “unsubscribe link” contained in each marketing communication; - 6.1.2 editing the relevant account settings to unsubscribe; or - 6.1.3 sending a request to pdpa@wellai.app. + 6.1. You are entitled to limit our processing of your personal data by + expressly withdrawing in full, your consent given previously, in each + case, including for direct marketing purposes subject to any + applicable legal restrictions, contractual conditions, and within a + reasonable amount of time period. You may opt out of receiving any + communications from us at any time by: 6.1.1 following the opt-out + instructions or by clicking on the “unsubscribe link” contained in + each marketing communication; 6.1.2 editing the relevant account + settings to unsubscribe; or 6.1.3 sending a request to + pdpa@wellai.app. 7. Complaints - 7.1. For any concerns or complaints regarding the use of Personal Data, you may contact the designated email address via pdpa@wellai.app. + 7.1. For any concerns or complaints regarding the use of Personal + Data, you may contact the designated email address via + pdpa@wellai.app. 8. Children - 8.1. Minors under the age of 18 may not use the respective websites or apps. WellAI does not knowingly collect personal information from anyone under the age of 18. No part of the website or application is designed to attract anyone under the age of 18. WellAI does not sell products and/or services for purchase by children. In certain instances, WellAI sells products and/or services for children but for purchase by adults. - 8.2 Links to Third Party Website - Parts of our website may contain links to third party websites not owned by WellAI (“Third Party websites”). When you access a Third-Party website, please understand that we do not control the content of that Third Party website and are not responsible for the privacy practices or the content of that Third Party website. This Policy applies only to our site and you should be aware that other sites linked by this web site may have different privacy and personal data protection policies and we highly recommend that you read and understand the privacy statement of each site. We accept no responsibility or liability in respect of any such third-party materials or for the operation or content of other websites (whether or not linked to our website) which are not under WellAI's control. + 8.1. Minors under the age of 18 may not use the respective websites or + apps. WellAI does not knowingly collect personal information from + anyone under the age of 18. No part of the website or application is + designed to attract anyone under the age of 18. WellAI does not sell + products and/or services for purchase by children. In certain + instances, WellAI sells products and/or services for children but for + purchase by adults. 8.2 Links to Third Party Website Parts of our + website may contain links to third party websites not owned by WellAI + (“Third Party websites”). When you access a Third-Party website, + please understand that we do not control the content of that Third + Party website and are not responsible for the privacy practices or the + content of that Third Party website. This Policy applies only to our + site and you should be aware that other sites linked by this web site + may have different privacy and personal data protection policies and + we highly recommend that you read and understand the privacy statement + of each site. We accept no responsibility or liability in respect of + any such third-party materials or for the operation or content of + other websites (whether or not linked to our website) which are not + under WellAI's control. - 9. External Links + + 9. External Links + - 9.1. The websites or apps may contain links to third-party websites. WellAI is not responsible for the privacy practices or content of these external sites. + 9.1. The websites or apps may contain links to third-party websites. + WellAI is not responsible for the privacy practices or content of + these external sites. - 10. Security Precautions + + 10. Security Precautions + - 10.1. WellAI has stringent security measures in place to protect against unauthorized access, loss, misuse and alteration to Personal Data. - 10.2 A "Force Majeure Event" shall mean any event that is beyond the reasonable control and shall include, without limitation, sabotage, fire, flood, explosion, acts of God, civil commotion, strikes or industrial action of any kind, riots, insurrection, war, acts of government, computer hacking, unauthorised access to computer data and storage device, computer crashes, breach of security and encryption, etc. + 10.1. WellAI has stringent security measures in place to protect + against unauthorized access, loss, misuse and alteration to Personal + Data. 10.2 A "Force Majeure Event" shall mean any event that is beyond + the reasonable control and shall include, without limitation, + sabotage, fire, flood, explosion, acts of God, civil commotion, + strikes or industrial action of any kind, riots, insurrection, war, + acts of government, computer hacking, unauthorised access to computer + data and storage device, computer crashes, breach of security and + encryption, etc. - 11. Changes to this Privacy Policy + + 11. Changes to this Privacy Policy + - 11.1. WellAI reserves the right to update, change, or modify this Privacy Policy. Such changes will be effective from the date of the update. If You continue to use Your account and/or access the Website/App even after any such changes have been made, it would be deemed to be an implied consent to the changed policy. + 11.1. WellAI reserves the right to update, change, or modify this + Privacy Policy. Such changes will be effective from the date of the + update. If You continue to use Your account and/or access the + Website/App even after any such changes have been made, it would be + deemed to be an implied consent to the changed policy. - 12. Cookies and Other Tracking Technologies + + 12. Cookies and Other Tracking Technologies + - 12.1. The websites or apps may use cookies and tracking technologies to collect information about user activity for analysis and customization of user experience. A "cookie" is a small text file that may be used, for example, to collect information about Site activity. Some cookies and other technologies may serve to recall Personal Information previously indicated by a Web user. Most browsers allow You to control cookies, including whether or not to accept them and how to remove them. - 12.2 Tracking technologies may record information such as Internet domain and host names; Internet protocol (IP) addresses; browser software and operating system types; stream patterns; and dates and times that our website or application is accessed. Our use of cookies and other tracking technologies allows WellAIs to improve our website, application and user experience. We may also analyze information that does not contain Personal Information for trends and statistics. + 12.1. The websites or apps may use cookies and tracking technologies + to collect information about user activity for analysis and + customization of user experience. A "cookie" is a small text file that + may be used, for example, to collect information about Site activity. + Some cookies and other technologies may serve to recall Personal + Information previously indicated by a Web user. Most browsers allow + You to control cookies, including whether or not to accept them and + how to remove them. 12.2 Tracking technologies may record information + such as Internet domain and host names; Internet protocol (IP) + addresses; browser software and operating system types; stream + patterns; and dates and times that our website or application is + accessed. Our use of cookies and other tracking technologies allows + WellAIs to improve our website, application and user experience. We + may also analyze information that does not contain Personal + Information for trends and statistics. - 13. Data Transfer and Sharing + + 13. Data Transfer and Sharing + - 13.1. Personal Data may be transferred to third parties, including within the respective group of companies or any third party service or product providers within or outside the country, for purposes of data storage, processing, and service provision. + 13.1. Personal Data may be transferred to third parties, including + within the respective group of companies or any third party service or + product providers within or outside the country, for purposes of data + storage, processing, and service provision. 14. Indemnity - 14.1. Users agree to indemnify WellAI against any disputes arising from the disclosure of Personal Information to third parties either through WellAI website or application or otherwise. We assume no liability for any actions of third parties with regard to the Personal Data, which you may have disclosed to such third parties. + 14.1. Users agree to indemnify WellAI against any disputes arising + from the disclosure of Personal Information to third parties either + through WellAI website or application or otherwise. We assume no + liability for any actions of third parties with regard to the Personal + Data, which you may have disclosed to such third parties. - 15. Severability + + 15. Severability + - 15.1. Each paragraph of this Privacy Policy is separate and independent. Nullification of one paragraph does not affect the other paragraphs herein except where otherwise expressly indicated or indicated by the context of the agreement. + 15.1. Each paragraph of this Privacy Policy is separate and + independent. Nullification of one paragraph does not affect the other + paragraphs herein except where otherwise expressly indicated or + indicated by the context of the agreement. - 16. Contact Information + + 16. Contact Information + - 16.1. For any feedback or comments about this Privacy Policy, you may contact the designated email address via pdpa@wellai.app. + 16.1. For any feedback or comments about this Privacy Policy, you may + contact the designated email address via pdpa@wellai.app. - + ); }; -export default PrivacyNotice; \ No newline at end of file +export default PrivacyNotice; diff --git a/client/src/components/ReportTemplate.js b/client/src/components/ReportTemplate.js index 9df107ab..f987e123 100644 --- a/client/src/components/ReportTemplate.js +++ b/client/src/components/ReportTemplate.js @@ -11,6 +11,15 @@ import { useMediaQuery, } from "@mui/material"; +/** + * A template that can be filled in with health data from generated from + * reports and displays them in a readable format. + * + * @param {Object} props + * @param {Object} [props.report] - Health data in the report. + * @param {Object} [props.date] - Date the report was generated. + * @returns {@mui.material.Box} + */ const ReportTemplate = ({ report, date }) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); diff --git a/client/src/components/RequestPatientAccessForm.js b/client/src/components/RequestPatientAccessForm.js index 269ff178..53653fdd 100644 --- a/client/src/components/RequestPatientAccessForm.js +++ b/client/src/components/RequestPatientAccessForm.js @@ -15,6 +15,8 @@ const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** * Provides a merchant user with a form to send an access request to an existing patient. + * + * @returns {@mui.material.Container} */ const RequestPatientAccessForm = () => { const [email, setEmail] = useState(null); diff --git a/client/src/components/WelcomePanel.js b/client/src/components/WelcomePanel.js index 4137a6ee..f75c52a2 100644 --- a/client/src/components/WelcomePanel.js +++ b/client/src/components/WelcomePanel.js @@ -2,7 +2,10 @@ import { Box, Container, Stack, Typography } from "@mui/material"; import Logo from "../assets/WellAiLogoTR.png"; /** - * An panel that introduce the web service. + * A panel that displays the name of the web application and the + * company's logo as an introduction to the web application. + * + * @returns {@mui.material.Container} */ const WelcomePanel = () => { return ( diff --git a/client/src/components/administrator/AccountApprovalTable.js b/client/src/components/administrator/AccountApprovalTable.js index 58a490b8..9b2ae6cc 100644 --- a/client/src/components/administrator/AccountApprovalTable.js +++ b/client/src/components/administrator/AccountApprovalTable.js @@ -5,6 +5,11 @@ import ConfirmationDialog from "../confirmationDialog"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A table that lists the merchant accounts awaiting approval by an administrator. + * + * @returns {@mui.material.Box} + */ const AccountApprovalTable = ({}) => { const [userData, setUserData] = useState([]); // Stores user data const [selectedUser, setselectedUser] = useState(); // Stores the selected user @@ -49,9 +54,27 @@ const AccountApprovalTable = ({}) => { }; const columns = [ - { field: "email", headerName: "Email", flex: 2, width: 250, sortable: true }, - { field: "fullName", headerName: "Full Name", flex: 1.5, width: 250, sortable: true }, - { field: "createdAt", headerName: "Created At", flex: 1.2, width: 200, sortable: true }, + { + field: "email", + headerName: "Email", + flex: 2, + width: 250, + sortable: true, + }, + { + field: "fullName", + headerName: "Full Name", + flex: 1.5, + width: 250, + sortable: true, + }, + { + field: "createdAt", + headerName: "Created At", + flex: 1.2, + width: 200, + sortable: true, + }, { field: "confirm", headerName: "Confirm", diff --git a/client/src/components/administrator/AuditLogSearchBar.js b/client/src/components/administrator/AuditLogSearchBar.js index 82461176..b98af4f3 100644 --- a/client/src/components/administrator/AuditLogSearchBar.js +++ b/client/src/components/administrator/AuditLogSearchBar.js @@ -2,6 +2,18 @@ import SearchIcon from "@mui/icons-material/Search"; import { TextField, InputAdornment, Box } from "@mui/material"; import { memo, useEffect, useState } from "react"; +/** + * A search bar that is used as an element in the AuditLogTable to filter + * the data inside of it. + * + * @param {Object} props + * @param {function} [props.onSearchChange] - Callback function for changes + * in search criteria + * @param {int} [props.delay] - Millisecond delay before calling onSearchChange + * callback + * @param {string} [props.placeholder] - Placeholder text in search bar + * @returns {@mui.material.Box} + */ const AuditLogSearchBar = ({ onSearchChange, delay = 500, placeholder }) => { const [value, setValue] = useState(""); diff --git a/client/src/components/administrator/AuditLogTable.js b/client/src/components/administrator/AuditLogTable.js index 131435ce..7e4c241e 100644 --- a/client/src/components/administrator/AuditLogTable.js +++ b/client/src/components/administrator/AuditLogTable.js @@ -31,6 +31,12 @@ const EVENT_TYPES = [ "PATIENT_PROFILE_UPDATED", ]; +/** + * A table that lists the system's audit logs with the tools to filter + * them efficiently. + * + * @returns {@mui.material.Box} + */ const AuditLogTable = () => { const [logData, setLogData] = useState([]); const [totalLogs, setTotalLogs] = useState(0); @@ -81,7 +87,13 @@ const AuditLogTable = () => { useEffect(() => { fetchLogs(); - }, [paginationModel.page, paginationModel.pageSize, userEmail, eventType, sortModel]); + }, [ + paginationModel.page, + paginationModel.pageSize, + userEmail, + eventType, + sortModel, + ]); const handleEmailSearchChange = useCallback((value) => { setPaginationModel((prev) => ({ ...prev, page: 0 })); @@ -94,14 +106,62 @@ const AuditLogTable = () => { }, []); const columns = [ - { field: "logID", headerName: "Log ID", flex: 0.7, width: 70, sortable: true }, - { field: "eventType", headerName: "Event Type", flex: 1, minWidth: 140, sortable: true }, - { field: "success", headerName: "Success", width: 80, type: "boolean", sortable: true }, - { field: "userEmail", headerName: "User Email", flex: 1.5, minWidth: 180, sortable: true }, - { field: "ipAddress", headerName: "IP Address", flex: 0.8, minWidth: 100, sortable: true }, - { field: "device", headerName: "Device", flex: 1, minWidth: 120, sortable: false }, - { field: "createdAt", headerName: "Created At", flex: 1, minWidth: 160, sortable: true }, - { field: "description", headerName: "Description", flex: 2, minWidth: 200, sortable: false }, + { + field: "logID", + headerName: "Log ID", + flex: 0.7, + width: 70, + sortable: true, + }, + { + field: "eventType", + headerName: "Event Type", + flex: 1, + minWidth: 140, + sortable: true, + }, + { + field: "success", + headerName: "Success", + width: 80, + type: "boolean", + sortable: true, + }, + { + field: "userEmail", + headerName: "User Email", + flex: 1.5, + minWidth: 180, + sortable: true, + }, + { + field: "ipAddress", + headerName: "IP Address", + flex: 0.8, + minWidth: 100, + sortable: true, + }, + { + field: "device", + headerName: "Device", + flex: 1, + minWidth: 120, + sortable: false, + }, + { + field: "createdAt", + headerName: "Created At", + flex: 1, + minWidth: 160, + sortable: true, + }, + { + field: "description", + headerName: "Description", + flex: 2, + minWidth: 200, + sortable: false, + }, ]; return ( @@ -128,9 +188,10 @@ const AuditLogTable = () => { /> { Filter by Event Type @@ -174,11 +235,10 @@ const AuditLogTable = () => { - - + { }, }} /> - + ); diff --git a/client/src/components/administrator/UserManagementTable.js b/client/src/components/administrator/UserManagementTable.js index d3c72852..0a9ae480 100644 --- a/client/src/components/administrator/UserManagementTable.js +++ b/client/src/components/administrator/UserManagementTable.js @@ -1,4 +1,12 @@ -import { Paper, Box, Snackbar, Alert, Stack, Typography, Divider } from "@mui/material"; +import { + Paper, + Box, + Snackbar, + Alert, + Stack, + Typography, + Divider, +} from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; import { useState, useEffect, useCallback } from "react"; import ConfirmationDialog from "../confirmationDialog"; @@ -6,6 +14,13 @@ import UserSearchBar from "./UserSearchBar"; import UserToolBar from "./UserToolBar"; import * as React from "react"; +/** + * A table listing all the user accounts in the system with tools to search + * and filter through them. It details the account's email, full name, + * creation date, validation status, and role. + * + * @returns {@mui.material.Box} + */ const UserManagementTable = () => { const [userData, setUserData] = useState([]); // Stores user data const [totalUsers, setTotalUsers] = useState(0); @@ -49,7 +64,7 @@ const UserManagementTable = () => { if (selectedClinic) { params.append("clinic_id", selectedClinic); } - + // Append sort params when sort is active if (sortModel.length > 0) { params.append("sort_by", sortModel[0].field); @@ -103,7 +118,7 @@ const UserManagementTable = () => { }); }, [API_BASE]); - useEffect(() => { + useEffect(() => { fetch(`${API_BASE}/clinics`) .then((response) => { if (!response.ok) { @@ -117,7 +132,6 @@ const UserManagementTable = () => { }); }, [API_BASE]); - const confirmRoleChange = async (e) => { e.preventDefault(); const emails = getSelectedEmails(); @@ -229,11 +243,42 @@ const UserManagementTable = () => { }, []); const columns = [ - { field: "email", headerName: "Email", flex: 2, width: 250, sortable: true }, - { field: "fullName", headerName: "Full Name", flex: 1.5, width: 250, sortable: false }, - { field: "createdAt", headerName: "Created At", flex: 1.2, width: 200, sortable: true }, - { field: "validated", headerName: "Validation Status", flex: 1, width: 150, sortable: true }, - { field: "role", headerName: "Role", flex: 1.3, width: 220, sortable: false, valueGetter: (params) => params?.name }, + { + field: "email", + headerName: "Email", + flex: 2, + width: 250, + sortable: true, + }, + { + field: "fullName", + headerName: "Full Name", + flex: 1.5, + width: 250, + sortable: false, + }, + { + field: "createdAt", + headerName: "Created At", + flex: 1.2, + width: 200, + sortable: true, + }, + { + field: "validated", + headerName: "Validation Status", + flex: 1, + width: 150, + sortable: true, + }, + { + field: "role", + headerName: "Role", + flex: 1.3, + width: 220, + sortable: false, + valueGetter: (params) => params?.name, + }, ]; return ( @@ -247,11 +292,12 @@ const UserManagementTable = () => { }} > - { delay={400} /> - + - + diff --git a/client/src/components/administrator/UserSearchBar.js b/client/src/components/administrator/UserSearchBar.js index e4f1b7e7..bc72c3ed 100644 --- a/client/src/components/administrator/UserSearchBar.js +++ b/client/src/components/administrator/UserSearchBar.js @@ -3,6 +3,16 @@ import { TextField, Box } from "@mui/material"; import InputAdornment from "@mui/material/InputAdornment"; import { useEffect, useState, memo } from "react"; +/** + * A search bar that is used as an element in the UserManagementTable to + * filter the data inside of it. + * + * @param {Object} props + * @param {function} [props.onSearchChange] - Callback function for changes in search criteria. + * @param {int} [props.delay] - Millisecond delay before calling onSearchChange callback. + * @param {string} [props.placeholder] - Placeholder text in search bar. + * @returns {@mui.material.Box} + */ const UserSearchBar = ({ placeholder, onSearchChange, delay = 400 }) => { const [inputValue, setInputValue] = useState(""); diff --git a/client/src/components/administrator/UserToolBar.js b/client/src/components/administrator/UserToolBar.js index 7092f49c..e57404a7 100644 --- a/client/src/components/administrator/UserToolBar.js +++ b/client/src/components/administrator/UserToolBar.js @@ -1,6 +1,24 @@ import { Box, Select, Button, MenuItem } from "@mui/material"; import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; +/** + * A tool bar to that provides the controls for modifying and filtering user + * data including deletion, role changing, and clinic filtering. This + * component is designed to be used with the UserManagementTable. + * + * @param {Object} props + * @param {Object} [props.rowSelectionModel] + * @param {string} [props.rowSelectionModel.type] - "include" or "exclude" selection. + * @param {Set} [props.rowSelectionModel.ids] - User ID's that are selected + * @param {int} [props.totalRowCount] - Number of rows when selection type is "exclude" + * @param {function} [props.onUsersDelete] - Callback function for deleting user + * @param {function} [props.onUsersRoleChange] - Callback function for changing user role + * @param {function} [props.onClinicChange] - Callback function for changing clinic + * @param {Object} [props.roleData] - A list of all roles in the system and their ID + * @param {Object} [props.clinicData] - A list of all clinics in the system and their ID + * @param {int} [props.selectedClinic] - ID of the selected clinic + * @returns {@mui.material.Box} + */ const UserToolBar = ({ rowSelectionModel, totalRowCount, @@ -39,12 +57,10 @@ const UserToolBar = ({ value={selectedClinic} onChange={(e) => onClinicChange(e.target.value)} sx={{ - width: { xs: "100%", sm:200}, + width: { xs: "100%", sm: 200 }, }} > - - All Clinics - + All Clinics {clinicData.map((clinic) => ( {clinic.name} @@ -58,7 +74,7 @@ const UserToolBar = ({ value="" onChange={(e) => onUsersRoleChange(e.target.value)} sx={{ - width: { xs: "100%", sm: "auto" } + width: { xs: "100%", sm: "auto" }, }} > @@ -77,7 +93,7 @@ const UserToolBar = ({ disabled={emailCount === 0} onClick={onUsersDelete} sx={{ - width: { xs: "100%", sm: "auto" } + width: { xs: "100%", sm: "auto" }, }} > Delete Selected diff --git a/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js b/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js index f6b6960d..cec30903 100644 --- a/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js +++ b/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js @@ -12,8 +12,10 @@ import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Displays the amount of merchants that have logged into the system over the - * past month and past week. + * A card that displays the amount of merchants that have logged into the + * system over the past month and past week. + * + * @returns {@mui.material.Card} */ const ActiveMerchantsAnalytics = () => { const [data, setData] = useState({}); diff --git a/client/src/components/administrator/analytics/ActiveUsersAnalytics.js b/client/src/components/administrator/analytics/ActiveUsersAnalytics.js index a394f804..782c02bc 100644 --- a/client/src/components/administrator/analytics/ActiveUsersAnalytics.js +++ b/client/src/components/administrator/analytics/ActiveUsersAnalytics.js @@ -12,8 +12,10 @@ import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Displays the amount of standard users that have logged into the system over the - * past month and past week. + * A card that displays the amount of standard users that have logged into + * the system over the past month and past week. + * + * @returns {@mui.material.Card} */ const ActiveUsersAnalytics = () => { const [data, setData] = useState({}); diff --git a/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js b/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js index 225c747a..b2351026 100644 --- a/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js +++ b/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js @@ -20,8 +20,10 @@ const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; const DEFAULT_YEAR = 2026; /** - * Displays the average progression for stroke, diabetes, and cardiovascular - * disease of the years. + * A card that displays the average progression for stroke, diabetes, + * and cardiovascular disease of the years. + * + * @returns {@mui.material.Card} */ const AverageRiskSeriesAnalytics = () => { const [availableYears, setAvailableYears] = useState([]); diff --git a/client/src/components/administrator/analytics/LoginActivityAnalytics.js b/client/src/components/administrator/analytics/LoginActivityAnalytics.js index 961ec0d5..32b816a3 100644 --- a/client/src/components/administrator/analytics/LoginActivityAnalytics.js +++ b/client/src/components/administrator/analytics/LoginActivityAnalytics.js @@ -21,7 +21,9 @@ const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; const TIMESPAN_IN_DAYS = 7; /** - * Displays the number of logins over the past seven days. + * A card that displays the number of logins over the past seven days. + * + * @returns {@mui.material.Card} */ const LoginActivityAnalytics = () => { const [xAxisData, setXAxisData] = useState([]); diff --git a/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js b/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js index a8ca645c..0af169c0 100644 --- a/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js +++ b/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js @@ -13,7 +13,9 @@ import ErrorIcon from "@mui/icons-material/Error"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Displays the amount of merchants awaiting approval from a moderator. + * A card that displays the amount of merchants awaiting approval from a moderator. + * + * @returns {@mui.material.Card} */ const PendingMerchantsAnalytics = () => { const [data, setData] = useState({}); diff --git a/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js b/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js index b098cec6..8981e19b 100644 --- a/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js +++ b/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js @@ -12,8 +12,10 @@ import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Displays the amount of health reports that have been generated over the - * past month and past week. + * A card that displays the amount of health reports that have been + * generated over the past month and past week. + * + * @returns {@mui.material.Card} */ const RecentReportsGeneratedAnalytics = () => { const [data, setData] = useState({}); diff --git a/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js b/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js index 940005f1..cc5b4d90 100644 --- a/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js +++ b/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js @@ -12,7 +12,9 @@ import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Displays the number of unvalidated user accounts in the system. + * A card that displays the number of unvalidated user accounts in the system. + * + * @returns {@mui.material.Card} */ const UnvalidatedAccountAnalytics = () => { const [data, setData] = useState({}); diff --git a/client/src/components/administrator/analytics/UserAccountAnalytics.js b/client/src/components/administrator/analytics/UserAccountAnalytics.js index 234038fe..48d41840 100644 --- a/client/src/components/administrator/analytics/UserAccountAnalytics.js +++ b/client/src/components/administrator/analytics/UserAccountAnalytics.js @@ -13,7 +13,11 @@ import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Displays general account information in the system. + * A card that displays general information about the users in the system. + * This includes the total number of accounts, how they are distributed + * between standard and merchant accounts, and the total number of patients. + * + * @returns {@mui.material.Card} */ const UserAccountAnalytics = () => { const [data, setData] = useState({}); diff --git a/client/src/components/authentication/CreatePatientForm.js b/client/src/components/authentication/CreatePatientForm.js index cfb31db7..ee30e215 100644 --- a/client/src/components/authentication/CreatePatientForm.js +++ b/client/src/components/authentication/CreatePatientForm.js @@ -20,9 +20,14 @@ import { } from "@mui/material"; const FULL_NAME_MAX_LENGTH = 255; - const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A form that contains all the necessary fields and functionality + * for the creation of a new patient. + * + * @returns {@mui.material.Paper} + */ const CreatePatientForm = () => { const navigate = useNavigate(); const [givenNames, setGivenNames] = useState(null); @@ -169,7 +174,7 @@ const CreatePatientForm = () => { if (!response.ok) { setShowFailMessage(true); setShowSuccessMessage(false); - return + return; } setShowSuccessMessage(true); setShowFailMessage(false); @@ -205,7 +210,8 @@ const CreatePatientForm = () => { Create Patient - Register a patient profile to enable adding health records for them. + Register a patient profile to enable adding health records for + them. diff --git a/client/src/components/authentication/EmailInputField.js b/client/src/components/authentication/EmailInputField.js index 93ffafef..6a992aec 100644 --- a/client/src/components/authentication/EmailInputField.js +++ b/client/src/components/authentication/EmailInputField.js @@ -6,8 +6,11 @@ import validator from "validator"; * An input field that provides basic validation for an entered email. * * @param {Object} props - * @param {function} [props.onChange] - Callback function called when input is changed. - * @param {boolean} [props.showRequired] - Force the component to show the error state. + * @param {function} [props.onChange] - Callback function called when + * input is changed. + * @param {boolean} [props.showRequired] - Force the component to show + * the error state. + * @returns {@mui.material.TextField} */ const EmailInputField = ({ onChange, showRequired = false }) => { const [email, setEmail] = useState(""); diff --git a/client/src/components/authentication/ForgotPasswordForm.js b/client/src/components/authentication/ForgotPasswordForm.js index 3c3b6796..a4ca8fa6 100644 --- a/client/src/components/authentication/ForgotPasswordForm.js +++ b/client/src/components/authentication/ForgotPasswordForm.js @@ -17,7 +17,9 @@ import Logo from "../../assets/WellAiLogoTR.png"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Provides the user a form to request a password reset using their email. + * A form that can be filled in by a user to submit a password reset request. + * + * @return {@mui.material.Card} */ const ForgotPasswordForm = () => { const [email, setEmail] = useState(null); diff --git a/client/src/components/authentication/LoginForm.js b/client/src/components/authentication/LoginForm.js index 2cc8fc93..5d63ba58 100644 --- a/client/src/components/authentication/LoginForm.js +++ b/client/src/components/authentication/LoginForm.js @@ -19,8 +19,10 @@ import { UserContext } from "../../utils/UserContext"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Provides the user a form with the required fields and buttons to login to - * the application. + * A form that can be filled in with a user's account credentials to login + * to the service. + * + * @returns {@mui.material.Card} */ const LoginForm = () => { const navigate = useNavigate(); diff --git a/client/src/components/authentication/PasswordInputField.js b/client/src/components/authentication/PasswordInputField.js index 6d42bcf2..d85347cf 100644 --- a/client/src/components/authentication/PasswordInputField.js +++ b/client/src/components/authentication/PasswordInputField.js @@ -10,6 +10,7 @@ import { TextField } from "@mui/material"; * outside the suitable length. * @param {boolean} [props.truncate] - Truncate the password in the onChange callback. * @param {boolean} [props.showRequired] - Force the component to show the error state. + * @returns {@mui.material.TextField} */ const PasswordInputField = ({ onChange, diff --git a/client/src/components/authentication/PhoneInputField.js b/client/src/components/authentication/PhoneInputField.js index 500ecdb2..21db14b5 100644 --- a/client/src/components/authentication/PhoneInputField.js +++ b/client/src/components/authentication/PhoneInputField.js @@ -12,7 +12,8 @@ import { * * @param {Object} props * @param {function} [props.onChange] - Callback function called when input - * is changed. + * is changed. + * @returns {@mui.material.Box} */ const PhoneInputField = ({ onChange }) => { const [rawPhoneNumber, setRawPhoneNumber] = useState(""); diff --git a/client/src/components/authentication/RegistrationForm.js b/client/src/components/authentication/RegistrationForm.js index 08d71cb2..748feac8 100644 --- a/client/src/components/authentication/RegistrationForm.js +++ b/client/src/components/authentication/RegistrationForm.js @@ -35,6 +35,12 @@ const ACCOUNT_TYPES = Object.freeze({ const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A form that can be filled out by a user to create an account with the + * service. + * + * @returns {@mui.material.Card} + */ const RegistrationForm = () => { const navigate = useNavigate(); const [givenNameState, setGivenNameState] = useState(null); diff --git a/client/src/components/authentication/ResetPasswordForm.js b/client/src/components/authentication/ResetPasswordForm.js index 6f78200a..22e84d10 100644 --- a/client/src/components/authentication/ResetPasswordForm.js +++ b/client/src/components/authentication/ResetPasswordForm.js @@ -18,7 +18,9 @@ import PasswordInputField from "../authentication/PasswordInputField"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; /** - * Provides a form to reset their password one time with a reset token. + * A form to reset a user's password using a one-time reset token. + * + * @returns {@mui.material.Card} */ const ResetPasswordForm = () => { const { token } = useParams(); diff --git a/client/src/components/confirmationDialog.js b/client/src/components/confirmationDialog.js index b8b4ce05..4084f58c 100644 --- a/client/src/components/confirmationDialog.js +++ b/client/src/components/confirmationDialog.js @@ -7,17 +7,21 @@ import { Button, } from "@mui/material"; -// Reusable confirmation dialog with fully controlled content/styles by caller. -// Props: -// - open: boolean -// - title: string | ReactNode -// - message: string | ReactNode -// - confirm: () => void -// - cancel: () => void -// - confirmText: string -// - cancelText: string -// - confirmColor: 'primary' | 'secondary' | 'error' | 'info' | 'success' | 'warning' -// - cancelColor: same as above +/** + * A confirmation dialog with fully controlled content/styles by caller. + * + * @param {Object} props + * @param {boolean} [props.open] + * @param {string | ReactNode} [props.title] + * @param {string | ReactNode} [props.message] + * @param {function} [props.confirm] + * @param {function} [props.cancel] + * @param {string} [props.confirmText] + * @param {string} [props.cancelText] + * @param {string} [props.confirmColor] primary, secondary, error, info, success, warning + * @param {string} [props.cancelColor] Same as above + * @returns {@mui.material.Dialog} + */ const ConfirmationDialog = ({ open, title, diff --git a/client/src/routes/AcceptAccessRequest.js b/client/src/routes/AcceptAccessRequest.js index 867b78d4..388ea533 100644 --- a/client/src/routes/AcceptAccessRequest.js +++ b/client/src/routes/AcceptAccessRequest.js @@ -10,7 +10,10 @@ import { import AcceptRequestForm from "../components/AcceptRequestForm"; /** - * Displays the form to accept an access request + * A page that displays the accept access request form for users to + * accept requests from merchants. + * + * @returns {@mui.material.Container} */ const AcceptAccessRequest = () => { return ( diff --git a/client/src/routes/AdministratorApproval.js b/client/src/routes/AdministratorApproval.js index 77caa26e..d1221ee5 100644 --- a/client/src/routes/AdministratorApproval.js +++ b/client/src/routes/AdministratorApproval.js @@ -1,12 +1,12 @@ import AccountApprovalTable from "../components/administrator/AccountApprovalTable"; -import { - Box, - Typography, - Divider, - Stack, - Container -} from "@mui/material"; +import { Box, Typography, Divider, Stack, Container } from "@mui/material"; +/** + * A route that displays the merchant approval table where administrators + * can audit and approve the creation of a merchant's account. + * + * @returns {@mui.material.Container} + */ const AdministratorApproval = () => { return ( { pb: { xs: "2px", sm: "0px", md: "4px" }, pr: { xs: "2px", sm: "0px", md: "4px" }, mr: "0px", - maxWidth: "1600px", + maxWidth: "1600px", }} > diff --git a/client/src/routes/AdministratorDashboard.js b/client/src/routes/AdministratorDashboard.js index cda7d720..1514c5c2 100644 --- a/client/src/routes/AdministratorDashboard.js +++ b/client/src/routes/AdministratorDashboard.js @@ -21,6 +21,12 @@ import UserAccountAnalytics from "../components/administrator/analytics/UserAcco const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A route that displays a collection of metrics describing the current + * state of the system, it's users, and the health reports generated. + * + * @returns {@mui.material.Container} + */ const AdministratorDashboard = () => { return ( { return ( { pb: { xs: "2px", sm: "0px", md: "4px" }, pr: { xs: "2px", sm: "0px", md: "4px" }, mr: "0px", - maxWidth: "1600px", + maxWidth: "1600px", }} > diff --git a/client/src/routes/AdministratorUsers.js b/client/src/routes/AdministratorUsers.js index 8cfb58a6..f2993c17 100644 --- a/client/src/routes/AdministratorUsers.js +++ b/client/src/routes/AdministratorUsers.js @@ -1,12 +1,12 @@ import UserManagementTable from "../components/administrator/UserManagementTable"; -import { - Box, - Typography, - Divider, - Stack, - Container -} from "@mui/material"; +import { Box, Typography, Divider, Stack, Container } from "@mui/material"; +/** + * A route that provides tools to manage, filter and search through the users of the + * system. Facilitating the deletion or role changes for an account. + * + * @returns {@mui.material.Container} + */ const AdministratorUsers = () => { return ( { return ( diff --git a/client/src/routes/ErrorPage.js b/client/src/routes/ErrorPage.js index 3421526a..cd04b2ab 100644 --- a/client/src/routes/ErrorPage.js +++ b/client/src/routes/ErrorPage.js @@ -1,13 +1,13 @@ -import { - Typography, - Button, - Box, - Stack, -} from "@mui/material"; +import { Typography, Button, Box, Stack } from "@mui/material"; import { useNavigate } from "react-router-dom"; import Logo from "../assets/WellAiLogoTR.png"; - +/** + * A page that is displayed when a user tried to navigate a non-existent + * or malformed route. + * + * @returns {@mui.material.Container} + */ const ErrorPage = () => { const navigate = useNavigate(); return ( @@ -25,10 +25,7 @@ const ErrorPage = () => { }} > - + Error 404 { sx={{ overflow: "hidden", px: { xs: 2, sm: 8 }, - wordBreak: "break-word" + wordBreak: "break-word", }} > The requested URL cannot be found. - diff --git a/client/src/routes/ForgotPassword.js b/client/src/routes/ForgotPassword.js index 152e9f04..2fcd8744 100644 --- a/client/src/routes/ForgotPassword.js +++ b/client/src/routes/ForgotPassword.js @@ -2,7 +2,10 @@ import { Box, Container } from "@mui/material"; import ForgotPasswordForm from "../components/authentication/ForgotPasswordForm"; /** - * Displays the form to reset a user's password if it has been forgotten. + * A route that provides the form for a user to reset their password by initiating + * the one-time password reset process. + * + * @returns {@mui.material.Container} */ const ForgotPassword = () => { return ( diff --git a/client/src/routes/GenerateReport.js b/client/src/routes/GenerateReport.js index 552ab5d2..63c32a5a 100644 --- a/client/src/routes/GenerateReport.js +++ b/client/src/routes/GenerateReport.js @@ -1,6 +1,11 @@ import GenerateReportForm from "../components/GenerateReportForm"; import { Box } from "@mui/material"; +/** + * A page that provides the form for user's to generate a health report. + * + * @returns {@mui.material.Box} + */ const GenerateReport = ({}) => { return ( { return label; }; +/** + * A page that displays the trends in the user's health based on + * their report history. + * + * @returns {@mui.material.Box} + */ const HealthAnalytics = () => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); @@ -352,7 +358,9 @@ const HealthAnalytics = () => { control={ handleMetricChange("diabetesProbability")} + onChange={() => + handleMetricChange("diabetesProbability") + } sx={{ color: colors[2], "&.Mui-checked": { color: colors[2] }, @@ -369,7 +377,11 @@ const HealthAnalytics = () => { - + Health Risk Trends Over Time {chartSeries.length > 0 && healthData.length > 0 ? ( diff --git a/client/src/routes/Login.js b/client/src/routes/Login.js index 9a34eb9e..76d63171 100644 --- a/client/src/routes/Login.js +++ b/client/src/routes/Login.js @@ -4,8 +4,10 @@ import Logo from "../assets/WellAiLogoTR.png"; import WelcomePanel from "../components/WelcomePanel"; /** - * Introduces the services and provides forms for logging in to - * the service. + * A page that introduces the service and provides the form for logging + * into the service. + * + * @returns {@mui.material.Box} */ const Login = () => { return ( diff --git a/client/src/routes/MerchantGenerateReport.js b/client/src/routes/MerchantGenerateReport.js index d080455e..5c3ef0ef 100644 --- a/client/src/routes/MerchantGenerateReport.js +++ b/client/src/routes/MerchantGenerateReport.js @@ -2,6 +2,12 @@ import { Container, ButtonGroup, Button, Box } from "@mui/material"; import MerchantReportForm from "../components/MerchantReportForm"; import { useState } from "react"; +/** + * A page that provides the health report form for merchants to generate + * reports for their patients. + * + * @returns {@mui.material.Container} + */ const MerchantGenerateReport = ({}) => { const [page, setPage] = useState("manual"); diff --git a/client/src/routes/MerchantLanding.js b/client/src/routes/MerchantLanding.js index c253a142..dc0b7763 100644 --- a/client/src/routes/MerchantLanding.js +++ b/client/src/routes/MerchantLanding.js @@ -4,6 +4,13 @@ import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A page that displays a collections of metrics that summarises the + * history and health data for all of a merchant's patients providing + * a quick overview. + * + * @returns {@mui.material.Container} + */ const MerchantLanding = () => { const [data, setData] = useState({}); const [name, setName] = useState(""); diff --git a/client/src/routes/MerchantReports.js b/client/src/routes/MerchantReports.js index 5786f6fb..d09e2007 100644 --- a/client/src/routes/MerchantReports.js +++ b/client/src/routes/MerchantReports.js @@ -4,11 +4,9 @@ import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; - import ConfirmationDialog from "../components/confirmationDialog"; import React, { useState, useEffect } from "react"; import { useLocation } from "react-router-dom"; - import { Box, Typography, @@ -30,6 +28,12 @@ import { const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A page that provides the tools for a merchants to browse through their + * list of patients and view their past history reports. + * + * @returns {@mui.material.Box} + */ const MerchantReports = ({}) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); diff --git a/client/src/routes/PatientDetails.js b/client/src/routes/PatientDetails.js index d56ca32d..ccd79a0d 100644 --- a/client/src/routes/PatientDetails.js +++ b/client/src/routes/PatientDetails.js @@ -1,4 +1,12 @@ -import { Box, Typography, Card, CardContent, Button, useTheme, useMediaQuery } from "@mui/material"; +import { + Box, + Typography, + Card, + CardContent, + Button, + useTheme, + useMediaQuery, +} from "@mui/material"; import { useState, useEffect } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { BarChart } from "@mui/x-charts/BarChart"; @@ -8,7 +16,9 @@ const APPBAR_HEIGHT = { xs: "58px", sm: "66px" }; const DRAWER_WIDTH = "65px"; /** - * A page used to display a individual patient information for a merchant user. + * A route used to display a individual patient information for a merchant user. + * + * @returns {@mui.materials.Box} */ const PatientDetails = () => { const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; @@ -201,23 +211,33 @@ const PatientDetails = () => { > {["stroke", "diabetes", "cvd"].map((key) => ( - {key.toUpperCase()} - + {patientData?.risks?.[key]?.slice(-1)[0] ?? 0}% { const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; @@ -214,7 +215,16 @@ const PatientManagement = () => { alignItems: { md: "center" }, }} > - + { }} /> { { paginationMode="server" onPaginationModelChange={setPaginationModel} /> - + { return ( diff --git a/client/src/routes/ReportHistory.js b/client/src/routes/ReportHistory.js index d50bbb00..1dc227ca 100644 --- a/client/src/routes/ReportHistory.js +++ b/client/src/routes/ReportHistory.js @@ -27,6 +27,11 @@ import { const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A page that provide the tools for a user to past health reports. + * + * @returns {@mui.material.Container} + */ const AIHealthPrediction = ({}) => { const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); diff --git a/client/src/routes/RequestPatientAccess.js b/client/src/routes/RequestPatientAccess.js index 38503025..2f7337ce 100644 --- a/client/src/routes/RequestPatientAccess.js +++ b/client/src/routes/RequestPatientAccess.js @@ -11,7 +11,9 @@ import ForgotPasswordForm from "../components/authentication/ForgotPasswordForm" import RequestPatientAccessForm from "../components/RequestPatientAccessForm"; /** - * Displays the form to request access to a patients record. + * A page that displays the form to request access to a patients record. + * + * @returns {@mui.material.Box} */ const RequestPatientAccess = () => { return ( diff --git a/client/src/routes/ResetPassword.js b/client/src/routes/ResetPassword.js index 61f3064b..af6eaf08 100644 --- a/client/src/routes/ResetPassword.js +++ b/client/src/routes/ResetPassword.js @@ -2,7 +2,10 @@ import { Container } from "@mui/material"; import ResetPasswordForm from "../components/authentication/ResetPasswordForm"; /** - * Displays the form to reset a user's password if it has been forgotten. + * A page that displays the form to reset a user's password using a one-time + * password reset token. + * + * @return {@mui.material.Container} */ const ResetPassword = () => { return ( diff --git a/client/src/routes/UserLanding.js b/client/src/routes/UserLanding.js index 82c2ae87..2e7ab4c5 100644 --- a/client/src/routes/UserLanding.js +++ b/client/src/routes/UserLanding.js @@ -1,4 +1,12 @@ -import { Card, CardContent, Box, Divider, Typography, useTheme, useMediaQuery } from "@mui/material"; +import { + Card, + CardContent, + Box, + Divider, + Typography, + useTheme, + useMediaQuery, +} from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useEffect, useState } from "react"; import { BarChart } from "@mui/x-charts/BarChart"; @@ -6,9 +14,13 @@ import { BarChart } from "@mui/x-charts/BarChart"; // AppBar height: 56px toolbar + 2px border on mobile (xs), 64px + 2px on desktop (sm+) const APPBAR_HEIGHT = { xs: "58px", sm: "66px" }; const DRAWER_WIDTH = "65px"; - const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * A page that displays a collection of metrics based on the user's data. + * + * @returns {@mui.material.Box} + */ const UserLanding = ({}) => { const navigate = useNavigate(); const [name, setName] = useState(null); diff --git a/client/src/routes/UserSettings.js b/client/src/routes/UserSettings.js index 753b7655..47e54f05 100644 --- a/client/src/routes/UserSettings.js +++ b/client/src/routes/UserSettings.js @@ -22,6 +22,12 @@ import ConfirmationDialog from "../components/confirmationDialog"; import { useNavigate } from "react-router-dom"; import { UserContext } from "../utils/UserContext"; +/** + * A page that provides the tools for a user to securely update their + * account and personal details. + * + * @returns {@mui.material.Box} + */ const UserSettings = () => { const navigate = useNavigate(); const { @@ -326,7 +332,10 @@ const UserSettings = () => { {deleteError && ( - + {deleteError} )} @@ -638,39 +647,37 @@ const UserSettings = () => { - {["Account Details", "Profile", "Password"].map( - (item) => ( - setSelectedSection(item)} - sx={{ - py: 2, - px: 3, - borderLeft: - selectedSection === item - ? "4px solid" - : "4px solid transparent", - borderLeftColor: "primary.main", - bgcolor: - selectedSection === item - ? "action.selected" - : "transparent", - "&:hover": { - bgcolor: "action.hover", - }, + {["Account Details", "Profile", "Password"].map((item) => ( + setSelectedSection(item)} + sx={{ + py: 2, + px: 3, + borderLeft: + selectedSection === item + ? "4px solid" + : "4px solid transparent", + borderLeftColor: "primary.main", + bgcolor: + selectedSection === item + ? "action.selected" + : "transparent", + "&:hover": { + bgcolor: "action.hover", + }, + }} + > + - - - ), - )} + /> + + ))} )} diff --git a/client/src/utils/LandingRoute.js b/client/src/utils/LandingRoute.js index 47cc01dd..388e2b30 100644 --- a/client/src/utils/LandingRoute.js +++ b/client/src/utils/LandingRoute.js @@ -3,6 +3,11 @@ import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * Reroutes a user to the landing page that corresponds to their role. + * + * @returns {react-router-dom.Navigate} + */ const LandingRoute = () => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); diff --git a/client/src/utils/ProtectedRoutes.js b/client/src/utils/ProtectedRoutes.js index 7dc84b3f..bf1ca4be 100644 --- a/client/src/utils/ProtectedRoutes.js +++ b/client/src/utils/ProtectedRoutes.js @@ -4,6 +4,15 @@ import NavBar from "../components/Navbar"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; +/** + * Only grants access to components nested within to user's with the + * specified role. If the user is not logged when attempting to navigate to + * a protected route, they will be redirected once they log in. + * + * @param {Object} props + * @param {string} [props.role] - The role of a user + * @returns { empty | react-router-dom.Navigate} + */ const ProtectedRoutes = ({ role }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); diff --git a/client/src/utils/PublicOnlyRoutes.js b/client/src/utils/PublicOnlyRoutes.js index 9e505d7b..ef0b560d 100644 --- a/client/src/utils/PublicOnlyRoutes.js +++ b/client/src/utils/PublicOnlyRoutes.js @@ -4,7 +4,13 @@ import NavBar from "../components/Navbar"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; -const PublicOnlyRoutes = ({ role }) => { +/** + * Only grants access to child components when a user is not logged in + * the website. + * + * @returns {react-router-dom.Navigate | react-router-dom.Outlet} + */ +const PublicOnlyRoutes = () => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); diff --git a/client/src/utils/UserContext.js b/client/src/utils/UserContext.js index 7d291519..13c738af 100644 --- a/client/src/utils/UserContext.js +++ b/client/src/utils/UserContext.js @@ -8,6 +8,13 @@ import { export const UserContext = createContext(null); +/** + * A utility used to securely share information across the application. + * + * @param {Object} props + * @param {object} [props.children] - child elements nested in this element + * @returns {UserContext} + */ export const UserProvider = ({ children }) => { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); From 31c62222ffcbd27669f03527e27146aa50c5cbae Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 12:11:28 +0930 Subject: [PATCH 049/105] Remove drawer from user report history page --- client/src/routes/ReportHistory.js | 157 ++++++++++++----------------- 1 file changed, 67 insertions(+), 90 deletions(-) diff --git a/client/src/routes/ReportHistory.js b/client/src/routes/ReportHistory.js index 3c35a642..3234fe79 100644 --- a/client/src/routes/ReportHistory.js +++ b/client/src/routes/ReportHistory.js @@ -23,7 +23,9 @@ import { Drawer, useTheme, useMediaQuery, + Paper, } from "@mui/material"; +import { textAlign } from "@mui/system"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; @@ -150,88 +152,79 @@ const AIHealthPrediction = ({}) => { return ( - setIsOpen(false)} - variant="temporary" - PaperProps={{ - sx: { - p: 2, - maxHeight: "50vh", - }, - }} - > + + + + Report History + + + + {/* Date Select */} - - + Year + { - setSelectedYear(e.target.value); - setSelectedMonth(null); - }} - > - {years.map((year) => ( - - {year} - - ))} - - - - {/* Month */} - - Month - - + {years.map((year) => ( + + {year} + + ))} + + - - + {/* Month */} + + Month + + + + + {filteredReportDates.map((item) => ( { > {`Report: ${new Date(item.date).toLocaleDateString("en-AU")}`} - {" "}{`${new Date(item.date).toLocaleTimeString("en-AU")}`} + {" "} + {`${new Date(item.date).toLocaleTimeString("en-AU")}`} } /> + {/* Delete Report Button */} {selectedDate.healthDataId === item.healthDataId && ( { ))} - + {/* Menu and Download Buttons */} @@ -298,24 +293,6 @@ const AIHealthPrediction = ({}) => { p: 2, }} > - - - View Report History - - - {!isOpen && ( - - - - )} - - Date: Sat, 23 May 2026 12:24:14 +0930 Subject: [PATCH 050/105] Remove drawer from Merchant Reports page --- client/src/routes/MerchantReports.js | 194 ++++++++++++--------------- client/src/routes/ReportHistory.js | 2 +- 2 files changed, 87 insertions(+), 109 deletions(-) diff --git a/client/src/routes/MerchantReports.js b/client/src/routes/MerchantReports.js index f5ee5b9d..316a4115 100644 --- a/client/src/routes/MerchantReports.js +++ b/client/src/routes/MerchantReports.js @@ -26,6 +26,7 @@ import { Stack, useTheme, useMediaQuery, + Paper, } from "@mui/material"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; @@ -170,45 +171,26 @@ const MerchantReports = ({}) => { return ( - {/* Top Bar */} - setIsOpen(false)} - variant="temporary" - PaperProps={{ - sx: { - p: 2, - maxHeight: "50vh", - }, + - + - - - Report History - - - - - + Report History + {/* Patient List */} @@ -224,7 +206,17 @@ const MerchantReports = ({}) => { /> - + {/* Year */} Year @@ -264,61 +256,69 @@ const MerchantReports = ({}) => { - - - {filteredReportDates.map((item) => ( - setSelectedDate(item)} - button - sx={{ - py: 2, - px: 3, - borderLeft: - selectedDate?.healthDataId === item.healthDataId - ? "4px solid" - : "4px solid transparent", - borderLeftColor: "primary.main", - bgcolor: - selectedDate?.healthDataId === item.healthDataId - ? "action.selected" - : "transparent", - }} - > - - {`Report: ${new Date(item.date).toLocaleDateString("en-AU")}`} - - {" "}{`${new Date(item.date).toLocaleTimeString("en-AU")}`} + + + {filteredReportDates.map((item) => ( + setSelectedDate(item)} + button + sx={{ + py: 2, + px: 3, + borderLeft: + selectedDate?.healthDataId === item.healthDataId + ? "4px solid" + : "4px solid transparent", + borderLeftColor: "primary.main", + bgcolor: + selectedDate?.healthDataId === item.healthDataId + ? "action.selected" + : "transparent", + }} + > + + {`Report: ${new Date(item.date).toLocaleDateString("en-AU")}`} + + {" "} + {`${new Date(item.date).toLocaleTimeString("en-AU")}`} + - - } - /> - {/* Delete Report Button */} - {selectedDate?.healthDataId === item.healthDataId && ( - setDeleteDialogOpen(true)} - > - - - )} - - ))} - - - + } + /> + {/* Delete Report Button */} + {selectedDate?.healthDataId === item.healthDataId && ( + setDeleteDialogOpen(true)} + > + + + )} + + ))} + + + + + {/* Report Content */} @@ -329,31 +329,9 @@ const MerchantReports = ({}) => { alignItems: { xs: "stretch", sm: "center" }, gap: 2, p: 2, + justifyContent: "flex-end", }} > - - - View Patient Reports - - - {!isOpen && ( - - - - )} - - {selectedDate && reportData && ( { From 0e7585470637c3784ecce61adf5007d9c1d30d96 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 12:44:31 +0930 Subject: [PATCH 051/105] Move merchant routes into a merchant route directory --- client/src/index.js | 14 +++++++------- client/src/routes/{ => merchant}/CreatePatient.js | 2 +- .../{ => merchant}/MerchantGenerateReport.js | 2 +- .../src/routes/{ => merchant}/MerchantLanding.js | 0 .../src/routes/{ => merchant}/MerchantReports.js | 9 +++++---- client/src/routes/{ => merchant}/PatientDetails.js | 0 .../src/routes/{ => merchant}/PatientManagement.js | 2 +- .../routes/{ => merchant}/RequestPatientAccess.js | 4 ++-- 8 files changed, 17 insertions(+), 16 deletions(-) rename client/src/routes/{ => merchant}/CreatePatient.js (90%) rename client/src/routes/{ => merchant}/MerchantGenerateReport.js (89%) rename client/src/routes/{ => merchant}/MerchantLanding.js (100%) rename client/src/routes/{ => merchant}/MerchantReports.js (97%) rename client/src/routes/{ => merchant}/PatientDetails.js (100%) rename client/src/routes/{ => merchant}/PatientManagement.js (99%) rename client/src/routes/{ => merchant}/RequestPatientAccess.js (81%) diff --git a/client/src/index.js b/client/src/index.js index b5fb0fd5..b3f88732 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -11,26 +11,26 @@ import Login from "./routes/Login"; import Register from "./routes/Register"; import UserLanding from "./routes/UserLanding"; import UserSettings from "./routes/UserSettings"; -import MerchantGenerateReport from "./routes/MerchantGenerateReport"; +import MerchantGenerateReport from "./routes/merchant/MerchantGenerateReport"; import AdministratorDashboard from "./routes/AdministratorDashboard"; import AdministratorApproval from "./routes/AdministratorApproval"; import AdministratorLogs from "./routes/AdministratorLogs"; import AdministratorUsers from "./routes/AdministratorUsers"; -import MerchantLanding from "./routes/MerchantLanding"; -import MerchantReports from "./routes/MerchantReports"; +import MerchantLanding from "./routes/merchant/MerchantLanding"; +import MerchantReports from "./routes/merchant/MerchantReports"; import AppThemeProvider from "./components/AppThemeProvider"; import ProtectedRoutes from "./utils/ProtectedRoutes"; import PublicOnlyRoutes from "./utils/PublicOnlyRoutes"; import LandingRoute from "./utils/LandingRoute"; import { UserProvider } from "./utils/UserContext"; import ErrorPage from "./routes/ErrorPage"; -import PatientManagement from "./routes/PatientManagement"; -import PatientDetails from "./routes/PatientDetails"; -import CreatePatient from "./routes/CreatePatient"; +import PatientManagement from "./routes/merchant/PatientManagement"; +import PatientDetails from "./routes/merchant/PatientDetails"; +import CreatePatient from "./routes/merchant/CreatePatient"; import ForgotPassword from "./routes/ForgotPassword"; import ResetPassword from "./routes/ResetPassword"; import AcceptAccessRequest from "./routes/AcceptAccessRequest"; -import RequestPatientAccess from "./routes/RequestPatientAccess"; +import RequestPatientAccess from "./routes/merchant/RequestPatientAccess"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( diff --git a/client/src/routes/CreatePatient.js b/client/src/routes/merchant/CreatePatient.js similarity index 90% rename from client/src/routes/CreatePatient.js rename to client/src/routes/merchant/CreatePatient.js index 8735ea67..863ea59f 100644 --- a/client/src/routes/CreatePatient.js +++ b/client/src/routes/merchant/CreatePatient.js @@ -1,5 +1,5 @@ import { Box } from "@mui/material"; -import CreatePatientForm from "../components/authentication/CreatePatientForm"; +import CreatePatientForm from "../../components/authentication/CreatePatientForm"; // AppBar height: 56px toolbar + 2px border on mobile (xs), 64px + 2px on desktop (sm+) const APPBAR_HEIGHT = { xs: "58px", sm: "66px" }; diff --git a/client/src/routes/MerchantGenerateReport.js b/client/src/routes/merchant/MerchantGenerateReport.js similarity index 89% rename from client/src/routes/MerchantGenerateReport.js rename to client/src/routes/merchant/MerchantGenerateReport.js index 5c3ef0ef..7fc80438 100644 --- a/client/src/routes/MerchantGenerateReport.js +++ b/client/src/routes/merchant/MerchantGenerateReport.js @@ -1,5 +1,5 @@ import { Container, ButtonGroup, Button, Box } from "@mui/material"; -import MerchantReportForm from "../components/MerchantReportForm"; +import MerchantReportForm from "../../components/MerchantReportForm"; import { useState } from "react"; /** diff --git a/client/src/routes/MerchantLanding.js b/client/src/routes/merchant/MerchantLanding.js similarity index 100% rename from client/src/routes/MerchantLanding.js rename to client/src/routes/merchant/MerchantLanding.js diff --git a/client/src/routes/MerchantReports.js b/client/src/routes/merchant/MerchantReports.js similarity index 97% rename from client/src/routes/MerchantReports.js rename to client/src/routes/merchant/MerchantReports.js index 39323b87..075c712e 100644 --- a/client/src/routes/MerchantReports.js +++ b/client/src/routes/merchant/MerchantReports.js @@ -1,10 +1,10 @@ -import ReportTemplate from "../components/ReportTemplate"; -import DownloadReportButton from "../components/DownloadReportButton"; +import ReportTemplate from "../../components/ReportTemplate"; +import DownloadReportButton from "../../components/DownloadReportButton"; import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import ConfirmationDialog from "../components/confirmationDialog"; +import ConfirmationDialog from "../../components/confirmationDialog"; import React, { useState, useEffect } from "react"; import { useLocation } from "react-router-dom"; import { @@ -303,7 +303,8 @@ const MerchantReports = ({}) => { > {`Report: ${new Date(item.date).toLocaleDateString("en-AU")}`} - {" "}{`${new Date(item.date).toLocaleTimeString("en-AU")}`} + {" "} + {`${new Date(item.date).toLocaleTimeString("en-AU")}`} } diff --git a/client/src/routes/PatientDetails.js b/client/src/routes/merchant/PatientDetails.js similarity index 100% rename from client/src/routes/PatientDetails.js rename to client/src/routes/merchant/PatientDetails.js diff --git a/client/src/routes/PatientManagement.js b/client/src/routes/merchant/PatientManagement.js similarity index 99% rename from client/src/routes/PatientManagement.js rename to client/src/routes/merchant/PatientManagement.js index 6dfe85c6..565e9175 100644 --- a/client/src/routes/PatientManagement.js +++ b/client/src/routes/merchant/PatientManagement.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import ConfirmationDialog from "../components/confirmationDialog"; +import ConfirmationDialog from "../../components/confirmationDialog"; import InputAdornment from "@mui/material/InputAdornment"; import { Box, diff --git a/client/src/routes/RequestPatientAccess.js b/client/src/routes/merchant/RequestPatientAccess.js similarity index 81% rename from client/src/routes/RequestPatientAccess.js rename to client/src/routes/merchant/RequestPatientAccess.js index 2f7337ce..16afe84f 100644 --- a/client/src/routes/RequestPatientAccess.js +++ b/client/src/routes/merchant/RequestPatientAccess.js @@ -7,8 +7,8 @@ import { Link, Divider, } from "@mui/material"; -import ForgotPasswordForm from "../components/authentication/ForgotPasswordForm"; -import RequestPatientAccessForm from "../components/RequestPatientAccessForm"; +import ForgotPasswordForm from "../../components/authentication/ForgotPasswordForm"; +import RequestPatientAccessForm from "../../components/RequestPatientAccessForm"; /** * A page that displays the form to request access to a patients record. From 1fb82b7437436555521450430e36233119601912 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 12:52:26 +0930 Subject: [PATCH 052/105] Move standard user pages into a starndardUser route dir --- client/src/index.js | 12 ++++++------ .../routes/{ => standardUser}/AcceptAccessRequest.js | 2 +- .../src/routes/{ => standardUser}/GenerateReport.js | 2 +- .../src/routes/{ => standardUser}/HealthAnalytics.js | 0 .../src/routes/{ => standardUser}/ReportHistory.js | 9 +++++---- client/src/routes/{ => standardUser}/UserLanding.js | 0 client/src/routes/{ => standardUser}/UserSettings.js | 6 +++--- 7 files changed, 16 insertions(+), 15 deletions(-) rename client/src/routes/{ => standardUser}/AcceptAccessRequest.js (91%) rename client/src/routes/{ => standardUser}/GenerateReport.js (86%) rename client/src/routes/{ => standardUser}/HealthAnalytics.js (100%) rename client/src/routes/{ => standardUser}/ReportHistory.js (97%) rename client/src/routes/{ => standardUser}/UserLanding.js (100%) rename client/src/routes/{ => standardUser}/UserSettings.js (99%) diff --git a/client/src/index.js b/client/src/index.js index b3f88732..7f6db99c 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -4,13 +4,13 @@ import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import ReportHistory from "./routes/ReportHistory"; -import GenerateReport from "./routes/GenerateReport"; -import HealthAnalytics from "./routes/HealthAnalytics"; +import ReportHistory from "./routes/standardUser/ReportHistory"; +import GenerateReport from "./routes/standardUser/GenerateReport"; +import HealthAnalytics from "./routes/standardUser/HealthAnalytics"; import Login from "./routes/Login"; import Register from "./routes/Register"; -import UserLanding from "./routes/UserLanding"; -import UserSettings from "./routes/UserSettings"; +import UserLanding from "./routes/standardUser/UserLanding"; +import UserSettings from "./routes/standardUser/UserSettings"; import MerchantGenerateReport from "./routes/merchant/MerchantGenerateReport"; import AdministratorDashboard from "./routes/AdministratorDashboard"; import AdministratorApproval from "./routes/AdministratorApproval"; @@ -29,7 +29,7 @@ import PatientDetails from "./routes/merchant/PatientDetails"; import CreatePatient from "./routes/merchant/CreatePatient"; import ForgotPassword from "./routes/ForgotPassword"; import ResetPassword from "./routes/ResetPassword"; -import AcceptAccessRequest from "./routes/AcceptAccessRequest"; +import AcceptAccessRequest from "./routes/standardUser/AcceptAccessRequest"; import RequestPatientAccess from "./routes/merchant/RequestPatientAccess"; const root = ReactDOM.createRoot(document.getElementById("root")); diff --git a/client/src/routes/AcceptAccessRequest.js b/client/src/routes/standardUser/AcceptAccessRequest.js similarity index 91% rename from client/src/routes/AcceptAccessRequest.js rename to client/src/routes/standardUser/AcceptAccessRequest.js index 388ea533..c1b670e4 100644 --- a/client/src/routes/AcceptAccessRequest.js +++ b/client/src/routes/standardUser/AcceptAccessRequest.js @@ -7,7 +7,7 @@ import { Link, Divider, } from "@mui/material"; -import AcceptRequestForm from "../components/AcceptRequestForm"; +import AcceptRequestForm from "../../components/AcceptRequestForm"; /** * A page that displays the accept access request form for users to diff --git a/client/src/routes/GenerateReport.js b/client/src/routes/standardUser/GenerateReport.js similarity index 86% rename from client/src/routes/GenerateReport.js rename to client/src/routes/standardUser/GenerateReport.js index 63c32a5a..1f4d1b01 100644 --- a/client/src/routes/GenerateReport.js +++ b/client/src/routes/standardUser/GenerateReport.js @@ -1,4 +1,4 @@ -import GenerateReportForm from "../components/GenerateReportForm"; +import GenerateReportForm from "../../components/GenerateReportForm"; import { Box } from "@mui/material"; /** diff --git a/client/src/routes/HealthAnalytics.js b/client/src/routes/standardUser/HealthAnalytics.js similarity index 100% rename from client/src/routes/HealthAnalytics.js rename to client/src/routes/standardUser/HealthAnalytics.js diff --git a/client/src/routes/ReportHistory.js b/client/src/routes/standardUser/ReportHistory.js similarity index 97% rename from client/src/routes/ReportHistory.js rename to client/src/routes/standardUser/ReportHistory.js index c67ceeb0..738ce1b3 100644 --- a/client/src/routes/ReportHistory.js +++ b/client/src/routes/standardUser/ReportHistory.js @@ -1,9 +1,9 @@ -import ReportTemplate from "../components/ReportTemplate"; -import DownloadReportButton from "../components/DownloadReportButton"; +import ReportTemplate from "../../components/ReportTemplate"; +import DownloadReportButton from "../../components/DownloadReportButton"; import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import MenuIcon from "@mui/icons-material/Menu"; -import ConfirmationDialog from "../components/confirmationDialog"; +import ConfirmationDialog from "../../components/confirmationDialog"; import Stack from "@mui/material/Stack"; import React, { useState, useEffect } from "react"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; @@ -271,7 +271,8 @@ const AIHealthPrediction = ({}) => { > {`Report: ${new Date(item.date).toLocaleDateString("en-AU")}`} - {" "}{`${new Date(item.date).toLocaleTimeString("en-AU")}`} + {" "} + {`${new Date(item.date).toLocaleTimeString("en-AU")}`} } diff --git a/client/src/routes/UserLanding.js b/client/src/routes/standardUser/UserLanding.js similarity index 100% rename from client/src/routes/UserLanding.js rename to client/src/routes/standardUser/UserLanding.js diff --git a/client/src/routes/UserSettings.js b/client/src/routes/standardUser/UserSettings.js similarity index 99% rename from client/src/routes/UserSettings.js rename to client/src/routes/standardUser/UserSettings.js index e3c4694e..93de7862 100644 --- a/client/src/routes/UserSettings.js +++ b/client/src/routes/standardUser/UserSettings.js @@ -18,10 +18,10 @@ import { Tabs, Tab, } from "@mui/material"; -import ConfirmationDialog from "../components/confirmationDialog"; +import ConfirmationDialog from "../../components/confirmationDialog"; import { useNavigate } from "react-router-dom"; -import { UserContext } from "../utils/UserContext"; -import PhoneInputField from "../components/authentication/PhoneInputField"; +import { UserContext } from "../../utils/UserContext"; +import PhoneInputField from "../../components/authentication/PhoneInputField"; /** * A page that provides the tools for a user to securely update their From 1bb01b5f6b78d873210333369c167eb5a9b846f4 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 12:56:24 +0930 Subject: [PATCH 053/105] Move admin routes into a admin route directory --- client/src/index.js | 8 ++++---- .../routes/{ => admin}/AdministratorApproval.js | 2 +- .../routes/{ => admin}/AdministratorDashboard.js | 16 ++++++++-------- .../src/routes/{ => admin}/AdministratorLogs.js | 2 +- .../src/routes/{ => admin}/AdministratorUsers.js | 2 +- 5 files changed, 15 insertions(+), 15 deletions(-) rename client/src/routes/{ => admin}/AdministratorApproval.js (93%) rename client/src/routes/{ => admin}/AdministratorDashboard.js (79%) rename client/src/routes/{ => admin}/AdministratorLogs.js (93%) rename client/src/routes/{ => admin}/AdministratorUsers.js (93%) diff --git a/client/src/index.js b/client/src/index.js index 7f6db99c..e73e71df 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -12,10 +12,10 @@ import Register from "./routes/Register"; import UserLanding from "./routes/standardUser/UserLanding"; import UserSettings from "./routes/standardUser/UserSettings"; import MerchantGenerateReport from "./routes/merchant/MerchantGenerateReport"; -import AdministratorDashboard from "./routes/AdministratorDashboard"; -import AdministratorApproval from "./routes/AdministratorApproval"; -import AdministratorLogs from "./routes/AdministratorLogs"; -import AdministratorUsers from "./routes/AdministratorUsers"; +import AdministratorDashboard from "./routes/admin/AdministratorDashboard"; +import AdministratorApproval from "./routes/admin/AdministratorApproval"; +import AdministratorLogs from "./routes/admin/AdministratorLogs"; +import AdministratorUsers from "./routes/admin/AdministratorUsers"; import MerchantLanding from "./routes/merchant/MerchantLanding"; import MerchantReports from "./routes/merchant/MerchantReports"; import AppThemeProvider from "./components/AppThemeProvider"; diff --git a/client/src/routes/AdministratorApproval.js b/client/src/routes/admin/AdministratorApproval.js similarity index 93% rename from client/src/routes/AdministratorApproval.js rename to client/src/routes/admin/AdministratorApproval.js index d1221ee5..e7bf8030 100644 --- a/client/src/routes/AdministratorApproval.js +++ b/client/src/routes/admin/AdministratorApproval.js @@ -1,4 +1,4 @@ -import AccountApprovalTable from "../components/administrator/AccountApprovalTable"; +import AccountApprovalTable from "../../components/administrator/AccountApprovalTable"; import { Box, Typography, Divider, Stack, Container } from "@mui/material"; /** diff --git a/client/src/routes/AdministratorDashboard.js b/client/src/routes/admin/AdministratorDashboard.js similarity index 79% rename from client/src/routes/AdministratorDashboard.js rename to client/src/routes/admin/AdministratorDashboard.js index 1514c5c2..c2e1c8e4 100644 --- a/client/src/routes/AdministratorDashboard.js +++ b/client/src/routes/admin/AdministratorDashboard.js @@ -10,14 +10,14 @@ import { } from "@mui/material"; import { BarChart } from "@mui/x-charts/BarChart"; import { useEffect, useState } from "react"; -import ActiveMerchantsAnalytics from "../components/administrator/analytics/ActiveMerchantsAnalytics"; -import ActiveUsersAnalytics from "../components/administrator/analytics/ActiveUsersAnalytics"; -import AverageRiskSeriesAnalytics from "../components/administrator/analytics/AverageRiskSeriesAnalytics"; -import LoginActivityAnalytics from "../components/administrator/analytics/LoginActivityAnalytics"; -import PendingMerchantsAnalytics from "../components/administrator/analytics/PendingMerchantsAnalytics"; -import RecentReportsGeneratedAnalytics from "../components/administrator/analytics/RecentReportsGeneratedAnalytics"; -import UnvalidatedAccountAnalytics from "../components/administrator/analytics/UnvalidatedAccountAnalytics"; -import UserAccountAnalytics from "../components/administrator/analytics/UserAccountAnalytics"; +import ActiveMerchantsAnalytics from "../../components/administrator/analytics/ActiveMerchantsAnalytics"; +import ActiveUsersAnalytics from "../../components/administrator/analytics/ActiveUsersAnalytics"; +import AverageRiskSeriesAnalytics from "../../components/administrator/analytics/AverageRiskSeriesAnalytics"; +import LoginActivityAnalytics from "../../components/administrator/analytics/LoginActivityAnalytics"; +import PendingMerchantsAnalytics from "../../components/administrator/analytics/PendingMerchantsAnalytics"; +import RecentReportsGeneratedAnalytics from "../../components/administrator/analytics/RecentReportsGeneratedAnalytics"; +import UnvalidatedAccountAnalytics from "../../components/administrator/analytics/UnvalidatedAccountAnalytics"; +import UserAccountAnalytics from "../../components/administrator/analytics/UserAccountAnalytics"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/routes/AdministratorLogs.js b/client/src/routes/admin/AdministratorLogs.js similarity index 93% rename from client/src/routes/AdministratorLogs.js rename to client/src/routes/admin/AdministratorLogs.js index 15311038..2a58ecc7 100644 --- a/client/src/routes/AdministratorLogs.js +++ b/client/src/routes/admin/AdministratorLogs.js @@ -1,4 +1,4 @@ -import AuditLogTable from "../components/administrator/AuditLogTable"; +import AuditLogTable from "../../components/administrator/AuditLogTable"; import { Box, Typography, Divider, Stack, Container } from "@mui/material"; import AdministratorApproval from "./AdministratorApproval"; diff --git a/client/src/routes/AdministratorUsers.js b/client/src/routes/admin/AdministratorUsers.js similarity index 93% rename from client/src/routes/AdministratorUsers.js rename to client/src/routes/admin/AdministratorUsers.js index f2993c17..45e469e9 100644 --- a/client/src/routes/AdministratorUsers.js +++ b/client/src/routes/admin/AdministratorUsers.js @@ -1,4 +1,4 @@ -import UserManagementTable from "../components/administrator/UserManagementTable"; +import UserManagementTable from "../../components/administrator/UserManagementTable"; import { Box, Typography, Divider, Stack, Container } from "@mui/material"; /** From c74121c526b01b07cdca54b8a47b2b20b085079c Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 12:59:25 +0930 Subject: [PATCH 054/105] Move publicly accessed routes to a public routes directory --- client/src/index.js | 10 +++++----- client/src/routes/{ => public}/ErrorPage.js | 2 +- client/src/routes/{ => public}/ForgotPassword.js | 2 +- client/src/routes/{ => public}/Login.js | 6 +++--- client/src/routes/{ => public}/Register.js | 2 +- client/src/routes/{ => public}/ResetPassword.js | 2 +- 6 files changed, 12 insertions(+), 12 deletions(-) rename client/src/routes/{ => public}/ErrorPage.js (95%) rename client/src/routes/{ => public}/ForgotPassword.js (89%) rename client/src/routes/{ => public}/Login.js (83%) rename client/src/routes/{ => public}/Register.js (87%) rename client/src/routes/{ => public}/ResetPassword.js (88%) diff --git a/client/src/index.js b/client/src/index.js index e73e71df..8412d7c0 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -7,8 +7,8 @@ import { BrowserRouter, Routes, Route } from "react-router-dom"; import ReportHistory from "./routes/standardUser/ReportHistory"; import GenerateReport from "./routes/standardUser/GenerateReport"; import HealthAnalytics from "./routes/standardUser/HealthAnalytics"; -import Login from "./routes/Login"; -import Register from "./routes/Register"; +import Login from "./routes/public/Login"; +import Register from "./routes/public/Register"; import UserLanding from "./routes/standardUser/UserLanding"; import UserSettings from "./routes/standardUser/UserSettings"; import MerchantGenerateReport from "./routes/merchant/MerchantGenerateReport"; @@ -23,12 +23,12 @@ import ProtectedRoutes from "./utils/ProtectedRoutes"; import PublicOnlyRoutes from "./utils/PublicOnlyRoutes"; import LandingRoute from "./utils/LandingRoute"; import { UserProvider } from "./utils/UserContext"; -import ErrorPage from "./routes/ErrorPage"; +import ErrorPage from "./routes/public/ErrorPage"; import PatientManagement from "./routes/merchant/PatientManagement"; import PatientDetails from "./routes/merchant/PatientDetails"; import CreatePatient from "./routes/merchant/CreatePatient"; -import ForgotPassword from "./routes/ForgotPassword"; -import ResetPassword from "./routes/ResetPassword"; +import ForgotPassword from "./routes/public/ForgotPassword"; +import ResetPassword from "./routes/public/ResetPassword"; import AcceptAccessRequest from "./routes/standardUser/AcceptAccessRequest"; import RequestPatientAccess from "./routes/merchant/RequestPatientAccess"; diff --git a/client/src/routes/ErrorPage.js b/client/src/routes/public/ErrorPage.js similarity index 95% rename from client/src/routes/ErrorPage.js rename to client/src/routes/public/ErrorPage.js index cd04b2ab..c4ca9e26 100644 --- a/client/src/routes/ErrorPage.js +++ b/client/src/routes/public/ErrorPage.js @@ -1,6 +1,6 @@ import { Typography, Button, Box, Stack } from "@mui/material"; import { useNavigate } from "react-router-dom"; -import Logo from "../assets/WellAiLogoTR.png"; +import Logo from "../../assets/WellAiLogoTR.png"; /** * A page that is displayed when a user tried to navigate a non-existent diff --git a/client/src/routes/ForgotPassword.js b/client/src/routes/public/ForgotPassword.js similarity index 89% rename from client/src/routes/ForgotPassword.js rename to client/src/routes/public/ForgotPassword.js index 2fcd8744..34d95202 100644 --- a/client/src/routes/ForgotPassword.js +++ b/client/src/routes/public/ForgotPassword.js @@ -1,5 +1,5 @@ import { Box, Container } from "@mui/material"; -import ForgotPasswordForm from "../components/authentication/ForgotPasswordForm"; +import ForgotPasswordForm from "../../components/authentication/ForgotPasswordForm"; /** * A route that provides the form for a user to reset their password by initiating diff --git a/client/src/routes/Login.js b/client/src/routes/public/Login.js similarity index 83% rename from client/src/routes/Login.js rename to client/src/routes/public/Login.js index 76d63171..bde537d5 100644 --- a/client/src/routes/Login.js +++ b/client/src/routes/public/Login.js @@ -1,7 +1,7 @@ import { Box, Container, Stack, Typography } from "@mui/material"; -import LoginForm from "../components/authentication/LoginForm"; -import Logo from "../assets/WellAiLogoTR.png"; -import WelcomePanel from "../components/WelcomePanel"; +import LoginForm from "../../components/authentication/LoginForm"; +import Logo from "../../assets/WellAiLogoTR.png"; +import WelcomePanel from "../../components/WelcomePanel"; /** * A page that introduces the service and provides the form for logging diff --git a/client/src/routes/Register.js b/client/src/routes/public/Register.js similarity index 87% rename from client/src/routes/Register.js rename to client/src/routes/public/Register.js index 9b397e6c..a10f8237 100644 --- a/client/src/routes/Register.js +++ b/client/src/routes/public/Register.js @@ -1,5 +1,5 @@ import { Container } from "@mui/material"; -import RegistrationForm from "../components/authentication/RegistrationForm"; +import RegistrationForm from "../../components/authentication/RegistrationForm"; /** * A page that displays the registration form. diff --git a/client/src/routes/ResetPassword.js b/client/src/routes/public/ResetPassword.js similarity index 88% rename from client/src/routes/ResetPassword.js rename to client/src/routes/public/ResetPassword.js index af6eaf08..c61123e8 100644 --- a/client/src/routes/ResetPassword.js +++ b/client/src/routes/public/ResetPassword.js @@ -1,5 +1,5 @@ import { Container } from "@mui/material"; -import ResetPasswordForm from "../components/authentication/ResetPasswordForm"; +import ResetPasswordForm from "../../components/authentication/ResetPasswordForm"; /** * A page that displays the form to reset a user's password using a one-time From 8081219301d00e6cc16003bfcdf048a0d1e63d22 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 13:10:21 +0930 Subject: [PATCH 055/105] Add comments to differentiate routes in index.js --- client/src/index.js | 38 ++++++++++++++++++++++++-------------- 1 file changed, 24 insertions(+), 14 deletions(-) diff --git a/client/src/index.js b/client/src/index.js index 8412d7c0..f3f78875 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -4,33 +4,43 @@ import "./index.css"; import App from "./App"; import reportWebVitals from "./reportWebVitals"; import { BrowserRouter, Routes, Route } from "react-router-dom"; -import ReportHistory from "./routes/standardUser/ReportHistory"; -import GenerateReport from "./routes/standardUser/GenerateReport"; -import HealthAnalytics from "./routes/standardUser/HealthAnalytics"; + +// Public routes import Login from "./routes/public/Login"; import Register from "./routes/public/Register"; +import ForgotPassword from "./routes/public/ForgotPassword"; +import ResetPassword from "./routes/public/ResetPassword"; +import ErrorPage from "./routes/public/ErrorPage"; + +// Standard user routes import UserLanding from "./routes/standardUser/UserLanding"; +import ReportHistory from "./routes/standardUser/ReportHistory"; +import GenerateReport from "./routes/standardUser/GenerateReport"; +import HealthAnalytics from "./routes/standardUser/HealthAnalytics"; import UserSettings from "./routes/standardUser/UserSettings"; +import AcceptAccessRequest from "./routes/standardUser/AcceptAccessRequest"; + +// Merchant routes +import MerchantLanding from "./routes/merchant/MerchantLanding"; +import MerchantReports from "./routes/merchant/MerchantReports"; import MerchantGenerateReport from "./routes/merchant/MerchantGenerateReport"; +import PatientManagement from "./routes/merchant/PatientManagement"; +import PatientDetails from "./routes/merchant/PatientDetails"; +import CreatePatient from "./routes/merchant/CreatePatient"; +import RequestPatientAccess from "./routes/merchant/RequestPatientAccess"; + +// Admin routes import AdministratorDashboard from "./routes/admin/AdministratorDashboard"; import AdministratorApproval from "./routes/admin/AdministratorApproval"; import AdministratorLogs from "./routes/admin/AdministratorLogs"; import AdministratorUsers from "./routes/admin/AdministratorUsers"; -import MerchantLanding from "./routes/merchant/MerchantLanding"; -import MerchantReports from "./routes/merchant/MerchantReports"; + +// Utils import AppThemeProvider from "./components/AppThemeProvider"; +import { UserProvider } from "./utils/UserContext"; import ProtectedRoutes from "./utils/ProtectedRoutes"; import PublicOnlyRoutes from "./utils/PublicOnlyRoutes"; import LandingRoute from "./utils/LandingRoute"; -import { UserProvider } from "./utils/UserContext"; -import ErrorPage from "./routes/public/ErrorPage"; -import PatientManagement from "./routes/merchant/PatientManagement"; -import PatientDetails from "./routes/merchant/PatientDetails"; -import CreatePatient from "./routes/merchant/CreatePatient"; -import ForgotPassword from "./routes/public/ForgotPassword"; -import ResetPassword from "./routes/public/ResetPassword"; -import AcceptAccessRequest from "./routes/standardUser/AcceptAccessRequest"; -import RequestPatientAccess from "./routes/merchant/RequestPatientAccess"; const root = ReactDOM.createRoot(document.getElementById("root")); root.render( From 64c6cfed62afc7ca2afbbe92886293d9623fb6b9 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 13:22:11 +0930 Subject: [PATCH 056/105] Move health report component files into a grouped folder --- client/src/components/{ => healthReport}/BloodReportUpload.js | 0 .../src/components/{ => healthReport}/DownloadReportButton.js | 0 .../src/components/{ => healthReport}/GenerateReportForm.js | 0 .../src/components/{ => healthReport}/HealthReportPDFFlat.js | 2 +- .../src/components/{ => healthReport}/MerchantReportForm.js | 0 client/src/components/{ => healthReport}/ReportTemplate.js | 0 client/src/routes/merchant/MerchantGenerateReport.js | 2 +- client/src/routes/merchant/MerchantReports.js | 4 ++-- client/src/routes/standardUser/GenerateReport.js | 2 +- client/src/routes/standardUser/ReportHistory.js | 4 ++-- 10 files changed, 7 insertions(+), 7 deletions(-) rename client/src/components/{ => healthReport}/BloodReportUpload.js (100%) rename client/src/components/{ => healthReport}/DownloadReportButton.js (100%) rename client/src/components/{ => healthReport}/GenerateReportForm.js (100%) rename client/src/components/{ => healthReport}/HealthReportPDFFlat.js (99%) rename client/src/components/{ => healthReport}/MerchantReportForm.js (100%) rename client/src/components/{ => healthReport}/ReportTemplate.js (100%) diff --git a/client/src/components/BloodReportUpload.js b/client/src/components/healthReport/BloodReportUpload.js similarity index 100% rename from client/src/components/BloodReportUpload.js rename to client/src/components/healthReport/BloodReportUpload.js diff --git a/client/src/components/DownloadReportButton.js b/client/src/components/healthReport/DownloadReportButton.js similarity index 100% rename from client/src/components/DownloadReportButton.js rename to client/src/components/healthReport/DownloadReportButton.js diff --git a/client/src/components/GenerateReportForm.js b/client/src/components/healthReport/GenerateReportForm.js similarity index 100% rename from client/src/components/GenerateReportForm.js rename to client/src/components/healthReport/GenerateReportForm.js diff --git a/client/src/components/HealthReportPDFFlat.js b/client/src/components/healthReport/HealthReportPDFFlat.js similarity index 99% rename from client/src/components/HealthReportPDFFlat.js rename to client/src/components/healthReport/HealthReportPDFFlat.js index 0312fe5b..9ed4645a 100644 --- a/client/src/components/HealthReportPDFFlat.js +++ b/client/src/components/healthReport/HealthReportPDFFlat.js @@ -6,7 +6,7 @@ import { StyleSheet, Image, } from "@react-pdf/renderer"; -import WellAiLogo from "../assets/WellAiLogoTR.png"; +import WellAiLogo from "../../assets/WellAiLogoTR.png"; /** * The document used for converting a health report to a PDF File. diff --git a/client/src/components/MerchantReportForm.js b/client/src/components/healthReport/MerchantReportForm.js similarity index 100% rename from client/src/components/MerchantReportForm.js rename to client/src/components/healthReport/MerchantReportForm.js diff --git a/client/src/components/ReportTemplate.js b/client/src/components/healthReport/ReportTemplate.js similarity index 100% rename from client/src/components/ReportTemplate.js rename to client/src/components/healthReport/ReportTemplate.js diff --git a/client/src/routes/merchant/MerchantGenerateReport.js b/client/src/routes/merchant/MerchantGenerateReport.js index 7fc80438..f9dc16a4 100644 --- a/client/src/routes/merchant/MerchantGenerateReport.js +++ b/client/src/routes/merchant/MerchantGenerateReport.js @@ -1,5 +1,5 @@ import { Container, ButtonGroup, Button, Box } from "@mui/material"; -import MerchantReportForm from "../../components/MerchantReportForm"; +import MerchantReportForm from "../../components/healthReport/MerchantReportForm"; import { useState } from "react"; /** diff --git a/client/src/routes/merchant/MerchantReports.js b/client/src/routes/merchant/MerchantReports.js index 075c712e..abc1a0a6 100644 --- a/client/src/routes/merchant/MerchantReports.js +++ b/client/src/routes/merchant/MerchantReports.js @@ -1,5 +1,5 @@ -import ReportTemplate from "../../components/ReportTemplate"; -import DownloadReportButton from "../../components/DownloadReportButton"; +import ReportTemplate from "../../components/healthReport/ReportTemplate"; +import DownloadReportButton from "../../components/healthReport/DownloadReportButton"; import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; diff --git a/client/src/routes/standardUser/GenerateReport.js b/client/src/routes/standardUser/GenerateReport.js index 1f4d1b01..2b49cb83 100644 --- a/client/src/routes/standardUser/GenerateReport.js +++ b/client/src/routes/standardUser/GenerateReport.js @@ -1,4 +1,4 @@ -import GenerateReportForm from "../../components/GenerateReportForm"; +import GenerateReportForm from "../../components/healthReport/GenerateReportForm"; import { Box } from "@mui/material"; /** diff --git a/client/src/routes/standardUser/ReportHistory.js b/client/src/routes/standardUser/ReportHistory.js index 738ce1b3..7f100f6b 100644 --- a/client/src/routes/standardUser/ReportHistory.js +++ b/client/src/routes/standardUser/ReportHistory.js @@ -1,5 +1,5 @@ -import ReportTemplate from "../../components/ReportTemplate"; -import DownloadReportButton from "../../components/DownloadReportButton"; +import ReportTemplate from "../../components/healthReport/ReportTemplate"; +import DownloadReportButton from "../../components/healthReport/DownloadReportButton"; import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import MenuIcon from "@mui/icons-material/Menu"; From 2a7770a4bb1b5e851ff91c4c3cd8e9f874927ca1 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 13:29:19 +0930 Subject: [PATCH 057/105] Create dialog and patientAccessRequest component folders --- client/src/components/Navbar.js | 4 ++-- client/src/components/administrator/AccountApprovalTable.js | 2 +- client/src/components/administrator/UserManagementTable.js | 2 +- client/src/components/{ => dialog}/DisclaimerPolicy.js | 0 client/src/components/{ => dialog}/PrivacyNotice.js | 0 client/src/components/{ => dialog}/confirmationDialog.js | 0 .../{ => patientAccessRequest}/AcceptRequestForm.js | 0 .../{ => patientAccessRequest}/RequestPatientAccessForm.js | 2 +- client/src/routes/merchant/MerchantReports.js | 2 +- client/src/routes/merchant/PatientManagement.js | 2 +- client/src/routes/merchant/RequestPatientAccess.js | 2 +- client/src/routes/standardUser/AcceptAccessRequest.js | 2 +- client/src/routes/standardUser/ReportHistory.js | 2 +- client/src/routes/standardUser/UserSettings.js | 2 +- 14 files changed, 11 insertions(+), 11 deletions(-) rename client/src/components/{ => dialog}/DisclaimerPolicy.js (100%) rename client/src/components/{ => dialog}/PrivacyNotice.js (100%) rename client/src/components/{ => dialog}/confirmationDialog.js (100%) rename client/src/components/{ => patientAccessRequest}/AcceptRequestForm.js (100%) rename client/src/components/{ => patientAccessRequest}/RequestPatientAccessForm.js (98%) diff --git a/client/src/components/Navbar.js b/client/src/components/Navbar.js index 3a9843b6..8a9d3eb4 100644 --- a/client/src/components/Navbar.js +++ b/client/src/components/Navbar.js @@ -18,8 +18,8 @@ import { Backdrop, } from "@mui/material"; import { UserContext } from "../utils/UserContext"; -import PrivacyNotice from "./PrivacyNotice"; -import DisclaimerPolicy from "./DisclaimerPolicy"; +import PrivacyNotice from "./dialog/PrivacyNotice"; +import DisclaimerPolicy from "./dialog/DisclaimerPolicy"; // Icons import AccountCircleIcon from "@mui/icons-material/AccountCircle"; diff --git a/client/src/components/administrator/AccountApprovalTable.js b/client/src/components/administrator/AccountApprovalTable.js index 9b2ae6cc..9d2ae3b8 100644 --- a/client/src/components/administrator/AccountApprovalTable.js +++ b/client/src/components/administrator/AccountApprovalTable.js @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; import { Paper, Button, Box } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; -import ConfirmationDialog from "../confirmationDialog"; +import ConfirmationDialog from "../dialog/confirmationDialog"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/UserManagementTable.js b/client/src/components/administrator/UserManagementTable.js index 0a9ae480..4c28ac26 100644 --- a/client/src/components/administrator/UserManagementTable.js +++ b/client/src/components/administrator/UserManagementTable.js @@ -9,7 +9,7 @@ import { } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; import { useState, useEffect, useCallback } from "react"; -import ConfirmationDialog from "../confirmationDialog"; +import ConfirmationDialog from "../dialog/confirmationDialog"; import UserSearchBar from "./UserSearchBar"; import UserToolBar from "./UserToolBar"; import * as React from "react"; diff --git a/client/src/components/DisclaimerPolicy.js b/client/src/components/dialog/DisclaimerPolicy.js similarity index 100% rename from client/src/components/DisclaimerPolicy.js rename to client/src/components/dialog/DisclaimerPolicy.js diff --git a/client/src/components/PrivacyNotice.js b/client/src/components/dialog/PrivacyNotice.js similarity index 100% rename from client/src/components/PrivacyNotice.js rename to client/src/components/dialog/PrivacyNotice.js diff --git a/client/src/components/confirmationDialog.js b/client/src/components/dialog/confirmationDialog.js similarity index 100% rename from client/src/components/confirmationDialog.js rename to client/src/components/dialog/confirmationDialog.js diff --git a/client/src/components/AcceptRequestForm.js b/client/src/components/patientAccessRequest/AcceptRequestForm.js similarity index 100% rename from client/src/components/AcceptRequestForm.js rename to client/src/components/patientAccessRequest/AcceptRequestForm.js diff --git a/client/src/components/RequestPatientAccessForm.js b/client/src/components/patientAccessRequest/RequestPatientAccessForm.js similarity index 98% rename from client/src/components/RequestPatientAccessForm.js rename to client/src/components/patientAccessRequest/RequestPatientAccessForm.js index 53653fdd..8c8e2dc0 100644 --- a/client/src/components/RequestPatientAccessForm.js +++ b/client/src/components/patientAccessRequest/RequestPatientAccessForm.js @@ -9,7 +9,7 @@ import { Link, Divider, } from "@mui/material"; -import EmailInputField from "./authentication/EmailInputField"; +import EmailInputField from "../authentication/EmailInputField"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/routes/merchant/MerchantReports.js b/client/src/routes/merchant/MerchantReports.js index abc1a0a6..03163d6c 100644 --- a/client/src/routes/merchant/MerchantReports.js +++ b/client/src/routes/merchant/MerchantReports.js @@ -4,7 +4,7 @@ import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import ConfirmationDialog from "../../components/confirmationDialog"; +import ConfirmationDialog from "../../components/dialog/confirmationDialog"; import React, { useState, useEffect } from "react"; import { useLocation } from "react-router-dom"; import { diff --git a/client/src/routes/merchant/PatientManagement.js b/client/src/routes/merchant/PatientManagement.js index 565e9175..fbf6a874 100644 --- a/client/src/routes/merchant/PatientManagement.js +++ b/client/src/routes/merchant/PatientManagement.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; -import ConfirmationDialog from "../../components/confirmationDialog"; +import ConfirmationDialog from "../../components/dialog/confirmationDialog"; import InputAdornment from "@mui/material/InputAdornment"; import { Box, diff --git a/client/src/routes/merchant/RequestPatientAccess.js b/client/src/routes/merchant/RequestPatientAccess.js index 16afe84f..7c8e2a6b 100644 --- a/client/src/routes/merchant/RequestPatientAccess.js +++ b/client/src/routes/merchant/RequestPatientAccess.js @@ -8,7 +8,7 @@ import { Divider, } from "@mui/material"; import ForgotPasswordForm from "../../components/authentication/ForgotPasswordForm"; -import RequestPatientAccessForm from "../../components/RequestPatientAccessForm"; +import RequestPatientAccessForm from "../../components/patientAccessRequest/RequestPatientAccessForm"; /** * A page that displays the form to request access to a patients record. diff --git a/client/src/routes/standardUser/AcceptAccessRequest.js b/client/src/routes/standardUser/AcceptAccessRequest.js index c1b670e4..853b6927 100644 --- a/client/src/routes/standardUser/AcceptAccessRequest.js +++ b/client/src/routes/standardUser/AcceptAccessRequest.js @@ -7,7 +7,7 @@ import { Link, Divider, } from "@mui/material"; -import AcceptRequestForm from "../../components/AcceptRequestForm"; +import AcceptRequestForm from "../../components/patientAccessRequest/AcceptRequestForm"; /** * A page that displays the accept access request form for users to diff --git a/client/src/routes/standardUser/ReportHistory.js b/client/src/routes/standardUser/ReportHistory.js index 7f100f6b..81098b0f 100644 --- a/client/src/routes/standardUser/ReportHistory.js +++ b/client/src/routes/standardUser/ReportHistory.js @@ -3,7 +3,7 @@ import DownloadReportButton from "../../components/healthReport/DownloadReportBu import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; import MenuIcon from "@mui/icons-material/Menu"; -import ConfirmationDialog from "../../components/confirmationDialog"; +import ConfirmationDialog from "../../components/dialog/confirmationDialog"; import Stack from "@mui/material/Stack"; import React, { useState, useEffect } from "react"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; diff --git a/client/src/routes/standardUser/UserSettings.js b/client/src/routes/standardUser/UserSettings.js index 93de7862..866d0f85 100644 --- a/client/src/routes/standardUser/UserSettings.js +++ b/client/src/routes/standardUser/UserSettings.js @@ -18,7 +18,7 @@ import { Tabs, Tab, } from "@mui/material"; -import ConfirmationDialog from "../../components/confirmationDialog"; +import ConfirmationDialog from "../../components/dialog/confirmationDialog"; import { useNavigate } from "react-router-dom"; import { UserContext } from "../../utils/UserContext"; import PhoneInputField from "../../components/authentication/PhoneInputField"; From 3af655e330d2d8b11d6a2ab62abec479e9ba389f Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sat, 23 May 2026 14:08:19 +0930 Subject: [PATCH 058/105] Remove unused imports --- client/src/routes/ReportHistory.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/client/src/routes/ReportHistory.js b/client/src/routes/ReportHistory.js index 74309478..0ebae81d 100644 --- a/client/src/routes/ReportHistory.js +++ b/client/src/routes/ReportHistory.js @@ -20,12 +20,10 @@ import { Select, MenuItem, Button, - Drawer, useTheme, useMediaQuery, Paper, } from "@mui/material"; -import { textAlign } from "@mui/system"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; From 0df3341f49128dd8a0e9405ea5748a0f9dc66a09 Mon Sep 17 00:00:00 2001 From: birjd002 Date: Sat, 23 May 2026 15:22:49 +0930 Subject: [PATCH 059/105] feat: add chart component for PDF rendering --- client/src/components/PDFHealthChart.js | 252 ++++++++++++++++++++++++ 1 file changed, 252 insertions(+) create mode 100644 client/src/components/PDFHealthChart.js diff --git a/client/src/components/PDFHealthChart.js b/client/src/components/PDFHealthChart.js new file mode 100644 index 00000000..4e53ce02 --- /dev/null +++ b/client/src/components/PDFHealthChart.js @@ -0,0 +1,252 @@ +import React, { useMemo, forwardRef } from "react"; +import { + Box, + Typography, + Card, + CardContent, + useTheme, +} from "@mui/material"; +import { LineChart } from "@mui/x-charts/LineChart"; + +const formatXAxisLabel = (label, index, total) => { + if (!label) { + return label; + } + + // Thin out middle ticks for crowded datasets on smaller screens + if (total > 4 && index > 0 && index < total - 1) { + if (total > 6 && index % 2 === 1) { + return ""; + } + } + + if (/^\d{4}-\d{2}-\d{2}$/.test(label)) { + const [, month, day] = label.split("-"); + return `${month}/${day}`; + } + + const parts = label.split(" "); + if (parts.length === 2 && parts[1].length === 4) { + return parts[0]; + } + + return label; +}; + +/** + * A Component that displays the trends in the user's health based on + * their report history. Used for PDF download. + * + * @returns {@mui.material.Box} + */ +const PDFHealthChart = forwardRef(({ healthData }, ref) => { + const theme = useTheme(); + + // Helpers for aggregation and y-axis scaling + const colors = [theme.palette.primary.main, "#ff7043", "#42a5f5"]; + + const { xAxisData, chartSeries, yAxisMax } = useMemo(() => { + if (!healthData || healthData.length === 0) { + return { xAxisData: [], chartSeries: [], yAxisMax: 60 }; + } + + // Parse dates and sort ascending + const parsed = healthData + .map((d) => { + const dt = d.date ? new Date(d.date) : null; + return { ...d, _dateObj: dt }; + }) + .filter((d) => d._dateObj && !isNaN(d._dateObj.getTime())) + .sort((a, b) => a._dateObj - b._dateObj); + + if (parsed.length === 0) { + return { xAxisData: [], chartSeries: [], yAxisMax: 60 }; + } + + const minDate = parsed[0]._dateObj; + const maxDate = parsed[parsed.length - 1]._dateObj; + const crossYear = minDate.getFullYear() !== maxDate.getFullYear(); + const diffDays = (maxDate - minDate) / (1000 * 60 * 60 * 24); + + const needAggregateMonthly = + crossYear || diffDays > 180 || parsed.length > 20; + + let working = []; + let xLabels = []; + + if (needAggregateMonthly) { + // Group by year-month and compute averages + const groups = new Map(); + for (const d of parsed) { + const y = d._dateObj.getFullYear(); + const m = d._dateObj.getMonth(); // 0-11 + const key = `${y}-${m}`; + if (!groups.has(key)) { + groups.set(key, { y, m, items: [] }); + } + groups.get(key).items.push(d); + } + + const monthNames = [ + "Jan", + "Feb", + "Mar", + "Apr", + "May", + "Jun", + "Jul", + "Aug", + "Sep", + "Oct", + "Nov", + "Dec", + ]; + const aggregated = Array.from(groups.values()) + .sort((a, b) => a.y - b.y || a.m - b.m) + .map(({ y, m, items }) => { + const n = items.length || 1; + const avg = (arr, key) => + arr.reduce((sum, it) => sum + (Number(it[key]) || 0), 0) / n; + return { + label: `${monthNames[m]} ${y}`, + strokeProbability: avg(items, "strokeProbability"), + cardioProbability: avg(items, "cardioProbability"), + diabetesProbability: avg(items, "diabetesProbability"), + }; + }); + + working = aggregated; + xLabels = aggregated.map((a) => a.label); + } else { + // Use precise date points; format label as YYYY-MM-DD + const formatDate = (dt) => { + const y = dt.getFullYear(); + const m = String(dt.getMonth() + 1).padStart(2, "0"); + const d = String(dt.getDate()).padStart(2, "0"); + return `${y}-${m}-${d}`; + }; + working = parsed; + xLabels = parsed.map((d) => formatDate(d._dateObj)); + } + + // Build series based on selection + const series = [ + { + data: working.map((w) => Number(w.strokeProbability) || 0), + label: "Stroke Probability (%)", + color: colors[0], + }, + { + data: working.map((w) => Number(w.cardioProbability) || 0), + label: "Cardio Probability (%)", + color: colors[1], + }, + { + data: working.map((w) => Number(w.diabetesProbability) || 0), + label: "Diabetes Probability (%)", + color: colors[2], + } + ]; + + // Dynamic y-axis max with padding, capped at 100 + let maxVal = 0; + for (const s of series) { + for (const v of s.data) maxVal = Math.max(maxVal, Number(v) || 0); + } + const padded = Math.min(100, Math.max(0, maxVal + 5)); + const yMax = Math.max(10, Math.min(100, Math.ceil(padded / 10) * 10)); + + return { xAxisData: xLabels, chartSeries: series, yAxisMax: yMax }; + }, [healthData, colors]); + + const xAxisValueFormatter = useMemo(() => { + if (!xAxisData || xAxisData.length === 0) { + return undefined; + } + + return (value) => { + const index = xAxisData.indexOf(value); + if (index === -1) { + return value; + } + return formatXAxisLabel(value, index, xAxisData.length); + }; + }, [xAxisData]); + + return ( + + + + + Health Risk Trends Over Time + + + + + + + + ); +}); + +export default PDFHealthChart; \ No newline at end of file From 6a1dd66e14adad85f0561c0068511458e43e104c Mon Sep 17 00:00:00 2001 From: birjd002 Date: Sun, 24 May 2026 00:02:24 +0930 Subject: [PATCH 060/105] feat: add PDFHealthChart component to ReportHistory --- client/package-lock.json | 95 ++++++++++++++++++++++++------ client/package.json | 1 + client/src/routes/ReportHistory.js | 20 ++++++- 3 files changed, 98 insertions(+), 18 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 04b79646..1b7d690e 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -24,6 +24,7 @@ "bootstrap": "^5.3.8", "dayjs": "^1.11.20", "file-saver": "^2.0.5", + "html2canvas": "^1.4.1", "libphonenumber-js": "^1.12.24", "react": "^19.1.1", "react-dom": "^19.1.1", @@ -86,6 +87,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", @@ -741,6 +743,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.27.1.tgz", "integrity": "sha512-p9OkPbZ5G7UT1MofwYFigGebnrzGJacoBSQM0/6bi/PUMVE+qlWDD/OalvQKbwgQzU6dl0xAv6r4X7Jme0RYxA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, @@ -1624,6 +1627,7 @@ "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.1", "@babel/helper-module-imports": "^7.27.1", @@ -2452,6 +2456,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -2495,6 +2500,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -3175,6 +3181,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-7.3.4.tgz", "integrity": "sha512-gEQL9pbJZZHT7lYJBKQCS723v1MGys2IFc94COXbUIyCTWa+qC77a7hUax4Yjd5ggEm35dk4AyYABpKKWC4MLw==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.4", @@ -3285,6 +3292,7 @@ "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.3.tgz", "integrity": "sha512-Lqq3emZr5IzRLKaHPuMaLBDVaGvxoh6z7HMWd1RPKawBM5uMRaQ4ImsmmgXWtwJdfZux5eugfDhXJUo2mliS8Q==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.3", @@ -3952,6 +3960,7 @@ "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", "license": "MIT", + "peer": true, "funding": { "type": "opencollective", "url": "https://opencollective.com/popperjs" @@ -4493,6 +4502,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "license": "MIT", + "peer": true, "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", @@ -5103,6 +5113,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz", "integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/regexpp": "^4.4.0", "@typescript-eslint/scope-manager": "5.62.0", @@ -5156,6 +5167,7 @@ "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz", "integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==", "license": "BSD-2-Clause", + "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "5.62.0", "@typescript-eslint/types": "5.62.0", @@ -5531,6 +5543,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5629,6 +5642,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -6374,6 +6388,15 @@ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "license": "MIT" }, + "node_modules/base64-arraybuffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz", + "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -6630,6 +6653,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "baseline-browser-mapping": "^2.8.9", "caniuse-lite": "^1.0.30001746", @@ -7474,6 +7498,15 @@ "postcss": "^8.4" } }, + "node_modules/css-line-break": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz", + "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/css-loader": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", @@ -8805,6 +8838,7 @@ "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -10791,6 +10825,19 @@ } } }, + "node_modules/html2canvas": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz", + "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==", + "license": "MIT", + "dependencies": { + "css-line-break": "^2.1.0", + "text-segmentation": "^1.0.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/htmlparser2": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", @@ -11846,6 +11893,7 @@ "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", "integrity": "sha512-Yn0mADZB89zTtjkPJEXwrac3LHudkQMR+Paqa8uxJHCBr9agxztUifWCyiYrjhMPBoUVBjyny0I7XH6ozDr7QQ==", "license": "MIT", + "peer": true, "dependencies": { "@jest/core": "^27.5.1", "import-local": "^3.0.2", @@ -12731,6 +12779,7 @@ "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", "license": "MIT", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -14930,6 +14979,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -16064,6 +16114,7 @@ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "license": "MIT", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -16556,6 +16607,7 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -16687,6 +16739,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", + "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -16821,6 +16874,7 @@ "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz", "integrity": "sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -17369,6 +17423,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", "license": "MIT", + "peer": true, "bin": { "rollup": "dist/bin/rollup" }, @@ -17611,6 +17666,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", @@ -19040,23 +19096,6 @@ } } }, - "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/tapable": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", @@ -19234,6 +19273,15 @@ "node": ">=8" } }, + "node_modules/text-segmentation": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", + "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==", + "license": "MIT", + "dependencies": { + "utrie": "^1.0.2" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -19479,6 +19527,7 @@ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "license": "(MIT OR CC0-1.0)", + "peer": true, "engines": { "node": ">=10" }, @@ -19931,6 +19980,15 @@ "node": ">= 0.4.0" } }, + "node_modules/utrie": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", + "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==", + "license": "MIT", + "dependencies": { + "base64-arraybuffer": "^1.0.2" + } + }, "node_modules/uuid": { "version": "8.3.2", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", @@ -20105,6 +20163,7 @@ "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.102.0.tgz", "integrity": "sha512-hUtqAR3ZLVEYDEABdBioQCIqSoguHbFn1K7WlPPWSuXmx0031BD73PSE35jKyftdSh4YLDoQNgK4pqBt5Q82MA==", "license": "MIT", + "peer": true, "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.8", @@ -20176,6 +20235,7 @@ "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", "license": "MIT", + "peer": true, "dependencies": { "@types/bonjour": "^3.5.9", "@types/connect-history-api-fallback": "^1.3.5", @@ -20588,6 +20648,7 @@ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "license": "MIT", + "peer": true, "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", diff --git a/client/package.json b/client/package.json index 4f779057..5e48a6eb 100644 --- a/client/package.json +++ b/client/package.json @@ -19,6 +19,7 @@ "bootstrap": "^5.3.8", "dayjs": "^1.11.20", "file-saver": "^2.0.5", + "html2canvas": "^1.4.1", "libphonenumber-js": "^1.12.24", "react": "^19.1.1", "react-dom": "^19.1.1", diff --git a/client/src/routes/ReportHistory.js b/client/src/routes/ReportHistory.js index c67ceeb0..673b9775 100644 --- a/client/src/routes/ReportHistory.js +++ b/client/src/routes/ReportHistory.js @@ -5,9 +5,10 @@ import IconButton from "@mui/material/IconButton"; import MenuIcon from "@mui/icons-material/Menu"; import ConfirmationDialog from "../components/confirmationDialog"; import Stack from "@mui/material/Stack"; -import React, { useState, useEffect } from "react"; +import React, { useState, useEffect, useRef } from "react"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import PDFHealthChart from "../components/PDFHealthChart"; import { Box, @@ -44,6 +45,8 @@ const AIHealthPrediction = ({}) => { const [selectedMonth, setSelectedMonth] = useState(null); const [selectedYear, setSelectedYear] = useState(null); const [isOpen, setIsOpen] = useState(false); + const chartRef = useRef(null); + const [chartData, setChartData] = useState([]); function openBar() { if (isOpen === true) { @@ -112,6 +115,16 @@ const AIHealthPrediction = ({}) => { setDeleteDialogOpen(false); } + // Health Analytics for Chart + useEffect(() => { + fetch(`${API_BASE}/health-analytics`, { + credentials: "include", + }) + .then((r) => r.json()) + .then(setChartData) + .catch(console.error); + }, []); + // Extract and sort month and years for drop down. const years = [ ...new Set(reportDates.map((r) => new Date(r.date).getFullYear())), @@ -162,6 +175,10 @@ const AIHealthPrediction = ({}) => { mt: "66px", }} > +
+ +
+ { Date: Sun, 24 May 2026 00:07:13 +0930 Subject: [PATCH 061/105] update: convert PDFHealthChart DOM node to png in DownloadReportButton --- client/src/components/DownloadReportButton.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/client/src/components/DownloadReportButton.js b/client/src/components/DownloadReportButton.js index d7c14104..1c79aa84 100644 --- a/client/src/components/DownloadReportButton.js +++ b/client/src/components/DownloadReportButton.js @@ -3,6 +3,8 @@ import { saveAs } from "file-saver"; import HealthReportPDFFlat from "./HealthReportPDFFlat"; import { Button } from "@mui/material"; import DownloadIcon from "@mui/icons-material/Download"; +import html2canvas from "html2canvas"; + /** * A simple button component to trigger a PDF report download for a specific health data ID. @@ -23,6 +25,7 @@ const DownloadReportButton = ({ flatReportData, meta, onError, + chartRef, }) => { const handleDownload = async () => { if (!flatReportData && !healthDataId) { @@ -46,11 +49,16 @@ const DownloadReportButton = ({ const fileName = meta?.fileNameHint || `HealthReport_${String(meta?.healthDataID ?? healthDataId)}_${buildLocalDatePart(meta?.date)}.pdf`; + + const canvas = await html2canvas(chartRef.current); + const chartImage = canvas.toDataURL("image/png"); + const blob = await pdf( , ).toBlob(); saveAs(blob, fileName); From e687363db6ce5f353279be811735e19344fe9bc2 Mon Sep 17 00:00:00 2001 From: birjd002 Date: Sun, 24 May 2026 00:09:10 +0930 Subject: [PATCH 062/105] feat: embed chartImage into PDF under session 3 Also removed title text and adjusted width of chart --- client/src/components/HealthReportPDFFlat.js | 23 +++++++++++++++++++- client/src/components/PDFHealthChart.js | 9 +------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/client/src/components/HealthReportPDFFlat.js b/client/src/components/HealthReportPDFFlat.js index 0312fe5b..20664682 100644 --- a/client/src/components/HealthReportPDFFlat.js +++ b/client/src/components/HealthReportPDFFlat.js @@ -98,6 +98,13 @@ const styles = StyleSheet.create({ textDecoration: "underline", marginBottom: 5, }, + chartSessionTitle: { + fontFamily: "Helvetica-Oblique", + fontSize: 12, + textDecoration: "underline", + marginBottom: 5, + marginTop: 30, + }, table: { borderWidth: 1, borderColor: "#000", @@ -191,6 +198,10 @@ const styles = StyleSheet.create({ fontSize: 9, color: "#888", }, + chartImage: { + marginTop: 5, + marginBottom: 30, + }, }); const getRisk = (val) => { @@ -278,7 +289,7 @@ const renderParagraph = (text) => { * @param {Object} [props.metaDate] - Date the health report was generated * @returns {@react-pdf.renderer.Document} */ -const HealthReportPDFFlat = ({ data, metaId, metaDate }) => { +const HealthReportPDFFlat = ({ data, metaId, metaDate, chartImage }) => { const dateObj = metaDate ? new Date(metaDate) : new Date(); const dateStr = dateObj.toLocaleDateString("en-GB", { day: "numeric", @@ -437,6 +448,16 @@ const HealthReportPDFFlat = ({ data, metaId, metaDate }) => { {renderParagraph(data.exerciseRecommendation)} + {/* Session 3 */} + + Session 3 – Health Trend Graph + + + + {/* Break hint: we want disclaimer to either stay at the end or push to next page. */} Disclaimer: The information provided by this health prediction diff --git a/client/src/components/PDFHealthChart.js b/client/src/components/PDFHealthChart.js index 4e53ce02..bdc3e7c6 100644 --- a/client/src/components/PDFHealthChart.js +++ b/client/src/components/PDFHealthChart.js @@ -177,18 +177,11 @@ const PDFHealthChart = forwardRef(({ healthData }, ref) => { - - Health Risk Trends Over Time - Date: Sun, 24 May 2026 01:00:44 +0930 Subject: [PATCH 063/105] feat: add chart component to MerchantReports Also modified get_health_analytics to pass an optional health_data_id param to retrieve the patient id. --- client/src/routes/MerchantReports.js | 23 ++++++++++++++++++++++- server/routers/users.py | 27 ++++++++++++++++++++++++--- 2 files changed, 46 insertions(+), 4 deletions(-) diff --git a/client/src/routes/MerchantReports.js b/client/src/routes/MerchantReports.js index 39323b87..01a5db3b 100644 --- a/client/src/routes/MerchantReports.js +++ b/client/src/routes/MerchantReports.js @@ -5,7 +5,8 @@ import IconButton from "@mui/material/IconButton"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import ConfirmationDialog from "../components/confirmationDialog"; -import React, { useState, useEffect } from "react"; +import PDFHealthChart from "../components/PDFHealthChart"; +import React, { useState, useEffect, useRef } from "react"; import { useLocation } from "react-router-dom"; import { Box, @@ -51,6 +52,8 @@ const MerchantReports = ({}) => { const [reports, setReports] = useState([]); // Stores all report data const [selectedMonth, setSelectedMonth] = useState(null); const [selectedYear, setSelectedYear] = useState(null); + const chartRef = useRef(null); + const [chartData, setChartData] = useState([]); function fetchMerchantReports() { fetch(`${API_BASE}/merchants/reports`, { @@ -132,6 +135,19 @@ const MerchantReports = ({}) => { setDeleteDialogOpen(false); } + // Health Analytics for Chart + useEffect(() => { + if (!selectedDate) { + return; + } + fetch(`${API_BASE}/health-analytics?health_data_id=${selectedDate.healthDataId}`, { + credentials: "include", + }) + .then((r) => r.json()) + .then(setChartData) + .catch(console.error); + }, [selectedDate]); + // Extract and sort month and years for drop down. const years = [ ...new Set(reportDates.map((r) => new Date(r.date).getFullYear())), @@ -181,6 +197,10 @@ const MerchantReports = ({}) => { mt: "66px", }} > +
+ +
+ {/* Top Bar */} { Date: Sun, 24 May 2026 01:55:28 +0930 Subject: [PATCH 064/105] fix: remove drawer component Accidentally merged into branch --- client/src/routes/MerchantReports.js | 11 ----------- client/src/routes/ReportHistory.js | 12 ------------ 2 files changed, 23 deletions(-) diff --git a/client/src/routes/MerchantReports.js b/client/src/routes/MerchantReports.js index 20fd4bb8..fd8709b1 100644 --- a/client/src/routes/MerchantReports.js +++ b/client/src/routes/MerchantReports.js @@ -203,17 +203,6 @@ const MerchantReports = ({}) => { - {/* Top Bar */} - setIsOpen(false)} - variant="temporary" - PaperProps={{ - sx: { - p: 2, - maxHeight: "50vh", - }, { - setIsOpen(false)} - variant="temporary" - PaperProps={{ - sx: { - p: 2, - maxHeight: "50vh", - }, - }} - > Date: Sun, 24 May 2026 11:44:23 +0930 Subject: [PATCH 065/105] Remove unused imports from login page --- client/src/components/authentication/LoginForm.js | 9 +++------ client/src/routes/public/Login.js | 3 +-- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/client/src/components/authentication/LoginForm.js b/client/src/components/authentication/LoginForm.js index 4a2e187b..6699f1f1 100644 --- a/client/src/components/authentication/LoginForm.js +++ b/client/src/components/authentication/LoginForm.js @@ -1,20 +1,17 @@ -import { useNavigate, useLocation, Link as RouterLink } from "react-router-dom"; -import { useContext, useState } from "react"; import { Alert, - Box, Button, - Container, Card, CardContent, Divider, - Link, Stack, Typography, } from "@mui/material"; +import { useContext, useState } from "react"; +import { Link as RouterLink, useLocation, useNavigate } from "react-router-dom"; +import { UserContext } from "../../utils/UserContext"; import EmailInputField from "../authentication/EmailInputField"; import PasswordInputField from "../authentication/PasswordInputField"; -import { UserContext } from "../../utils/UserContext"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/routes/public/Login.js b/client/src/routes/public/Login.js index bde537d5..1f3ed332 100644 --- a/client/src/routes/public/Login.js +++ b/client/src/routes/public/Login.js @@ -1,6 +1,5 @@ -import { Box, Container, Stack, Typography } from "@mui/material"; +import { Box, Stack } from "@mui/material"; import LoginForm from "../../components/authentication/LoginForm"; -import Logo from "../../assets/WellAiLogoTR.png"; import WelcomePanel from "../../components/WelcomePanel"; /** From fe434fb12e8e5fd1487a40741e217567ad240cc8 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 11:51:42 +0930 Subject: [PATCH 066/105] Remove unused imports from all pages --- .../administrator/AccountApprovalTable.js | 4 +-- .../administrator/AuditLogSearchBar.js | 2 +- .../components/administrator/AuditLogTable.js | 14 ++++---- .../administrator/UserManagementTable.js | 14 ++------ .../components/administrator/UserSearchBar.js | 4 +-- .../components/administrator/UserToolBar.js | 2 +- .../analytics/ActiveMerchantsAnalytics.js | 10 +----- .../analytics/ActiveUsersAnalytics.js | 10 +----- .../analytics/AverageRiskSeriesAnalytics.js | 15 ++++---- .../analytics/LoginActivityAnalytics.js | 13 ++----- .../analytics/PendingMerchantsAnalytics.js | 12 ++----- .../RecentReportsGeneratedAnalytics.js | 10 +----- .../analytics/UnvalidatedAccountAnalytics.js | 10 +----- .../analytics/UserAccountAnalytics.js | 11 +----- .../authentication/CreatePatientForm.js | 20 +++++------ .../authentication/EmailInputField.js | 2 +- .../authentication/ForgotPasswordForm.js | 10 +++--- .../authentication/PhoneInputField.js | 6 ++-- .../authentication/RegistrationForm.js | 14 ++++---- .../authentication/ResetPasswordForm.js | 8 ++--- .../src/components/dialog/DisclaimerPolicy.js | 6 ++-- client/src/components/dialog/PrivacyNotice.js | 6 ++-- .../components/dialog/confirmationDialog.js | 2 +- .../src/routes/admin/AdministratorApproval.js | 2 +- .../routes/admin/AdministratorDashboard.js | 4 --- client/src/routes/admin/AdministratorLogs.js | 3 +- client/src/routes/admin/AdministratorUsers.js | 2 +- .../routes/merchant/MerchantGenerateReport.js | 4 +-- client/src/routes/merchant/MerchantLanding.js | 3 +- client/src/routes/merchant/MerchantReports.js | 30 +++++++--------- client/src/routes/merchant/PatientDetails.js | 18 ++++++---- .../src/routes/merchant/PatientManagement.js | 18 +++++----- .../routes/merchant/RequestPatientAccess.js | 11 +----- client/src/routes/public/ErrorPage.js | 3 +- .../standardUser/AcceptAccessRequest.js | 10 +----- .../src/routes/standardUser/GenerateReport.js | 2 +- .../routes/standardUser/HealthAnalytics.js | 24 ++++++------- .../src/routes/standardUser/ReportHistory.js | 34 ++++++++++--------- client/src/routes/standardUser/UserLanding.js | 8 ++--- .../src/routes/standardUser/UserSettings.js | 26 +++++++------- client/src/utils/LandingRoute.js | 2 +- client/src/utils/ProtectedRoutes.js | 2 +- client/src/utils/PublicOnlyRoutes.js | 3 +- client/src/utils/UserContext.js | 4 +-- 44 files changed, 165 insertions(+), 253 deletions(-) diff --git a/client/src/components/administrator/AccountApprovalTable.js b/client/src/components/administrator/AccountApprovalTable.js index 9d2ae3b8..efc4df2b 100644 --- a/client/src/components/administrator/AccountApprovalTable.js +++ b/client/src/components/administrator/AccountApprovalTable.js @@ -1,6 +1,6 @@ -import { useState, useEffect } from "react"; -import { Paper, Button, Box } from "@mui/material"; +import { Box, Button, Paper } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; +import { useEffect, useState } from "react"; import ConfirmationDialog from "../dialog/confirmationDialog"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/AuditLogSearchBar.js b/client/src/components/administrator/AuditLogSearchBar.js index b98af4f3..f26e9740 100644 --- a/client/src/components/administrator/AuditLogSearchBar.js +++ b/client/src/components/administrator/AuditLogSearchBar.js @@ -1,5 +1,5 @@ import SearchIcon from "@mui/icons-material/Search"; -import { TextField, InputAdornment, Box } from "@mui/material"; +import { Box, InputAdornment, TextField } from "@mui/material"; import { memo, useEffect, useState } from "react"; /** diff --git a/client/src/components/administrator/AuditLogTable.js b/client/src/components/administrator/AuditLogTable.js index 7e4c241e..68843858 100644 --- a/client/src/components/administrator/AuditLogTable.js +++ b/client/src/components/administrator/AuditLogTable.js @@ -1,14 +1,14 @@ -import { useCallback, useEffect, useState } from "react"; -import { DataGrid } from "@mui/x-data-grid"; import { - Paper, Box, - Select, - MenuItem, - InputLabel, - FormControl, Divider, + FormControl, + InputLabel, + MenuItem, + Paper, + Select, } from "@mui/material"; +import { DataGrid } from "@mui/x-data-grid"; +import { useCallback, useEffect, useState } from "react"; import AuditLogSearchBar from "./AuditLogSearchBar"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/UserManagementTable.js b/client/src/components/administrator/UserManagementTable.js index 4c28ac26..45e83bcb 100644 --- a/client/src/components/administrator/UserManagementTable.js +++ b/client/src/components/administrator/UserManagementTable.js @@ -1,18 +1,10 @@ -import { - Paper, - Box, - Snackbar, - Alert, - Stack, - Typography, - Divider, -} from "@mui/material"; +import { Alert, Box, Divider, Paper, Snackbar } from "@mui/material"; import { DataGrid } from "@mui/x-data-grid"; -import { useState, useEffect, useCallback } from "react"; +import * as React from "react"; +import { useCallback, useEffect, useState } from "react"; import ConfirmationDialog from "../dialog/confirmationDialog"; import UserSearchBar from "./UserSearchBar"; import UserToolBar from "./UserToolBar"; -import * as React from "react"; /** * A table listing all the user accounts in the system with tools to search diff --git a/client/src/components/administrator/UserSearchBar.js b/client/src/components/administrator/UserSearchBar.js index bc72c3ed..595f84e3 100644 --- a/client/src/components/administrator/UserSearchBar.js +++ b/client/src/components/administrator/UserSearchBar.js @@ -1,7 +1,7 @@ import SearchIcon from "@mui/icons-material/Search"; -import { TextField, Box } from "@mui/material"; +import { Box, TextField } from "@mui/material"; import InputAdornment from "@mui/material/InputAdornment"; -import { useEffect, useState, memo } from "react"; +import { memo, useEffect, useState } from "react"; /** * A search bar that is used as an element in the UserManagementTable to diff --git a/client/src/components/administrator/UserToolBar.js b/client/src/components/administrator/UserToolBar.js index e57404a7..495d0a69 100644 --- a/client/src/components/administrator/UserToolBar.js +++ b/client/src/components/administrator/UserToolBar.js @@ -1,5 +1,5 @@ -import { Box, Select, Button, MenuItem } from "@mui/material"; import DeleteForeverIcon from "@mui/icons-material/DeleteForever"; +import { Box, Button, MenuItem, Select } from "@mui/material"; /** * A tool bar to that provides the controls for modifying and filtering user diff --git a/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js b/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js index cec30903..37ba18b6 100644 --- a/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js +++ b/client/src/components/administrator/analytics/ActiveMerchantsAnalytics.js @@ -1,12 +1,4 @@ -import { - Card, - CardContent, - Container, - Divider, - Paper, - Stack, - Typography, -} from "@mui/material"; +import { Card, CardContent, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/analytics/ActiveUsersAnalytics.js b/client/src/components/administrator/analytics/ActiveUsersAnalytics.js index 782c02bc..079e8ab5 100644 --- a/client/src/components/administrator/analytics/ActiveUsersAnalytics.js +++ b/client/src/components/administrator/analytics/ActiveUsersAnalytics.js @@ -1,12 +1,4 @@ -import { - Card, - CardContent, - Container, - Divider, - Paper, - Stack, - Typography, -} from "@mui/material"; +import { Card, CardContent, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js b/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js index b2351026..d31c06f4 100644 --- a/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js +++ b/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js @@ -1,20 +1,17 @@ import { Card, CardContent, - Container, - Paper, - Typography, - Select, - MenuItem, + FormControl, Grid, InputLabel, - FormControl, + MenuItem, + Select, + Typography, } from "@mui/material"; -import { Stack } from "@mui/system"; -import { useEffect, useState } from "react"; -import { LineChart } from "@mui/x-charts/LineChart"; import Divider from "@mui/material/Divider"; +import { LineChart } from "@mui/x-charts/LineChart"; import dayjs from "dayjs"; +import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; const DEFAULT_YEAR = 2026; diff --git a/client/src/components/administrator/analytics/LoginActivityAnalytics.js b/client/src/components/administrator/analytics/LoginActivityAnalytics.js index 32b816a3..8240027a 100644 --- a/client/src/components/administrator/analytics/LoginActivityAnalytics.js +++ b/client/src/components/administrator/analytics/LoginActivityAnalytics.js @@ -1,21 +1,14 @@ -import dayjs from "dayjs"; import { Box, Card, CardContent, - Container, - FormControl, + Divider, Grid, - InputLabel, - MenuItem, - Paper, - Select, - Stack, Typography, - Divider, } from "@mui/material"; -import { useEffect, useState } from "react"; import { BarChart } from "@mui/x-charts"; +import dayjs from "dayjs"; +import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; const TIMESPAN_IN_DAYS = 7; diff --git a/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js b/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js index 0af169c0..8a43387b 100644 --- a/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js +++ b/client/src/components/administrator/analytics/PendingMerchantsAnalytics.js @@ -1,14 +1,6 @@ -import { - Card, - CardContent, - Container, - Divider, - Paper, - Stack, - Typography, -} from "@mui/material"; -import { useEffect, useState } from "react"; import ErrorIcon from "@mui/icons-material/Error"; +import { Card, CardContent, Stack, Typography } from "@mui/material"; +import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js b/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js index 8981e19b..8ab2e2d9 100644 --- a/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js +++ b/client/src/components/administrator/analytics/RecentReportsGeneratedAnalytics.js @@ -1,12 +1,4 @@ -import { - Card, - CardContent, - Container, - Divider, - Paper, - Stack, - Typography, -} from "@mui/material"; +import { Card, CardContent, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js b/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js index cc5b4d90..68154c49 100644 --- a/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js +++ b/client/src/components/administrator/analytics/UnvalidatedAccountAnalytics.js @@ -1,12 +1,4 @@ -import { - Card, - CardContent, - Container, - Divider, - Paper, - Stack, - Typography, -} from "@mui/material"; +import { Card, CardContent, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/administrator/analytics/UserAccountAnalytics.js b/client/src/components/administrator/analytics/UserAccountAnalytics.js index 48d41840..ac4917e9 100644 --- a/client/src/components/administrator/analytics/UserAccountAnalytics.js +++ b/client/src/components/administrator/analytics/UserAccountAnalytics.js @@ -1,13 +1,4 @@ -import { - Card, - CardContent, - Container, - Divider, - Grid, - Paper, - Stack, - Typography, -} from "@mui/material"; +import { Card, CardContent, Grid, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/authentication/CreatePatientForm.js b/client/src/components/authentication/CreatePatientForm.js index ee30e215..d58c925b 100644 --- a/client/src/components/authentication/CreatePatientForm.js +++ b/client/src/components/authentication/CreatePatientForm.js @@ -1,23 +1,23 @@ -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; import { - Container, - Paper, - Stack, - TextField, - Button, Alert, + Button, + Container, Dialog, - DialogTitle, DialogActions, + DialogTitle, Divider, + FormControl, + FormHelperText, InputLabel, MenuItem, - FormControl, + Paper, Select, - FormHelperText, + Stack, + TextField, Typography, } from "@mui/material"; +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; const FULL_NAME_MAX_LENGTH = 255; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/authentication/EmailInputField.js b/client/src/components/authentication/EmailInputField.js index 6a992aec..509d8aff 100644 --- a/client/src/components/authentication/EmailInputField.js +++ b/client/src/components/authentication/EmailInputField.js @@ -1,5 +1,5 @@ -import { useState } from "react"; import { TextField } from "@mui/material"; +import { useState } from "react"; import validator from "validator"; /** diff --git a/client/src/components/authentication/ForgotPasswordForm.js b/client/src/components/authentication/ForgotPasswordForm.js index a4ca8fa6..35209cf6 100644 --- a/client/src/components/authentication/ForgotPasswordForm.js +++ b/client/src/components/authentication/ForgotPasswordForm.js @@ -1,5 +1,5 @@ -import { useState } from "react"; -import { Link as RouterLink } from "react-router-dom"; +import ForwardToInboxOutlinedIcon from "@mui/icons-material/ForwardToInboxOutlined"; +import PsychologyAltIcon from "@mui/icons-material/PsychologyAlt"; import { Box, Button, @@ -9,10 +9,10 @@ import { Stack, Typography, } from "@mui/material"; -import ForwardToInboxOutlinedIcon from "@mui/icons-material/ForwardToInboxOutlined"; -import PsychologyAltIcon from "@mui/icons-material/PsychologyAlt"; -import EmailInputField from "./EmailInputField"; +import { useState } from "react"; +import { Link as RouterLink } from "react-router-dom"; import Logo from "../../assets/WellAiLogoTR.png"; +import EmailInputField from "./EmailInputField"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/components/authentication/PhoneInputField.js b/client/src/components/authentication/PhoneInputField.js index ce9358ab..31a225c6 100644 --- a/client/src/components/authentication/PhoneInputField.js +++ b/client/src/components/authentication/PhoneInputField.js @@ -1,10 +1,10 @@ -import { useState } from "react"; -import { TextField, FormControl, Grid, Box, Autocomplete } from "@mui/material"; +import { Autocomplete, Box, FormControl, Grid, TextField } from "@mui/material"; import { getCountries, - parsePhoneNumberFromString, getCountryCallingCode, + parsePhoneNumberFromString, } from "libphonenumber-js"; +import { useState } from "react"; /** * An input field that provides basic validation for phone numbers and a diff --git a/client/src/components/authentication/RegistrationForm.js b/client/src/components/authentication/RegistrationForm.js index 748feac8..9a9742c7 100644 --- a/client/src/components/authentication/RegistrationForm.js +++ b/client/src/components/authentication/RegistrationForm.js @@ -1,5 +1,5 @@ -import { useState, useEffect } from "react"; -import { useNavigate, Link as RouterLink } from "react-router-dom"; +import AccountBalanceIcon from "@mui/icons-material/AccountBalance"; +import AccountCircleIcon from "@mui/icons-material/AccountCircle"; import { Alert, Box, @@ -7,9 +7,9 @@ import { Card, CardContent, Dialog, + DialogActions, DialogContent, DialogTitle, - DialogActions, Divider, FormControl, FormHelperText, @@ -20,12 +20,12 @@ import { TextField, Typography, } from "@mui/material"; -import AccountCircleIcon from "@mui/icons-material/AccountCircle"; -import AccountBalanceIcon from "@mui/icons-material/AccountBalance"; -import PasswordInputField from "../authentication/PasswordInputField"; +import { useEffect, useState } from "react"; +import { Link as RouterLink, useNavigate } from "react-router-dom"; +import Logo from "../../assets/WellAiLogoTR.png"; import EmailInputField from "../authentication/EmailInputField"; +import PasswordInputField from "../authentication/PasswordInputField"; import PhoneInputField from "../authentication/PhoneInputField"; -import Logo from "../../assets/WellAiLogoTR.png"; const FULL_NAME_MAX_LENGTH = 255; const ACCOUNT_TYPES = Object.freeze({ diff --git a/client/src/components/authentication/ResetPasswordForm.js b/client/src/components/authentication/ResetPasswordForm.js index 22e84d10..51f19108 100644 --- a/client/src/components/authentication/ResetPasswordForm.js +++ b/client/src/components/authentication/ResetPasswordForm.js @@ -1,4 +1,5 @@ -import { useState } from "react"; +import CheckIcon from "@mui/icons-material/Check"; +import LockResetIcon from "@mui/icons-material/LockReset"; import { Box, Button, @@ -9,9 +10,8 @@ import { TextField, Typography, } from "@mui/material"; -import { useParams, Link as RouterLink } from "react-router-dom"; -import CheckIcon from "@mui/icons-material/Check"; -import LockResetIcon from "@mui/icons-material/LockReset"; +import { useState } from "react"; +import { Link as RouterLink, useParams } from "react-router-dom"; import Logo from "../../assets/WellAiLogoTR.png"; import PasswordInputField from "../authentication/PasswordInputField"; diff --git a/client/src/components/dialog/DisclaimerPolicy.js b/client/src/components/dialog/DisclaimerPolicy.js index e0f3716b..7cd331fa 100644 --- a/client/src/components/dialog/DisclaimerPolicy.js +++ b/client/src/components/dialog/DisclaimerPolicy.js @@ -1,10 +1,10 @@ import { - Typography, + Box, + Button, Dialog, DialogContent, DialogTitle, - Box, - Button, + Typography, } from "@mui/material"; /** diff --git a/client/src/components/dialog/PrivacyNotice.js b/client/src/components/dialog/PrivacyNotice.js index d28f6c2e..36aa251c 100644 --- a/client/src/components/dialog/PrivacyNotice.js +++ b/client/src/components/dialog/PrivacyNotice.js @@ -1,10 +1,10 @@ import { - Typography, + Box, + Button, Dialog, DialogContent, DialogTitle, - Box, - Button, + Typography, } from "@mui/material"; /** diff --git a/client/src/components/dialog/confirmationDialog.js b/client/src/components/dialog/confirmationDialog.js index 4084f58c..af768655 100644 --- a/client/src/components/dialog/confirmationDialog.js +++ b/client/src/components/dialog/confirmationDialog.js @@ -1,10 +1,10 @@ import { + Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, - Button, } from "@mui/material"; /** diff --git a/client/src/routes/admin/AdministratorApproval.js b/client/src/routes/admin/AdministratorApproval.js index e7bf8030..1ef24148 100644 --- a/client/src/routes/admin/AdministratorApproval.js +++ b/client/src/routes/admin/AdministratorApproval.js @@ -1,5 +1,5 @@ +import { Box, Container, Divider, Stack, Typography } from "@mui/material"; import AccountApprovalTable from "../../components/administrator/AccountApprovalTable"; -import { Box, Typography, Divider, Stack, Container } from "@mui/material"; /** * A route that displays the merchant approval table where administrators diff --git a/client/src/routes/admin/AdministratorDashboard.js b/client/src/routes/admin/AdministratorDashboard.js index c2e1c8e4..913569fe 100644 --- a/client/src/routes/admin/AdministratorDashboard.js +++ b/client/src/routes/admin/AdministratorDashboard.js @@ -1,15 +1,11 @@ import { Box, - Card, - CardContent, Container, Divider, Grid, Stack, Typography, } from "@mui/material"; -import { BarChart } from "@mui/x-charts/BarChart"; -import { useEffect, useState } from "react"; import ActiveMerchantsAnalytics from "../../components/administrator/analytics/ActiveMerchantsAnalytics"; import ActiveUsersAnalytics from "../../components/administrator/analytics/ActiveUsersAnalytics"; import AverageRiskSeriesAnalytics from "../../components/administrator/analytics/AverageRiskSeriesAnalytics"; diff --git a/client/src/routes/admin/AdministratorLogs.js b/client/src/routes/admin/AdministratorLogs.js index 2a58ecc7..3d5d68f4 100644 --- a/client/src/routes/admin/AdministratorLogs.js +++ b/client/src/routes/admin/AdministratorLogs.js @@ -1,6 +1,5 @@ +import { Box, Container, Divider, Stack, Typography } from "@mui/material"; import AuditLogTable from "../../components/administrator/AuditLogTable"; -import { Box, Typography, Divider, Stack, Container } from "@mui/material"; -import AdministratorApproval from "./AdministratorApproval"; /** * A route that provides tools to filter through and audit the systems logs. diff --git a/client/src/routes/admin/AdministratorUsers.js b/client/src/routes/admin/AdministratorUsers.js index 45e469e9..bf233254 100644 --- a/client/src/routes/admin/AdministratorUsers.js +++ b/client/src/routes/admin/AdministratorUsers.js @@ -1,5 +1,5 @@ +import { Box, Container, Divider, Stack, Typography } from "@mui/material"; import UserManagementTable from "../../components/administrator/UserManagementTable"; -import { Box, Typography, Divider, Stack, Container } from "@mui/material"; /** * A route that provides tools to manage, filter and search through the users of the diff --git a/client/src/routes/merchant/MerchantGenerateReport.js b/client/src/routes/merchant/MerchantGenerateReport.js index f9dc16a4..9c686619 100644 --- a/client/src/routes/merchant/MerchantGenerateReport.js +++ b/client/src/routes/merchant/MerchantGenerateReport.js @@ -1,6 +1,6 @@ -import { Container, ButtonGroup, Button, Box } from "@mui/material"; -import MerchantReportForm from "../../components/healthReport/MerchantReportForm"; +import { Box } from "@mui/material"; import { useState } from "react"; +import MerchantReportForm from "../../components/healthReport/MerchantReportForm"; /** * A page that provides the health report form for merchants to generate diff --git a/client/src/routes/merchant/MerchantLanding.js b/client/src/routes/merchant/MerchantLanding.js index 045f1584..dd420d4a 100644 --- a/client/src/routes/merchant/MerchantLanding.js +++ b/client/src/routes/merchant/MerchantLanding.js @@ -1,5 +1,4 @@ -import { Box, Typography, Card, CardContent } from "@mui/material"; -import { grid } from "@mui/system"; +import { Box, Card, CardContent, Typography } from "@mui/material"; import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/routes/merchant/MerchantReports.js b/client/src/routes/merchant/MerchantReports.js index c0d7d42e..62220ef0 100644 --- a/client/src/routes/merchant/MerchantReports.js +++ b/client/src/routes/merchant/MerchantReports.js @@ -1,31 +1,27 @@ -import ReportTemplate from "../../components/healthReport/ReportTemplate"; -import DownloadReportButton from "../../components/healthReport/DownloadReportButton"; import CloseIcon from "@mui/icons-material/Close"; -import IconButton from "@mui/material/IconButton"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import ConfirmationDialog from "../../components/dialog/confirmationDialog"; -import React, { useState, useEffect } from "react"; -import { useLocation } from "react-router-dom"; import { + Autocomplete, Box, - Typography, + Button, + FormControl, + InputLabel, List, ListItem, ListItemText, MenuItem, - FormControl, - InputLabel, + Paper, Select, - Button, - Autocomplete, TextField, - Drawer, - Stack, - useTheme, + Typography, useMediaQuery, - Paper, + useTheme, } from "@mui/material"; +import IconButton from "@mui/material/IconButton"; +import { useEffect, useState } from "react"; +import { useLocation } from "react-router-dom"; +import ConfirmationDialog from "../../components/dialog/confirmationDialog"; +import DownloadReportButton from "../../components/healthReport/DownloadReportButton"; +import ReportTemplate from "../../components/healthReport/ReportTemplate"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/routes/merchant/PatientDetails.js b/client/src/routes/merchant/PatientDetails.js index 4eb54050..4ae741dc 100644 --- a/client/src/routes/merchant/PatientDetails.js +++ b/client/src/routes/merchant/PatientDetails.js @@ -1,15 +1,15 @@ import { Box, - Typography, + Button, Card, CardContent, - Button, - useTheme, + Typography, useMediaQuery, + useTheme, } from "@mui/material"; -import { useState, useEffect } from "react"; -import { useNavigate, useParams } from "react-router-dom"; import { BarChart } from "@mui/x-charts/BarChart"; +import { useEffect, useState } from "react"; +import { useNavigate, useParams } from "react-router-dom"; // AppBar height: 56px toolbar + 2px border on mobile (xs), 64px + 2px on desktop (sm+) const APPBAR_HEIGHT = { xs: "58px", sm: "66px" }; @@ -231,7 +231,13 @@ const PatientDetails = () => { > {key.toUpperCase()} - + {patientData?.risks?.[key]?.[0] ?? 0}% { if (!label) { diff --git a/client/src/routes/standardUser/ReportHistory.js b/client/src/routes/standardUser/ReportHistory.js index 4f925b5b..88faae0b 100644 --- a/client/src/routes/standardUser/ReportHistory.js +++ b/client/src/routes/standardUser/ReportHistory.js @@ -1,28 +1,24 @@ -import ReportTemplate from "../../components/healthReport/ReportTemplate"; -import DownloadReportButton from "../../components/healthReport/DownloadReportButton"; import CloseIcon from "@mui/icons-material/Close"; import IconButton from "@mui/material/IconButton"; -import MenuIcon from "@mui/icons-material/Menu"; +import { useEffect, useState } from "react"; import ConfirmationDialog from "../../components/dialog/confirmationDialog"; -import Stack from "@mui/material/Stack"; -import React, { useState, useEffect } from "react"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; +import DownloadReportButton from "../../components/healthReport/DownloadReportButton"; +import ReportTemplate from "../../components/healthReport/ReportTemplate"; import { Box, - Typography, + Button, + FormControl, + InputLabel, List, ListItem, ListItemText, - FormControl, - InputLabel, - Select, MenuItem, - Button, - useTheme, - useMediaQuery, Paper, + Select, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; @@ -118,8 +114,14 @@ const AIHealthPrediction = ({}) => { ].sort((a, b) => a - b); const months = [ - ...new Set(reportDates.filter((r) => !selectedYear || new Date(r.date).getFullYear() === selectedYear,) - .map((r) => new Date(r.date).getMonth() + 1),), + ...new Set( + reportDates + .filter( + (r) => + !selectedYear || new Date(r.date).getFullYear() === selectedYear, + ) + .map((r) => new Date(r.date).getMonth() + 1), + ), ].sort((a, b) => a - b); // Filters reports based on selected year and month if any. diff --git a/client/src/routes/standardUser/UserLanding.js b/client/src/routes/standardUser/UserLanding.js index 8a5fccac..35e7cb02 100644 --- a/client/src/routes/standardUser/UserLanding.js +++ b/client/src/routes/standardUser/UserLanding.js @@ -1,15 +1,15 @@ import { + Box, Card, CardContent, - Box, Divider, Typography, - useTheme, useMediaQuery, + useTheme, } from "@mui/material"; -import { useNavigate } from "react-router-dom"; -import { useEffect, useState } from "react"; import { BarChart } from "@mui/x-charts/BarChart"; +import { useEffect, useState } from "react"; +import { useNavigate } from "react-router-dom"; // AppBar height: 56px toolbar + 2px border on mobile (xs), 64px + 2px on desktop (sm+) const APPBAR_HEIGHT = { xs: "58px", sm: "66px" }; diff --git a/client/src/routes/standardUser/UserSettings.js b/client/src/routes/standardUser/UserSettings.js index 866d0f85..2741e2ab 100644 --- a/client/src/routes/standardUser/UserSettings.js +++ b/client/src/routes/standardUser/UserSettings.js @@ -1,27 +1,27 @@ -import React, { useContext, useEffect, useMemo, useState } from "react"; import { + Alert, Box, - Typography, - List, - ListItem, - ListItemText, - TextField, Button, FormControl, InputLabel, - Select, + List, + ListItem, + ListItemText, MenuItem, - Alert, + Select, Stack, - useTheme, - useMediaQuery, - Tabs, Tab, + Tabs, + TextField, + Typography, + useMediaQuery, + useTheme, } from "@mui/material"; -import ConfirmationDialog from "../../components/dialog/confirmationDialog"; +import { useContext, useEffect, useMemo, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { UserContext } from "../../utils/UserContext"; import PhoneInputField from "../../components/authentication/PhoneInputField"; +import ConfirmationDialog from "../../components/dialog/confirmationDialog"; +import { UserContext } from "../../utils/UserContext"; /** * A page that provides the tools for a user to securely update their diff --git a/client/src/utils/LandingRoute.js b/client/src/utils/LandingRoute.js index 388e2b30..7a72b836 100644 --- a/client/src/utils/LandingRoute.js +++ b/client/src/utils/LandingRoute.js @@ -1,5 +1,5 @@ -import { Navigate } from "react-router-dom"; import { useEffect, useState } from "react"; +import { Navigate } from "react-router-dom"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/utils/ProtectedRoutes.js b/client/src/utils/ProtectedRoutes.js index bf1ca4be..9dff833a 100644 --- a/client/src/utils/ProtectedRoutes.js +++ b/client/src/utils/ProtectedRoutes.js @@ -1,5 +1,5 @@ -import { Outlet, Navigate, useLocation } from "react-router-dom"; import { useEffect, useState } from "react"; +import { Navigate, Outlet, useLocation } from "react-router-dom"; import NavBar from "../components/Navbar"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/utils/PublicOnlyRoutes.js b/client/src/utils/PublicOnlyRoutes.js index ef0b560d..852b9c8a 100644 --- a/client/src/utils/PublicOnlyRoutes.js +++ b/client/src/utils/PublicOnlyRoutes.js @@ -1,6 +1,5 @@ -import { Outlet, Navigate } from "react-router-dom"; import { useEffect, useState } from "react"; -import NavBar from "../components/Navbar"; +import { Navigate, Outlet } from "react-router-dom"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; diff --git a/client/src/utils/UserContext.js b/client/src/utils/UserContext.js index 13c738af..66835553 100644 --- a/client/src/utils/UserContext.js +++ b/client/src/utils/UserContext.js @@ -1,9 +1,9 @@ import { createContext, - useState, + useCallback, useEffect, useMemo, - useCallback, + useState, } from "react"; export const UserContext = createContext(null); From 28914a837d2006f26ae0424e059c9a0fbddc87fd Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 11:57:45 +0930 Subject: [PATCH 067/105] Change href navigation to RouterLink in HealthAnalytics.js --- client/src/routes/standardUser/HealthAnalytics.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/client/src/routes/standardUser/HealthAnalytics.js b/client/src/routes/standardUser/HealthAnalytics.js index f330f17d..51bc8b9f 100644 --- a/client/src/routes/standardUser/HealthAnalytics.js +++ b/client/src/routes/standardUser/HealthAnalytics.js @@ -19,6 +19,7 @@ import { } from "@mui/material"; import { LineChart } from "@mui/x-charts/LineChart"; import { useEffect, useMemo, useState } from "react"; +import { Link as RouterLink } from "react-router-dom"; const formatXAxisLabel = (label, index, total, isMobile) => { if (!label) { @@ -468,7 +469,8 @@ const HealthAnalytics = () => { > @@ -371,7 +362,6 @@ const UserSettings = () => { width: { xs: "min(260px, 100%)", sm: "auto" }, py: { xs: "0.7rem", sm: "0.6rem" }, fontSize: { xs: "0.95rem", sm: "0.875rem" }, - fontWeight: 500, }} > {accountSaving ? "Saving…" : "Save Changes"} @@ -404,8 +394,8 @@ const UserSettings = () => { const Profile = () => ( Profile Settings @@ -420,15 +410,6 @@ const UserSettings = () => { )} - - - {user?.given_names || ""} {user?.family_name || ""} - - - {user?.email || ""} - - - { return ( Change Password From 37bab8874647ff6f8f2f20a94ce5e2c0799529a7 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 12:26:03 +0930 Subject: [PATCH 070/105] Fix mobile layout for report template --- client/src/components/AppThemeProvider.jsx | 4 ++++ .../src/components/healthReport/ReportTemplate.js | 15 ++++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/client/src/components/AppThemeProvider.jsx b/client/src/components/AppThemeProvider.jsx index be1f45f5..ff550784 100644 --- a/client/src/components/AppThemeProvider.jsx +++ b/client/src/components/AppThemeProvider.jsx @@ -137,6 +137,10 @@ export const appTheme = createTheme({ fontWeight: "regular", fontFamily: "'Russo One', 'sans-serif'", }, + h6: { + fontWeight: "regular", + fontFamily: "'Russo One', 'sans-serif'", + }, h7: { fontWeight: "regular", fontFamily: "'Russo One', 'sans-serif'", diff --git a/client/src/components/healthReport/ReportTemplate.js b/client/src/components/healthReport/ReportTemplate.js index c7a7dc2b..8c213cd2 100644 --- a/client/src/components/healthReport/ReportTemplate.js +++ b/client/src/components/healthReport/ReportTemplate.js @@ -154,7 +154,9 @@ const ReportTemplate = ({ report, date }) => { mx: "auto", }} > - Generated on: {new Date(date).toLocaleDateString("en-AU")}{" at "}{`${new Date(date).toLocaleTimeString("en-AU")}`} + Generated on: {new Date(date).toLocaleDateString("en-AU")} + {" at "} + {`${new Date(date).toLocaleTimeString("en-AU")}`} @@ -265,7 +267,7 @@ const ReportTemplate = ({ report, date }) => { }} > { p: 2, }} > - {/* Exercise */} - + Exercise Recommendations @@ -295,7 +296,7 @@ const ReportTemplate = ({ report, date }) => { {/* Diet */} - + Diet Recommendations @@ -305,7 +306,7 @@ const ReportTemplate = ({ report, date }) => { {/* Lifestyle */} - + Lifestyle Recommendations @@ -316,7 +317,7 @@ const ReportTemplate = ({ report, date }) => { {/* Diet to Avoid */} - + Diet to Avoid From ea8cee2d5e13ae0c07b722440cdab1eb2e493dd9 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 12:35:46 +0930 Subject: [PATCH 071/105] Update mobile UI on Generate Report Form --- .../healthReport/GenerateReportForm.js | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/client/src/components/healthReport/GenerateReportForm.js b/client/src/components/healthReport/GenerateReportForm.js index 65070815..585739ee 100644 --- a/client/src/components/healthReport/GenerateReportForm.js +++ b/client/src/components/healthReport/GenerateReportForm.js @@ -15,6 +15,8 @@ import { ListItemText, Select, FormHelperText, + useTheme, + useMediaQuery, } from "@mui/material"; import CheckBoxOutlineBlankIcon from "@mui/icons-material/CheckBoxOutlineBlank"; import CheckBoxIcon from "@mui/icons-material/CheckBox"; @@ -30,6 +32,8 @@ const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; */ const GenerateReportForm = () => { const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); // Options for each drop down input. This can be modified as required to change the input for each selection const healthConditions = [ @@ -376,7 +380,7 @@ const GenerateReportForm = () => { }} > { {/* Age & Physique Section */} Age & Physique @@ -490,10 +495,11 @@ const GenerateReportForm = () => { {/* Fitness Section */} Health & Fitness @@ -586,10 +592,11 @@ const GenerateReportForm = () => { /> Life Style @@ -718,7 +725,7 @@ const GenerateReportForm = () => { - + + +
@@ -206,7 +213,7 @@ const PatientDetails = () => { sx={{ display: "flex", gap: { xs: 1, sm: 2 }, - flexDirection: "row", + flexDirection: isMobile ? "column" : "row", }} > {["stroke", "diabetes", "cvd"].map((key) => ( @@ -299,7 +306,7 @@ const PatientDetails = () => { height: { xs: "auto", md: "95%" }, }} > - + Latest Recommendations @@ -317,7 +324,10 @@ const PatientDetails = () => { : "none", }} > - + {key.toUpperCase()} From efe375a6b1d6a9cf7828bf03523b139ca9db2772 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 13:06:28 +0930 Subject: [PATCH 075/105] Fix mobile font size on Merchant landing --- client/src/routes/merchant/MerchantLanding.js | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/client/src/routes/merchant/MerchantLanding.js b/client/src/routes/merchant/MerchantLanding.js index dd420d4a..1d53f170 100644 --- a/client/src/routes/merchant/MerchantLanding.js +++ b/client/src/routes/merchant/MerchantLanding.js @@ -1,4 +1,11 @@ -import { Box, Card, CardContent, Typography } from "@mui/material"; +import { + Box, + Card, + CardContent, + Typography, + useMediaQuery, + useTheme, +} from "@mui/material"; import { useEffect, useState } from "react"; const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; @@ -14,6 +21,9 @@ const MerchantLanding = () => { const [data, setData] = useState({}); const [name, setName] = useState(""); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + useEffect(() => { fetch(`${API_BASE}/user/me`, { method: "GET", @@ -76,7 +86,9 @@ const MerchantLanding = () => { }} > - Patient Overview + + Patient Overview + {/* Stat cards */} @@ -114,7 +126,10 @@ const MerchantLanding = () => { > {/* Condition risk graph */} - + Risk Distribution {["stroke", "cvd", "diabetes"].map((condition) => { @@ -193,7 +208,10 @@ const MerchantLanding = () => { p: 2, }} > - + Report Activity From 2478d740cec6525cf0bc077abaef24a6411462d7 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 13:08:17 +0930 Subject: [PATCH 076/105] Fix mobile font size in Patient Management --- client/src/routes/merchant/PatientManagement.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/client/src/routes/merchant/PatientManagement.js b/client/src/routes/merchant/PatientManagement.js index 48c03332..6826f280 100644 --- a/client/src/routes/merchant/PatientManagement.js +++ b/client/src/routes/merchant/PatientManagement.js @@ -8,6 +8,8 @@ import { Stack, TextField, Typography, + useMediaQuery, + useTheme, } from "@mui/material"; import InputAdornment from "@mui/material/InputAdornment"; import { DataGrid } from "@mui/x-data-grid"; @@ -30,6 +32,9 @@ const PatientManagement = () => { const API_BASE = process.env.REACT_APP_API_URL || "http://localhost:8000"; const navigate = useNavigate(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [patientData, setPatientData] = useState([]); const [paginationModel, setPaginationModel] = useState({ page: 0, @@ -198,10 +203,7 @@ const PatientManagement = () => { }} > - + Patient Management From 4c2718200d6000be121a53be7f5137833f7e69e8 Mon Sep 17 00:00:00 2001 From: birjd002 Date: Sun, 24 May 2026 13:11:51 +0930 Subject: [PATCH 077/105] fix: resolve merge conflicts with dev --- client/src/components/{ => healthReport}/PDFHealthChart.js | 0 client/src/routes/standardUser/ReportHistory.js | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename client/src/components/{ => healthReport}/PDFHealthChart.js (100%) diff --git a/client/src/components/PDFHealthChart.js b/client/src/components/healthReport/PDFHealthChart.js similarity index 100% rename from client/src/components/PDFHealthChart.js rename to client/src/components/healthReport/PDFHealthChart.js diff --git a/client/src/routes/standardUser/ReportHistory.js b/client/src/routes/standardUser/ReportHistory.js index 6bf469bf..fcbd1761 100644 --- a/client/src/routes/standardUser/ReportHistory.js +++ b/client/src/routes/standardUser/ReportHistory.js @@ -8,7 +8,7 @@ import Stack from "@mui/material/Stack"; import React, { useState, useEffect, useRef } from "react"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import PDFHealthChart from "../components/PDFHealthChart"; +import PDFHealthChart from "../components/healthReport/PDFHealthChart"; import { Box, From 87cd72cf53bab6df274d682fc393b730ce1cb899 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 13:14:19 +0930 Subject: [PATCH 078/105] Update mobile layout on UserLanding --- client/src/routes/standardUser/UserLanding.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/client/src/routes/standardUser/UserLanding.js b/client/src/routes/standardUser/UserLanding.js index 35e7cb02..7ed131b9 100644 --- a/client/src/routes/standardUser/UserLanding.js +++ b/client/src/routes/standardUser/UserLanding.js @@ -118,7 +118,7 @@ const UserLanding = ({}) => { @@ -137,7 +137,7 @@ const UserLanding = ({}) => { sx={{ display: "flex", gap: { xs: 1, sm: 2 }, - flexDirection: "row", + flexDirection: isMobile ? "column" : "row", }} > {["stroke", "diabetes", "cvd"].map((key) => ( @@ -228,7 +228,7 @@ const UserLanding = ({}) => { height: { xs: "auto", md: "95%" }, }} > - + Latest Recommendations @@ -246,7 +246,10 @@ const UserLanding = ({}) => { : "none", }} > - + {key.toUpperCase()} From d2ba52ca3d0ab56f6b1ddbe41565d08714dee498 Mon Sep 17 00:00:00 2001 From: birjd002 Date: Sun, 24 May 2026 13:16:05 +0930 Subject: [PATCH 079/105] fix: update relative path of imports --- client/src/routes/merchant/MerchantReports.js | 2 +- client/src/routes/standardUser/ReportHistory.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/routes/merchant/MerchantReports.js b/client/src/routes/merchant/MerchantReports.js index 655b7ed8..8f4a19fa 100644 --- a/client/src/routes/merchant/MerchantReports.js +++ b/client/src/routes/merchant/MerchantReports.js @@ -5,7 +5,7 @@ import IconButton from "@mui/material/IconButton"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; import ConfirmationDialog from "../../components/dialog/confirmationDialog"; -import PDFHealthChart from "../components/healthReport/PDFHealthChart"; +import PDFHealthChart from "../../components/healthReport/PDFHealthChart"; import React, { useState, useEffect, useRef } from "react"; import { useLocation } from "react-router-dom"; import { diff --git a/client/src/routes/standardUser/ReportHistory.js b/client/src/routes/standardUser/ReportHistory.js index fcbd1761..43cc8911 100644 --- a/client/src/routes/standardUser/ReportHistory.js +++ b/client/src/routes/standardUser/ReportHistory.js @@ -8,7 +8,7 @@ import Stack from "@mui/material/Stack"; import React, { useState, useEffect, useRef } from "react"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; import KeyboardArrowUpIcon from "@mui/icons-material/KeyboardArrowUp"; -import PDFHealthChart from "../components/healthReport/PDFHealthChart"; +import PDFHealthChart from "../../components/healthReport/PDFHealthChart"; import { Box, From 610726d1e7b294abf3fb1463cff12dcef581aad9 Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 13:22:15 +0930 Subject: [PATCH 080/105] Update no report screen on report history --- .../src/routes/standardUser/ReportHistory.js | 36 +++++++++++++++---- 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/client/src/routes/standardUser/ReportHistory.js b/client/src/routes/standardUser/ReportHistory.js index 88faae0b..b74c6c0e 100644 --- a/client/src/routes/standardUser/ReportHistory.js +++ b/client/src/routes/standardUser/ReportHistory.js @@ -4,6 +4,7 @@ import { useEffect, useState } from "react"; import ConfirmationDialog from "../../components/dialog/confirmationDialog"; import DownloadReportButton from "../../components/healthReport/DownloadReportButton"; import ReportTemplate from "../../components/healthReport/ReportTemplate"; +import { Link as RouterLink } from "react-router-dom"; import { Box, @@ -144,15 +145,38 @@ const AIHealthPrediction = ({}) => { - - No Health Prediction Reports Available - + + + No Health Prediction Reports Available + + + ); } else { From f7a1560b58a7cd71e401b0edea41111cee2880ea Mon Sep 17 00:00:00 2001 From: Luke Bradley Date: Sun, 24 May 2026 14:10:22 +0930 Subject: [PATCH 081/105] Fix mobile button alignment --- .../administrator/analytics/AverageRiskSeriesAnalytics.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js b/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js index d31c06f4..d631fa19 100644 --- a/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js +++ b/client/src/components/administrator/analytics/AverageRiskSeriesAnalytics.js @@ -7,6 +7,8 @@ import { MenuItem, Select, Typography, + useMediaQuery, + useTheme, } from "@mui/material"; import Divider from "@mui/material/Divider"; import { LineChart } from "@mui/x-charts/LineChart"; @@ -23,6 +25,9 @@ const DEFAULT_YEAR = 2026; * @returns {@mui.material.Card} */ const AverageRiskSeriesAnalytics = () => { + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [availableYears, setAvailableYears] = useState([]); const [year, setYear] = useState(""); const [xAxisData, setXAxisData] = useState([]); @@ -96,7 +101,7 @@ const AverageRiskSeriesAnalytics = () => { Average Disease Risk (%) - + Year