Modularize/patientprofilepage#86
Conversation
- Modularized the patient profile code into separate folders for better maintainability - Fixed incorrect imports in the frontend after restructuring - Resolved Cloudinary environment variable issue in the backend by importing and configuring dotenv
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR modularizes the PatientProfilePage into reusable, single-responsibility React components for patient profile sections (Basic Information, Address, Emergency Contact, Health Records) with edit-mode support, and updates the Cloudinary configuration to load environment variables via dotenv. Changes
Sequence DiagramsequenceDiagram
participant User
participant PatientProfilePage
participant ProfileHeader
participant ProfileContent
participant ProfileSection as Profile Sections<br/>(Basic, Address, etc.)
participant HealthRecords
participant HealthRecordModal
User->>PatientProfilePage: Load profile
PatientProfilePage->>ProfileHeader: Render with isEditing=false
User->>ProfileHeader: Click Edit Profile
ProfileHeader->>PatientProfilePage: setIsEditing(true)
PatientProfilePage->>ProfileContent: Render with isEditing=true
ProfileContent->>ProfileSection: Render inputs for editing
ProfileSection->>User: Display form fields
User->>ProfileSection: Enter/modify values
ProfileSection->>ProfileContent: handleInputChange (nested state)
ProfileContent->>PatientProfilePage: setEditFormData
User->>ProfileHeader: Click Save Changes
ProfileHeader->>PatientProfilePage: handleSaveProfile()
PatientProfilePage->>PatientProfilePage: updatePatientProfile()
User->>HealthRecords: Click Add Record
HealthRecords->>PatientProfilePage: setShowHealthRecordForm(true)
PatientProfilePage->>HealthRecordModal: Render modal
HealthRecordModal->>User: Display upload form
User->>HealthRecordModal: Submit record
HealthRecordModal->>PatientProfilePage: Upload & refresh
PatientProfilePage->>HealthRecords: Refresh records list
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Areas requiring extra attention:
Suggested labels
Poem
Pre-merge checks and finishing touches✅ Passed checks (4 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
server/config/cloudinary.js (2)
6-10: Add validation for required environment variables.Consider adding validation to ensure all required Cloudinary credentials are present before configuring the SDK. This will provide clearer error messages if environment variables are missing.
Apply this diff to add validation:
dotenv.config(); + +const requiredEnvVars = ['CLOUD_NAME', 'CLOUD_API_KEY', 'CLOUD_API_SECRET']; +const missingVars = requiredEnvVars.filter(varName => !process.env[varName]); + +if (missingVars.length > 0) { + throw new Error( + `Missing required Cloudinary environment variables: ${missingVars.join(', ')}` + ); +} + cloudinary.config({ cloud_name: process.env.CLOUD_NAME, api_key: process.env.CLOUD_API_KEY, api_secret: process.env.CLOUD_API_SECRET, });
3-5: Remove redundantdotenv.config()from cloudinary.js—it's already loaded at the application entry point.
dotenv.config()is correctly placed inserver/index.js(line 3) and runs before any modules are imported. The call incloudinary.jsis unnecessary redundancy.Apply this diff to cloudinary.js:
// config/cloudinary.js import { v2 as cloudinary } from 'cloudinary'; -import dotenv from 'dotenv'; - -dotenv.config(); cloudinary.config({ cloud_name: process.env.CLOUD_NAME, api_key: process.env.CLOUD_API_KEY, api_secret: process.env.CLOUD_API_SECRET, });client/src/components/Patient/PatientProfile/EmergencyContact.jsx (1)
19-52: Prevent controlled/uncontrolled input flipsWhen
editFormData.emergencyContactis missing (e.g., after a failed fetch and the user enters edit mode) thesevalueprops becomeundefined, so the inputs render uncontrolled and flip to controlled as soon as a user types, triggering React warnings. Defaulting to an empty string keeps them controlled throughout.- value={editFormData.emergencyContact?.name} + value={editFormData.emergencyContact?.name ?? ''} … - value={editFormData.emergencyContact?.relationship} + value={editFormData.emergencyContact?.relationship ?? ''} … - value={editFormData.emergencyContact?.phoneNumber} + value={editFormData.emergencyContact?.phoneNumber ?? ''}
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (10)
client/src/components/Patient/PatientProfile/AddressInformation.jsx(1 hunks)client/src/components/Patient/PatientProfile/BasicInformation.jsx(1 hunks)client/src/components/Patient/PatientProfile/EmergencyContact.jsx(1 hunks)client/src/components/Patient/PatientProfile/HealthRecordModal.jsx(1 hunks)client/src/components/Patient/PatientProfile/HealthRecords.jsx(1 hunks)client/src/components/Patient/PatientProfile/MessageAlerts.jsx(1 hunks)client/src/components/Patient/PatientProfile/ProfileContent.jsx(1 hunks)client/src/components/Patient/PatientProfile/ProfileHeader.jsx(1 hunks)client/src/pages/patient/PatientProfilePage.jsx(2 hunks)server/config/cloudinary.js(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (9)
client/src/components/Patient/PatientProfile/MessageAlerts.jsx (1)
client/src/pages/patient/PatientProfilePage.jsx (2)
error(23-23)success(24-24)
client/src/components/Patient/PatientProfile/HealthRecords.jsx (1)
client/src/pages/patient/PatientProfilePage.jsx (1)
profile(10-10)
client/src/components/Patient/PatientProfile/ProfileHeader.jsx (1)
client/src/pages/patient/PatientProfilePage.jsx (2)
isEditing(11-11)isLoading(12-12)
client/src/components/Patient/PatientProfile/ProfileContent.jsx (5)
client/src/components/Patient/PatientProfile/BasicInformation.jsx (1)
BasicInformation(4-145)client/src/pages/patient/PatientProfilePage.jsx (4)
profile(10-10)isEditing(11-11)editFormData(14-14)profilePictureFile(22-22)client/src/components/Patient/PatientProfile/AddressInformation.jsx (1)
AddressInformation(4-90)client/src/components/Patient/PatientProfile/EmergencyContact.jsx (1)
EmergencyContact(4-64)client/src/components/Patient/PatientProfile/HealthRecords.jsx (1)
HealthRecords(5-104)
client/src/components/Patient/PatientProfile/BasicInformation.jsx (3)
client/src/components/Patient/PatientProfile/HealthRecords.jsx (1)
formatDate(6-9)client/src/pages/patient/PatientProfilePage.jsx (4)
profile(10-10)isEditing(11-11)profilePictureFile(22-22)editFormData(14-14)client/src/components/Patient/PatientProfile/ProfileContent.jsx (2)
handleProfilePictureChange(36-41)handleInputChange(17-34)
client/src/components/Patient/PatientProfile/AddressInformation.jsx (2)
client/src/pages/patient/PatientProfilePage.jsx (3)
isEditing(11-11)editFormData(14-14)profile(10-10)client/src/components/Patient/PatientProfile/ProfileContent.jsx (1)
handleInputChange(17-34)
client/src/pages/patient/PatientProfilePage.jsx (6)
client/src/components/ui/Loading.jsx (1)
Loading(4-63)client/src/components/Patient/PatientProfile/ProfileHeader.jsx (1)
ProfileHeader(3-44)client/src/service/patientApiService.js (2)
updatePatientProfile(39-63)updatePatientProfile(39-63)client/src/components/Patient/PatientProfile/MessageAlerts.jsx (1)
MessageAlerts(3-25)client/src/components/Patient/PatientProfile/ProfileContent.jsx (1)
ProfileContent(7-77)client/src/components/Patient/PatientProfile/HealthRecordModal.jsx (1)
HealthRecordModal(5-180)
client/src/components/Patient/PatientProfile/HealthRecordModal.jsx (2)
client/src/components/Patient/PatientProfile/HealthRecords.jsx (1)
formatRecordType(11-16)client/src/pages/patient/PatientProfilePage.jsx (5)
selectedFile(21-21)healthRecordForm(15-20)fetchProfile(30-59)error(23-23)isLoading(12-12)
client/src/components/Patient/PatientProfile/EmergencyContact.jsx (2)
client/src/pages/patient/PatientProfilePage.jsx (3)
isEditing(11-11)editFormData(14-14)profile(10-10)client/src/components/Patient/PatientProfile/ProfileContent.jsx (1)
handleInputChange(17-34)
| <input | ||
| type="text" | ||
| name="address.street" | ||
| value={editFormData.address?.street} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| /> | ||
| ) : ( | ||
| <p className="text-gray-900">{profile?.address?.street || 'Not specified'}</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">City</label> | ||
| {isEditing ? ( | ||
| <input | ||
| type="text" | ||
| name="address.city" | ||
| value={editFormData.address?.city} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| /> | ||
| ) : ( | ||
| <p className="text-gray-900">{profile?.address?.city || 'Not specified'}</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">State</label> | ||
| {isEditing ? ( | ||
| <input | ||
| type="text" | ||
| name="address.state" | ||
| value={editFormData.address?.state} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| /> | ||
| ) : ( | ||
| <p className="text-gray-900">{profile?.address?.state || 'Not specified'}</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">Zip Code</label> | ||
| {isEditing ? ( | ||
| <input | ||
| type="text" | ||
| name="address.zipCode" | ||
| value={editFormData.address?.zipCode} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| /> | ||
| ) : ( | ||
| <p className="text-gray-900">{profile?.address?.zipCode || 'Not specified'}</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">Country</label> | ||
| {isEditing ? ( | ||
| <input | ||
| type="text" | ||
| name="address.country" | ||
| value={editFormData.address?.country} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| /> |
There was a problem hiding this comment.
Avoid uncontrolled→controlled inputs in address form
When editing begins, editFormData.address may not yet hydrate every field, so each value={editFormData.address?.…} starts as undefined and becomes a string after the first keystroke. React will yell about switching from uncontrolled to controlled inputs, and some testing setups treat that as a failure. Please coalesce to an empty string so the inputs stay controlled from the first render.
- value={editFormData.address?.street}
+ value={editFormData.address?.street ?? ''}
...
- value={editFormData.address?.city}
+ value={editFormData.address?.city ?? ''}
...
- value={editFormData.address?.state}
+ value={editFormData.address?.state ?? ''}
...
- value={editFormData.address?.zipCode}
+ value={editFormData.address?.zipCode ?? ''}
...
- value={editFormData.address?.country}
+ value={editFormData.address?.country ?? ''}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <input | |
| type="text" | |
| name="address.street" | |
| value={editFormData.address?.street} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.street || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">City</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.city" | |
| value={editFormData.address?.city} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.city || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">State</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.state" | |
| value={editFormData.address?.state} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.state || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Zip Code</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.zipCode" | |
| value={editFormData.address?.zipCode} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.zipCode || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Country</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.country" | |
| value={editFormData.address?.country} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| <input | |
| type="text" | |
| name="address.street" | |
| value={editFormData.address?.street ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.street || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">City</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.city" | |
| value={editFormData.address?.city ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.city || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">State</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.state" | |
| value={editFormData.address?.state ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.state || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Zip Code</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.zipCode" | |
| value={editFormData.address?.zipCode ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.address?.zipCode || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Country</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="address.country" | |
| value={editFormData.address?.country ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> |
🤖 Prompt for AI Agents
client/src/components/Patient/PatientProfile/AddressInformation.jsx around lines
16-82: the input value props use editFormData.address?.field which can be
undefined initially and cause uncontrolled→controlled warnings; for each
editable field (street, city, state, zipCode, country) coalesce the value to an
empty string (e.g. change value={editFormData.address?.street} to
value={editFormData.address?.street || ''}) so inputs are controlled from first
render, leaving name and onChange as-is.
| <input | ||
| type="text" | ||
| name="firstName" | ||
| value={editFormData.firstName} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| /> | ||
| ) : ( | ||
| <p className="text-gray-900">{profile?.firstName || 'Not specified'}</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">Last Name</label> | ||
| {isEditing ? ( | ||
| <input | ||
| type="text" | ||
| name="lastName" | ||
| value={editFormData.lastName} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| /> | ||
| ) : ( | ||
| <p className="text-gray-900">{profile?.lastName || 'Not specified'}</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">Email</label> | ||
| <p className="text-gray-900">{profile?.email || 'Not specified'}</p> | ||
| {isEditing && <p className="text-xs text-gray-500 mt-1">Email cannot be changed</p>} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">Date of Birth</label> | ||
| <p className="text-gray-900">{formatDate(profile?.dateOfBirth)}</p> | ||
| {isEditing && ( | ||
| <p className="text-xs text-gray-500 mt-1">Date of birth cannot be changed</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">Gender</label> | ||
| {isEditing ? ( | ||
| <select | ||
| name="gender" | ||
| value={editFormData.gender} | ||
| onChange={handleInputChange} | ||
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | ||
| > | ||
| <option value="">Select Gender</option> | ||
| <option value="male">Male</option> | ||
| <option value="female">Female</option> | ||
| <option value="other">Other</option> | ||
| </select> | ||
| ) : ( | ||
| <p className="text-gray-900">{profile?.gender || 'Not specified'}</p> | ||
| )} | ||
| </div> | ||
|
|
||
| <div> | ||
| <label className="block text-sm font-medium text-gray-700 mb-2">Phone Number</label> | ||
| {isEditing ? ( | ||
| <input | ||
| type="tel" | ||
| name="phoneNumber" | ||
| value={editFormData.phoneNumber} | ||
| onChange={handleInputChange} |
There was a problem hiding this comment.
Keep basic-info inputs controlled from first render
editFormData.firstName (and friends) can be undefined until edit mode primes the form state. That leaves each input uncontrolled initially, then controlled after the first keystroke, triggering React warnings and flaky tests. Please coalesce to empty strings (and for the select, to '') so they stay controlled.
- value={editFormData.firstName}
+ value={editFormData.firstName ?? ''}
...
- value={editFormData.lastName}
+ value={editFormData.lastName ?? ''}
...
- value={editFormData.gender}
+ value={editFormData.gender ?? ''}
...
- value={editFormData.phoneNumber}
+ value={editFormData.phoneNumber ?? ''}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <input | |
| type="text" | |
| name="firstName" | |
| value={editFormData.firstName} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.firstName || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Last Name</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="lastName" | |
| value={editFormData.lastName} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.lastName || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Email</label> | |
| <p className="text-gray-900">{profile?.email || 'Not specified'}</p> | |
| {isEditing && <p className="text-xs text-gray-500 mt-1">Email cannot be changed</p>} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Date of Birth</label> | |
| <p className="text-gray-900">{formatDate(profile?.dateOfBirth)}</p> | |
| {isEditing && ( | |
| <p className="text-xs text-gray-500 mt-1">Date of birth cannot be changed</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Gender</label> | |
| {isEditing ? ( | |
| <select | |
| name="gender" | |
| value={editFormData.gender} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| > | |
| <option value="">Select Gender</option> | |
| <option value="male">Male</option> | |
| <option value="female">Female</option> | |
| <option value="other">Other</option> | |
| </select> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.gender || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Phone Number</label> | |
| {isEditing ? ( | |
| <input | |
| type="tel" | |
| name="phoneNumber" | |
| value={editFormData.phoneNumber} | |
| onChange={handleInputChange} | |
| <input | |
| type="text" | |
| name="firstName" | |
| value={editFormData.firstName ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.firstName || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Last Name</label> | |
| {isEditing ? ( | |
| <input | |
| type="text" | |
| name="lastName" | |
| value={editFormData.lastName ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| /> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.lastName || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Email</label> | |
| <p className="text-gray-900">{profile?.email || 'Not specified'}</p> | |
| {isEditing && <p className="text-xs text-gray-500 mt-1">Email cannot be changed</p>} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Date of Birth</label> | |
| <p className="text-gray-900">{formatDate(profile?.dateOfBirth)}</p> | |
| {isEditing && ( | |
| <p className="text-xs text-gray-500 mt-1">Date of birth cannot be changed</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Gender</label> | |
| {isEditing ? ( | |
| <select | |
| name="gender" | |
| value={editFormData.gender ?? ''} | |
| onChange={handleInputChange} | |
| className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" | |
| > | |
| <option value="">Select Gender</option> | |
| <option value="male">Male</option> | |
| <option value="female">Female</option> | |
| <option value="other">Other</option> | |
| </select> | |
| ) : ( | |
| <p className="text-gray-900">{profile?.gender || 'Not specified'}</p> | |
| )} | |
| </div> | |
| <div> | |
| <label className="block text-sm font-medium text-gray-700 mb-2">Phone Number</label> | |
| {isEditing ? ( | |
| <input | |
| type="tel" | |
| name="phoneNumber" | |
| value={editFormData.phoneNumber ?? ''} | |
| onChange={handleInputChange} |
🤖 Prompt for AI Agents
In client/src/components/Patient/PatientProfile/BasicInformation.jsx around
lines 68 to 135 the input/select value props use editFormData properties that
may be undefined until edit mode, causing uncontrolled→controlled warnings;
change each value prop to coalesce to an empty string (e.g.
value={editFormData.firstName || ''}) and for the gender select use
value={editFormData.gender || ''} (do the same for lastName, phoneNumber and any
other form fields in this block) so all inputs are controlled from first render.
| return type | ||
| .split('-') | ||
| .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | ||
| .join(' '); | ||
| }; | ||
|
|
||
| const getRecordTypeIcon = (type) => { | ||
| const icons = { | ||
| allergy: <AlertCircle className="w-4 h-4" />, | ||
| condition: <Heart className="w-4 h-4" />, | ||
| immunization: <Shield className="w-4 h-4" />, | ||
| 'lab-result': <FileText className="w-4 h-4" />, | ||
| medication: <Plus className="w-4 h-4" />, | ||
| procedure: <UserCheck className="w-4 h-4" />, | ||
| 'vital-sign': <Heart className="w-4 h-4" />, | ||
| }; | ||
| return icons[type] || <FileText className="w-4 h-4" />; | ||
| }; | ||
|
|
||
| return ( | ||
| <div className="space-y-6"> | ||
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> | ||
| <div className="flex items-center justify-between mb-6"> | ||
| <div className="flex items-center gap-2"> | ||
| <FileText className="w-5 h-5 text-green-600" /> | ||
| <h2 className="text-lg font-semibold text-gray-900">Health Records</h2> | ||
| </div> | ||
| <button | ||
| onClick={() => setShowHealthRecordForm(true)} | ||
| className="inline-flex items-center gap-2 px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm" | ||
| > | ||
| <Plus className="w-4 h-4" /> | ||
| Add Record | ||
| </button> | ||
| </div> | ||
|
|
||
| <div className="space-y-4"> | ||
| {profile?.healthRecords && profile.healthRecords.length > 0 ? ( | ||
| profile.healthRecords.map((record, index) => ( | ||
| <div key={index} className="border border-gray-200 rounded-lg p-4"> | ||
| <div className="flex items-start justify-between mb-2"> | ||
| <div className="flex items-center gap-2"> | ||
| {getRecordTypeIcon(record.recordType)} | ||
| <h3 className="font-medium text-gray-900">{record.title}</h3> | ||
| </div> | ||
| <span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded"> | ||
| {formatRecordType(record.recordType)} | ||
| </span> |
There was a problem hiding this comment.
Guard formatRecordType against missing record types
formatRecordType(record.recordType) will throw when recordType is null/undefined, which is easy to hit if older records or drafts don’t populate the field. On the web this surfaces as a runtime TypeError and breaks the entire profile view. Please default the input before splitting.
- const formatRecordType = (type) => {
- return type
- .split('-')
+ const formatRecordType = (type) => {
+ if (!type) return 'Unknown';
+ return String(type)
+ .split('-')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| return type | |
| .split('-') | |
| .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | |
| .join(' '); | |
| }; | |
| const getRecordTypeIcon = (type) => { | |
| const icons = { | |
| allergy: <AlertCircle className="w-4 h-4" />, | |
| condition: <Heart className="w-4 h-4" />, | |
| immunization: <Shield className="w-4 h-4" />, | |
| 'lab-result': <FileText className="w-4 h-4" />, | |
| medication: <Plus className="w-4 h-4" />, | |
| procedure: <UserCheck className="w-4 h-4" />, | |
| 'vital-sign': <Heart className="w-4 h-4" />, | |
| }; | |
| return icons[type] || <FileText className="w-4 h-4" />; | |
| }; | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <div className="flex items-center gap-2"> | |
| <FileText className="w-5 h-5 text-green-600" /> | |
| <h2 className="text-lg font-semibold text-gray-900">Health Records</h2> | |
| </div> | |
| <button | |
| onClick={() => setShowHealthRecordForm(true)} | |
| className="inline-flex items-center gap-2 px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm" | |
| > | |
| <Plus className="w-4 h-4" /> | |
| Add Record | |
| </button> | |
| </div> | |
| <div className="space-y-4"> | |
| {profile?.healthRecords && profile.healthRecords.length > 0 ? ( | |
| profile.healthRecords.map((record, index) => ( | |
| <div key={index} className="border border-gray-200 rounded-lg p-4"> | |
| <div className="flex items-start justify-between mb-2"> | |
| <div className="flex items-center gap-2"> | |
| {getRecordTypeIcon(record.recordType)} | |
| <h3 className="font-medium text-gray-900">{record.title}</h3> | |
| </div> | |
| <span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded"> | |
| {formatRecordType(record.recordType)} | |
| </span> | |
| if (!type) return 'Unknown'; | |
| return String(type) | |
| .split('-') | |
| .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) | |
| .join(' '); | |
| }; | |
| const getRecordTypeIcon = (type) => { | |
| const icons = { | |
| allergy: <AlertCircle className="w-4 h-4" />, | |
| condition: <Heart className="w-4 h-4" />, | |
| immunization: <Shield className="w-4 h-4" />, | |
| 'lab-result': <FileText className="w-4 h-4" />, | |
| medication: <Plus className="w-4 h-4" />, | |
| procedure: <UserCheck className="w-4 h-4" />, | |
| 'vital-sign': <Heart className="w-4 h-4" />, | |
| }; | |
| return icons[type] || <FileText className="w-4 h-4" />; | |
| }; | |
| return ( | |
| <div className="space-y-6"> | |
| <div className="bg-white rounded-xl shadow-sm border border-gray-100 p-6"> | |
| <div className="flex items-center justify-between mb-6"> | |
| <div className="flex items-center gap-2"> | |
| <FileText className="w-5 h-5 text-green-600" /> | |
| <h2 className="text-lg font-semibold text-gray-900">Health Records</h2> | |
| </div> | |
| <button | |
| onClick={() => setShowHealthRecordForm(true)} | |
| className="inline-flex items-center gap-2 px-3 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm" | |
| > | |
| <Plus className="w-4 h-4" /> | |
| Add Record | |
| </button> | |
| </div> | |
| <div className="space-y-4"> | |
| {profile?.healthRecords && profile.healthRecords.length > 0 ? ( | |
| profile.healthRecords.map((record, index) => ( | |
| <div key={index} className="border border-gray-200 rounded-lg p-4"> | |
| <div className="flex items-start justify-between mb-2"> | |
| <div className="flex items-center gap-2"> | |
| {getRecordTypeIcon(record.recordType)} | |
| <h3 className="font-medium text-gray-900">{record.title}</h3> | |
| </div> | |
| <span className="text-xs bg-gray-100 text-gray-600 px-2 py-1 rounded"> | |
| {formatRecordType(record.recordType)} | |
| </span> |
🤖 Prompt for AI Agents
In client/src/components/Patient/PatientProfile/HealthRecords.jsx around lines
12 to 59, the call formatRecordType(record.recordType) can throw when
record.recordType is null/undefined; update the formatRecordType function to
guard its input (e.g., default to an empty string or 'unknown') before calling
split so it returns a safe string when recordType is missing; ensure the
function handles falsy values and still returns a user-friendly label (like
'Unknown' or an empty string) so rendering of the profile won't crash.
| const handleInputChange = (e) => { | ||
| const { name, value } = e.target; | ||
| if (name.includes('.')) { | ||
| const [parent, child] = name.split('.'); | ||
| setEditFormData((prev) => ({ | ||
| ...prev, | ||
| [parent]: { | ||
| ...prev[parent], | ||
| [child]: value, | ||
| }, | ||
| })); | ||
| } else { | ||
| setEditFormData((prev) => ({ | ||
| ...prev, | ||
| [name]: value, | ||
| })); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Handle nested form updates when parent state is missing
If editFormData lacks a nested object (common after fetchProfile fails, leaving it {}), this spread attempts ...prev[parent] where prev[parent] is undefined, throwing TypeError: Cannot convert undefined or null to object the moment a user edits an address/emergency-contact field. Guard the parent object so nested updates always work.
if (name.includes('.')) {
const [parent, child] = name.split('.');
setEditFormData((prev) => ({
...prev,
[parent]: {
- ...prev[parent],
+ ...(prev[parent] ?? {}),
[child]: value,
},
}));🤖 Prompt for AI Agents
In client/src/components/Patient/PatientProfile/ProfileContent.jsx around lines
17 to 34, the nested update assumes prev[parent] exists and will throw when it's
undefined; ensure you guard and default the parent object before spreading
(e.g., use prev[parent] || {} or nullish coalescing) so setEditFormData always
creates the nested object when missing, preserving other fields by spreading
prev and prev[parent] safely and then setting [child]: value.
Project
Change Type
Page Type
Stack
Route Status
What Changed
Route/API Affected
Description
Screenshots
Mobile View
Desktop View
Files Changed
Code Quality
Prettier Check: ✅ Passed
Related Issues
Closes #
Auto-generated on 2025-11-04T06:15:29.705Z