diff --git a/package-lock.json b/package-lock.json index 243e59dbfaf..24a1f6be783 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,12 +19,13 @@ "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", "@radix-ui/react-alert-dialog": "^1.1.2", - "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.1", @@ -5469,6 +5470,173 @@ } } }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.2.tgz", + "integrity": "sha512-E0MLLGfOP0l8P/NxgVzfXJ8w3Ch8cdO6UDzJfDChu4EJDy+/WdO5LqpdY8PYnCErkmZH3gZhDL1K7kQ41fAHuQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-roving-focus": "1.1.1", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-collection": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.1.tgz", + "integrity": "sha512-LwT3pSho9Dljg+wY2KN2mrrh6y3qELfftINERIzBUO9e0N+t0oMTyn3k9iv+ZqgrwGkRnLpNJrsMv9BZlt2yuA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-primitive": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.1.tgz", + "integrity": "sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==", + "dependencies": { + "@radix-ui/react-slot": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.1.tgz", + "integrity": "sha512-QE1RoxPGJ/Nm8Qmk0PxP8ojmoaS67i0s7hVssS7KuI2FQoc/uzVlZsqKfQvxPE6D8hICCPHJ4D88zNhT3OOmkw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.1", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group/node_modules/@radix-ui/react-slot": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.1.tgz", + "integrity": "sha512-RApLLOcINYJA+dMVbOju7MYv1Mb2EBp2nH4HdDzXTSyaR5optlm6Otrz1euW3HbdOR8UmmFK06TD+A9frYWv+g==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", diff --git a/package.json b/package.json index 39c933de45b..022bde94986 100644 --- a/package.json +++ b/package.json @@ -58,12 +58,13 @@ "@pnotify/core": "^5.2.0", "@pnotify/mobile": "^5.2.0", "@radix-ui/react-alert-dialog": "^1.1.2", - "@radix-ui/react-checkbox": "^1.1.2", + "@radix-ui/react-checkbox": "^1.1.3", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.2", "@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.0", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.1", diff --git a/public/locale/en.json b/public/locale/en.json index 3df3589c764..5186349b492 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -292,6 +292,7 @@ "add_insurance_details": "Add Insurance Details", "add_location": "Add Location", "add_new_beds": "Add New Bed(s)", + "add_new_patient": "Add new patient", "add_new_user": "Add New User", "add_notes": "Add notes", "add_policy": "Add Insurance Policy", @@ -314,8 +315,11 @@ "admitted_on": "Admitted On", "advanced_filters": "Advanced Filters", "age": "Age", + "age_input_warning": "While entering a patient's age is an option, please note that only the year of birth will be captured from this information.", + "age_input_warning_bold": "Recommended only when the patient's date of birth is unknown", "all_changes_have_been_saved": "All changes have been saved", "all_details": "All Details", + "all_patients": "All Patients", "allergies": "Allergies", "allow_transfer": "Allow Transfer", "allowed_formats_are": "Allowed formats are", @@ -589,6 +593,7 @@ "date_of_result": "Covid confirmation date", "date_of_return": "Date of Return", "date_of_test": "Date of sample collection for Covid testing", + "day": "Day", "days": "Days", "death_report": "Death Report", "delete": "Delete", @@ -638,6 +643,7 @@ "disease_status": "Disease status", "district": "District", "district_program_management_supporting_unit": "District Program Management Supporting Unit", + "dob": "DOB", "dob_format": "Please enter date in DD/MM/YYYY format", "doctor_experience_error": "Please enter a valid number between 0 and 100.", "doctor_experience_required": "Years of experience is required", @@ -686,8 +692,10 @@ "emergency_contact": "Emergency Contact", "emergency_contact_number": "Emergency Contact Number", "emergency_contact_person_name": "Emergency Contact Person Name", + "emergency_contact_person_name_details": "Emergency contact person (Father, Mother, Spouse, Sibling, Friend)", "emergency_contact_person_name_volunteer": "Emergency Contact Person Name (Volunteer)", "emergency_contact_volunteer": "Emergency Contact (Volunteer)", + "emergency_phone_number": "Emergency Phone Number", "empty_date_time": "--:-- --; --/--/----", "encounter_date_field_label__A": "Date & Time of Admission to the Facility", "encounter_date_field_label__DC": "Date & Time of Domiciliary Care commencement", @@ -777,6 +785,7 @@ "full_name": "Full Name", "full_screen": "Full Screen", "gender": "Gender", + "general_info_detail": "Provide the patient's personal details, including name, date of birth, gender, and contact information for accurate identification and communication.", "generate_link_abha": "Generate/Link ABHA Number", "generate_report": "Generate Report", "generated_summary_caution": "This is a computer generated summary using the information captured in the CARE system.", @@ -841,6 +850,7 @@ "insurance__insurer_name": "Insurer Name", "insurance__member_id": "Member ID", "insurance__policy_name": "Policy ID / Policy Name", + "insurance_details_detail": "Include details of all the Insurance Policies held by the Patient for smooth insurance processing", "insurer_name_required": "Insurer Name is required", "international_mobile": "International Mobile", "invalid_asset_id_msg": "Oops! The asset ID you entered does not appear to be valid.", @@ -893,6 +903,7 @@ "kasp_enabled_date": "{{kasp_string}} enabled date", "label": "Label", "landline": "Indian landline", + "landmark": "Landmark", "language_selection": "Language Selection", "last_administered": "Last administered", "last_discharge_reason": "Last Discharge Reason", @@ -985,6 +996,7 @@ "modified_date": "Modified Date", "modified_on": "Modified On", "monitor": "Monitor", + "month": "Month", "more_details": "More details", "more_info": "More Info", "move_to_onvif_preset": "Move to an ONVIF Preset", @@ -1014,6 +1026,7 @@ "no_facilities": "No Facilities found", "no_files_found": "No {{type}} files found", "no_home_facility": "No home facility", + "no_home_facility_found": "No home facility found", "no_image_found": "No image found", "no_investigation": "No investigation Reports found", "no_investigation_suggestions": "No Investigation Suggestions", @@ -1132,9 +1145,13 @@ "patient_registration__contact": "Emergency Contact", "patient_registration__gender": "Gender", "patient_registration__name": "Name", + "patient_registration_error": "Could not register patient", + "patient_registration_success": "Patient Registered Successfuly", "patient_state": "Patient State", "patient_status": "Patient Status", "patient_transfer_birth_match_note": "Note: Year of birth must match the patient to process the transfer request.", + "patient_update_error": "Could not update patient", + "patient_update_success": "Patient Updated Sucessfuly", "patients": "Patients", "permanent_address": "Permanent Address", "permission_denied": "You do not have permission to perform this action", @@ -1147,6 +1164,7 @@ "phone_number": "Phone Number", "phone_number_at_current_facility": "Phone Number of Contact person at current Facility", "pincode": "Pincode", + "pincode_autofill": "State and District auto-filled from Pincode", "please_assign_bed_to_patient": "Please assign a bed to this patient", "please_confirm_password": "Please confirm your new password.", "please_enter_a_reason_for_the_shift": "Please enter a reason for the shift.", @@ -1198,6 +1216,7 @@ "preset_name_placeholder": "Specify an identifiable name for the new preset", "preset_updated": "Preset updated", "prev_sessions": "Prev Sessions", + "primary_phone_no": "Primary ph. no.", "principal": "Principal", "principal_diagnosis": "Principal diagnosis", "print": "Print", @@ -1314,6 +1333,7 @@ "search_icd11_placeholder": "Search for ICD-11 Diagnoses", "search_investigation_placeholder": "Search Investigation & Groups", "search_patient": "Search Patient", + "search_patients": "Search Patients", "search_resource": "Search Resource", "see_attachments": "See Attachments", "select": "Select", @@ -1371,6 +1391,7 @@ "skill_added_successfully": "Skill added successfully", "skills": "Skills", "social_profile": "Social Profile", + "social_profile_detail": "Include occupation, ration card category, socioeconomic status, and 
domestic healthcare support for a complete profile.", "socioeconomic_status": "Socioeconomic status", "software_update": "Software Update", "something_went_wrong": "Something went wrong..!", @@ -1414,6 +1435,7 @@ "third_party_software_licenses": "Third Party Software Licenses", "titrate_dosage": "Titrate Dosage", "to_be_conducted": "To be conducted", + "to_proceed_with_registration": "To proceed with registration, please create a new patient.", "total_amount": "Total Amount", "total_beds": "Total Beds", "total_patients": "Total Patients", @@ -1439,6 +1461,7 @@ "type_b_cylinders": "B Type Cylinders", "type_c_cylinders": "C Type Cylinders", "type_d_cylinders": "D Type Cylinders", + "type_patient_name": "Type Patient Name", "type_to_search": "Type to search", "type_your_comment": "Type your comment", "type_your_reason_here": "Type your reason here", @@ -1497,11 +1520,14 @@ "upload_headings__supporting_info": "Upload Supporting Info", "upload_report": "Upload Report", "uploading": "Uploading", + "use_address_as_permanent": "Use this address for permanent address", "use_existing_abha_address": "Use Existing ABHA Address", + "use_phone_number_for_emergency": "Use this phone number for emergency contact", "user_add_error": "Error while adding User", "user_added_successfully": "User added successfully", "user_delete_error": "Error while deleting User", "user_deleted_successfully": "User Deleted Successfully", + "user_deleted_successfuly": "User Deleted Successfuly", "user_details": "User Details", "user_details_update_error": "Error while updating user details", "user_details_update_success": "User details updated successfully", @@ -1558,6 +1584,7 @@ "voice_autofill": "Voice Autofill", "volunteer_assigned": "Volunteer assigned successfully", "volunteer_contact": "Volunteer Contact", + "volunteer_contact_detail": "Provide the name and contact details of a volunteer who can assist 
the patient in emergencies. This should be someone outside the family.", "volunteer_unassigned": "Volunteer unassigned successfully", "volunteer_update": "Volunteer updated successfully", "ward": "Ward", @@ -1569,6 +1596,7 @@ "width": "Width ({{unit}})", "with": "with", "working_status": "Working Status", + "year": "Year", "year_of_birth": "Year of Birth", "years": "years", "years_of_experience": "Years of Experience", diff --git a/src/CAREUI/misc/SectionNavigator.tsx b/src/CAREUI/misc/SectionNavigator.tsx new file mode 100644 index 00000000000..78027bde371 --- /dev/null +++ b/src/CAREUI/misc/SectionNavigator.tsx @@ -0,0 +1,78 @@ +import { useEffect, useState } from "react"; + +import { cn } from "@/lib/utils"; + +import { Button } from "@/components/ui/button"; + +export default function SectionNavigator(props: { + sections: { label: string; id: string }[]; + className?: string; +}) { + const { sections, className } = props; + + const [activeSection, setActiveSection] = useState(null); + + useEffect(() => { + const updateActiveSection = () => { + sections.forEach((section) => { + const element = document.getElementById(section.id); + if (element) { + const rect = element.getBoundingClientRect(); + if (rect.top >= 0 && rect.bottom <= window.innerHeight) { + setActiveSection(section.id); + } + } + }); + }; + + const observer = new IntersectionObserver( + (entries) => { + entries.forEach((entry) => { + if (entry.isIntersecting) { + setActiveSection(entry.target.id); + } + }); + }, + { rootMargin: "0px 0px -80% 0px", threshold: 0.1 }, + ); + + sections.forEach((section) => { + const element = document.getElementById(section.id); + if (element) { + observer.observe(element); + } + }); + + updateActiveSection(); // Update on page load + + return () => { + sections.forEach((section) => { + const element = document.getElementById(section.id); + if (element) { + observer.unobserve(element); + } + }); + }; + }, [sections]); + + return ( +
+ {sections.map((section) => ( + + ))} +
+ ); +} diff --git a/src/Routers/routes/PatientRoutes.tsx b/src/Routers/routes/PatientRoutes.tsx index 31d6d99a037..8adc9ac1fd9 100644 --- a/src/Routers/routes/PatientRoutes.tsx +++ b/src/Routers/routes/PatientRoutes.tsx @@ -2,23 +2,27 @@ import DeathReport from "@/components/DeathReport/DeathReport"; import InvestigationReports from "@/components/Facility/Investigations/Reports"; import FileUploadPage from "@/components/Patient/FileUploadPage"; import { InsuranceDetails } from "@/components/Patient/InsuranceDetails"; -import { PatientManager } from "@/components/Patient/ManagePatients"; import { patientTabs } from "@/components/Patient/PatientDetailsTab"; import { PatientHome } from "@/components/Patient/PatientHome"; +import PatientIndex from "@/components/Patient/PatientIndex"; import PatientNotes from "@/components/Patient/PatientNotes"; -import { PatientRegister } from "@/components/Patient/PatientRegister"; +import PatientRegistration from "@/components/Patient/PatientRegistration"; import { AppRoutes } from "@/Routers/AppRouter"; const PatientRoutes: AppRoutes = { - "/patients": () => , + //"/patients": () => , + "/patients": () => , "/patient/:id": ({ id }) => , "/patient/:id/investigation_reports": ({ id }) => ( ), - "/facility/:facilityId/patient": ({ facilityId }) => ( - + "/facility/:facilityId/register-patient": ({ facilityId }) => ( + ), + // "/facility/:facilityId/patient": ({ facilityId }) => ( + // + // ), "/facility/:facilityId/patient/:id": ({ facilityId, id }) => ( ), @@ -33,7 +37,7 @@ const PatientRoutes: AppRoutes = { ), "/facility/:facilityId/patient/:id/update": ({ facilityId, id }) => ( - + ), "/facility/:facilityId/patient/:patientId/notes": ({ facilityId, diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index f63f04b3d61..3830af48728 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -233,6 +233,7 @@ export const parsePhoneNumber = (phoneNumber: string, countryCode?: string) => { if (phoneNumber === "+91") return ""; const phoneCodes: Record = phoneCodesJson; let parsedNumber = phoneNumber.replace(/[-+() ]/g, ""); + if (parsedNumber.length < 12) return ""; if (countryCode && phoneCodes[countryCode]) { parsedNumber = phoneCodes[countryCode].code + parsedNumber; } else if (!phoneNumber.startsWith("+")) { @@ -393,7 +394,7 @@ export const formatPatientAge = (obj: PatientModel, abbreviated = false) => { const years = end.diff(start, "years"); if (years) { - return `${years}${suffixes.year}`; + return `${years} ${suffixes.year}`; } // Skip representing as no. of months/days if we don't know the date of birth diff --git a/src/components/Common/Breadcrumbs.tsx b/src/components/Common/Breadcrumbs.tsx index c2c4aa57446..4c602d9f9cc 100644 --- a/src/components/Common/Breadcrumbs.tsx +++ b/src/components/Common/Breadcrumbs.tsx @@ -109,7 +109,7 @@ export default function Breadcrumbs({ diff --git a/src/components/Common/SearchByMultipleFields.tsx b/src/components/Common/SearchByMultipleFields.tsx index c709dab9b9f..35413076cc9 100644 --- a/src/components/Common/SearchByMultipleFields.tsx +++ b/src/components/Common/SearchByMultipleFields.tsx @@ -30,7 +30,6 @@ import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormFi interface SearchOption { key: string; - label: string; type: "text" | "phone"; placeholder: string; value: string; @@ -241,7 +240,7 @@ const SearchByMultipleFields: React.FC = ({ {t(option.key)} - {option.label.charAt(0).toUpperCase()} + {option.shortcutKey} ))} @@ -250,7 +249,7 @@ const SearchByMultipleFields: React.FC = ({ - {renderSearchInput} +
{renderSearchInput}
{error && (
diff --git a/src/components/Facility/FacilityList.tsx b/src/components/Facility/FacilityList.tsx index 0c1164649ca..220388cab92 100644 --- a/src/components/Facility/FacilityList.tsx +++ b/src/components/Facility/FacilityList.tsx @@ -197,7 +197,6 @@ export const FacilityList = () => { options={[ { key: "facility_district_name", - label: "Facility or District Name", type: "text" as const, placeholder: "facility_search_placeholder", value: qParams.search || "", diff --git a/src/components/Patient/ManagePatients.tsx b/src/components/Patient/ManagePatients.tsx index 0cae44163b2..c05d91175a5 100644 --- a/src/components/Patient/ManagePatients.tsx +++ b/src/components/Patient/ManagePatients.tsx @@ -7,7 +7,6 @@ import { Avatar } from "@/components/Common/Avatar"; import ButtonV2 from "@/components/Common/ButtonV2"; import { ExportMenu } from "@/components/Common/Export"; import Loading from "@/components/Common/Loading"; -import Page from "@/components/Common/Page"; import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; import SortDropdownMenu from "@/components/Common/SortDropdown"; import Tabs from "@/components/Common/Tabs"; @@ -801,169 +800,116 @@ export const PatientManager = () => { ); return ( - -
- { - const showAllFacilityUsers = ["DistrictAdmin", "StateAdmin"]; + <> +
+
+
+ { + if (tab === 0) { + updateQuery({ is_active: "True" }); + } else { + const id = qParams.facility || onlyAccessibleFacility?.id; + if (id) { + navigate(`facility/${id}/discharged-patients`); + return; + } + if ( - qParams.facility && - showAllFacilityUsers.includes(authUser.user_type) - ) - navigate(`/facility/${qParams.facility}/patient`); - else if ( - qParams.facility && - !showAllFacilityUsers.includes(authUser.user_type) && - authUser.home_facility_object?.id !== qParams.facility - ) - Notification.Error({ - msg: "Oops! Non-Home facility users don't have permission to perform this action.", - }); - else if ( - !showAllFacilityUsers.includes(authUser.user_type) && - authUser.home_facility_object?.id + authUser.user_type === "StateAdmin" || + authUser.user_type === "StateReadOnlyAdmin" ) { - navigate( - `/facility/${authUser.home_facility_object.id}/patient`, - ); - } else if (onlyAccessibleFacility) - navigate(`/facility/${onlyAccessibleFacility.id}/patient`); - else if ( - !showAllFacilityUsers.includes(authUser.user_type) && - !authUser.home_facility_object?.id - ) - Notification.Error({ - msg: "Oops! No home facility found", - }); - else setShowDialog("create"); - }} + updateQuery({ is_active: "False" }); + return; + } + + Notification.Warn({ + msg: t("select_facility_for_discharged_patients_warning"), + }); + setShowDialog("list-discharged"); + } + }} + currentTab={tabValue} + /> + {!!params.facility && ( + { + triggerGoal("Doctor Connect Clicked", { + facilityId: qParams.facility, + userId: authUser.id, + page: "FacilityPatientsList", + }); + setShowDoctors(true); + }} > - -

- Add Patient -

+ +

Doctor Connect

-
-
- { - if (tab === 0) { - updateQuery({ is_active: "True" }); - } else { - const id = qParams.facility || onlyAccessibleFacility?.id; - if (id) { - navigate(`facility/${id}/discharged-patients`); - return; - } + )} - if ( - authUser.user_type === "StateAdmin" || - authUser.user_type === "StateReadOnlyAdmin" - ) { - updateQuery({ is_active: "False" }); - return; - } - - Notification.Warn({ - msg: t("select_facility_for_discharged_patients_warning"), - }); - setShowDialog("list-discharged"); - } - }} - currentTab={tabValue} - /> - {!!params.facility && ( + advancedFilter.setShow(true)} /> + +
+ {!isExportAllowed ? ( { - triggerGoal("Doctor Connect Clicked", { - facilityId: qParams.facility, - userId: authUser.id, - page: "FacilityPatientsList", - }); - setShowDoctors(true); + advancedFilter.setShow(true); + setTimeout(() => { + const element = document.getElementById("bed-type-select"); + if (element) element.scrollIntoView({ behavior: "smooth" }); + Notification.Warn({ + msg: "Please select a seven day period.", + }); + }, 500); }} + className="mr-5 w-full lg:w-fit" > - -

Doctor Connect

+ + Export
- )} - - advancedFilter.setShow(true)} - /> - -
- {!isExportAllowed ? ( - { - advancedFilter.setShow(true); - setTimeout(() => { - const element = - document.getElementById("bed-type-select"); - if (element) - element.scrollIntoView({ behavior: "smooth" }); - Notification.Warn({ - msg: "Please select a seven day period.", + ) : ( + { + const query = { + ...params, + csv: true, + facility: qParams.facility, + }; + delete qParams.is_active; + const { data } = await request(routes.patientList, { + query, }); - }, 500); - }} - className="mr-5 w-full lg:w-fit" - > - - Export - - ) : ( - { - const query = { - ...params, - csv: true, - facility: qParams.facility, - }; - delete qParams.is_active; - const { data } = await request(routes.patientList, { - query, - }); - return data ?? null; - }, - parse: preventDuplicatePatientsDuetoPolicyId, + return data ?? null; }, - ]} - /> - )} + parse: preventDuplicatePatientsDuetoPolicyId, + }, + ]} + /> + )} - {!isExportAllowed && ( - - Select a seven day period - - )} -
+ {!isExportAllowed && ( + + Select a seven day period + + )}
- } - > +
+ setSelectedFacility(e)} @@ -1006,7 +952,7 @@ export const PatientManager = () => { className="w-full" />
-
+
{ setShow={setShowDoctors} />
- + ); }; diff --git a/src/components/Patient/PatientFilter.tsx b/src/components/Patient/PatientFilter.tsx index e2ab6cd30b4..9db5f34209e 100644 --- a/src/components/Patient/PatientFilter.tsx +++ b/src/components/Patient/PatientFilter.tsx @@ -1,7 +1,9 @@ import careConfig from "@careConfig"; import dayjs from "dayjs"; +import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; +import FilterBadge from "@/CAREUI/display/FilterBadge"; import CareIcon from "@/CAREUI/icons/CareIcon"; import FiltersSlideover from "@/CAREUI/interactive/FiltersSlideover"; @@ -22,10 +24,13 @@ import { import MultiSelectMenuV2 from "@/components/Form/MultiSelectMenuV2"; import SelectMenuV2 from "@/components/Form/SelectMenuV2"; import DiagnosesFilter, { + DIAGNOSES_FILTER_LABELS, + DiagnosesFilterKey, FILTER_BY_DIAGNOSES_KEYS, } from "@/components/Patient/DiagnosesFilter"; import useAuthUser from "@/hooks/useAuthUser"; +import useFilters from "@/hooks/useFilters"; import useMergeState from "@/hooks/useMergeState"; import { @@ -34,14 +39,19 @@ import { DISCHARGE_REASONS, FACILITY_TYPES, GENDER_TYPES, + PATIENT_CATEGORIES, PATIENT_FILTER_CATEGORIES, RATION_CARD_CATEGORY, } from "@/common/constants"; +import { parseOptionId } from "@/common/utils"; import routes from "@/Utils/request/api"; import request from "@/Utils/request/request"; import useTanStackQueryInstead from "@/Utils/request/useQuery"; -import { dateQueryString } from "@/Utils/utils"; +import { dateQueryString, humanizeStrings } from "@/Utils/utils"; + +import { ICD11DiagnosisModel } from "../Diagnosis/types"; +import { getDiagnosesByIds } from "../Diagnosis/utils"; const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); @@ -782,3 +792,266 @@ export default function PatientFilter(props: any) { ); } + +export function PatientFilterBadges() { + const { t } = useTranslation(); + + const { qParams, FilterBadges, updateQuery } = useFilters({ + limit: 12, + cacheBlacklist: [ + "name", + "patient_no", + "phone_number", + "emergency_phone_number", + ], + }); + + const [diagnoses, setDiagnoses] = useState([]); + + const { data: districtData } = useTanStackQueryInstead(routes.getDistrict, { + pathParams: { + id: qParams.district, + }, + prefetch: !!Number(qParams.district), + }); + + const { data: LocalBodyData } = useTanStackQueryInstead(routes.getLocalBody, { + pathParams: { + id: qParams.lsgBody, + }, + prefetch: !!Number(qParams.lsgBody), + }); + + const { data: facilityData } = useTanStackQueryInstead( + routes.getAnyFacility, + { + pathParams: { + id: qParams.facility, + }, + prefetch: !!qParams.facility, + }, + ); + const { data: facilityAssetLocationData } = useTanStackQueryInstead( + routes.getFacilityAssetLocation, + { + pathParams: { + facility_external_id: qParams.facility, + external_id: qParams.last_consultation_current_bed__location, + }, + prefetch: !!qParams.last_consultation_current_bed__location, + }, + ); + + const LastAdmittedToTypeBadges = () => { + const badge = (key: string, value: string | undefined, id: string) => { + return ( + value && ( + { + const lcat = qParams.last_consultation_admitted_bed_type_list + .split(",") + .filter((x: string) => x != id) + .join(","); + updateQuery({ + ...qParams, + last_consultation_admitted_bed_type_list: lcat, + }); + }} + /> + ) + ); + }; + return qParams.last_consultation_admitted_bed_type_list + .split(",") + .map((id: string) => { + const text = ADMITTED_TO.find((obj) => obj.id == id)?.text; + return badge("Bed Type", text, id); + }); + }; + + const HasConsentTypesBadges = () => { + const badge = (key: string, value: string | undefined, id: string) => { + return ( + value && ( + { + const lcat = qParams.last_consultation__consent_types + .split(",") + .filter((x: string) => x != id) + .join(","); + updateQuery({ + ...qParams, + last_consultation__consent_types: lcat, + }); + }} + /> + ) + ); + }; + + return qParams.last_consultation__consent_types + .split(",") + .map((id: string) => { + const text = [ + ...CONSENT_TYPE_CHOICES, + { id: "None", text: "No Consents" }, + ].find((obj) => obj.id == id)?.text; + return badge("Has Consent", text, id); + }); + }; + + const getTheCategoryFromId = () => { + let category_name; + if (qParams.category) { + category_name = PATIENT_CATEGORIES.find( + (item: any) => qParams.category === item.id, + )?.text; + + return String(category_name); + } else { + return ""; + } + }; + + const getDiagnosisFilterValue = (key: DiagnosesFilterKey) => { + const ids: string[] = (qParams[key] ?? "").split(","); + return ids.map((id) => diagnoses.find((obj) => obj.id == id)?.label ?? id); + }; + + useEffect(() => { + const ids: string[] = []; + FILTER_BY_DIAGNOSES_KEYS.forEach((key) => { + ids.push(...(qParams[key] ?? "").split(",").filter(Boolean)); + }); + const existing = diagnoses.filter(({ id }) => ids.includes(id)); + const objIds = existing.map((o) => o.id); + const diagnosesToBeFetched = ids.filter((id) => !objIds.includes(id)); + getDiagnosesByIds(diagnosesToBeFetched).then((data) => { + const retrieved = data.filter(Boolean) as ICD11DiagnosisModel[]; + setDiagnoses([...existing, ...retrieved]); + }); + }, [ + qParams.diagnoses, + qParams.diagnoses_confirmed, + qParams.diagnoses_provisional, + qParams.diagnoses_unconfirmed, + qParams.diagnoses_differential, + ]); + + return ( + [ + phoneNumber("Primary number", "phone_number"), + phoneNumber("Emergency number", "emergency_phone_number"), + badge("Patient name", "name"), + badge("IP/OP number", "patient_no"), + ...dateRange("Modified", "modified_date"), + ...dateRange("Created", "created_date"), + ...dateRange("Admitted", "last_consultation_encounter_date"), + ...dateRange("Discharged", "last_consultation_discharge_date"), + // Admitted to type badges + badge("No. of vaccination doses", "number_of_doses"), + kasp(), + badge("COWIN ID", "covin_id"), + badge("Is Antenatal", "is_antenatal"), + badge("Review Missed", "review_missed"), + badge("Is Medico-Legal Case", "last_consultation_medico_legal_case"), + value( + "Ration Card Category", + "ration_card_category", + qParams.ration_card_category + ? t(`ration_card__${qParams.ration_card_category}`) + : "", + ), + value( + "Facility", + "facility", + qParams.facility ? facilityData?.name || "" : "", + ), + value( + "Location", + "last_consultation_current_bed__location", + qParams.last_consultation_current_bed__location + ? facilityAssetLocationData?.name || + qParams.last_consultation_current_bed__locations + : "", + ), + badge("Facility Type", "facility_type"), + value( + "District", + "district", + qParams.district ? districtData?.name || "" : "", + ), + ordering(), + value("Category", "category", getTheCategoryFromId()), + value( + "Respiratory Support", + "ventilator_interface", + qParams.ventilator_interface && + t(`RESPIRATORY_SUPPORT_SHORT__${qParams.ventilator_interface}`), + ), + value( + "Gender", + "gender", + parseOptionId(GENDER_TYPES, qParams.gender) || "", + ), + { + name: "Admitted to", + value: ADMITTED_TO[qParams.last_consultation_admitted_to], + paramKey: "last_consultation_admitted_to", + }, + ...range("Age", "age"), + { + name: "LSG Body", + value: qParams.lsgBody ? LocalBodyData?.name || "" : "", + paramKey: "lsgBody", + }, + ...FILTER_BY_DIAGNOSES_KEYS.map((key) => + value( + DIAGNOSES_FILTER_LABELS[key], + key, + humanizeStrings(getDiagnosisFilterValue(key)), + ), + ), + badge("Declared Status", "is_declared_positive"), + ...dateRange("Declared positive", "date_declared_positive"), + ...dateRange("Last vaccinated", "last_vaccinated_date"), + { + name: "Telemedicine", + paramKey: "last_consultation_is_telemedicine", + }, + value( + "Discharge Reason", + "last_consultation__new_discharge_reason", + parseOptionId( + DISCHARGE_REASONS, + qParams.last_consultation__new_discharge_reason, + ) || "", + ), + ]} + children={ + (qParams.last_consultation_admitted_bed_type_list || + qParams.last_consultation__consent_types) && ( + <> + {qParams.last_consultation_admitted_bed_type_list && + LastAdmittedToTypeBadges()} + {qParams.last_consultation__consent_types && + HasConsentTypesBadges()} + + ) + } + /> + ); +} diff --git a/src/components/Patient/PatientIndex.tsx b/src/components/Patient/PatientIndex.tsx new file mode 100644 index 00000000000..2074f47f3e4 --- /dev/null +++ b/src/components/Patient/PatientIndex.tsx @@ -0,0 +1,359 @@ +import dayjs from "dayjs"; +import { navigate } from "raviger"; +import { useCallback, useState } from "react"; +import { useTranslation } from "react-i18next"; +import useKeyboardShortcut from "use-keyboard-shortcut"; + +import { cn } from "@/lib/utils"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import useAuthUser from "@/hooks/useAuthUser"; +import useFilters from "@/hooks/useFilters"; + +import { GENDER_TYPES } from "@/common/constants"; + +import routes from "@/Utils/request/api"; +import useQuery from "@/Utils/request/useQuery"; +import { formatPatientAge, parsePhoneNumber } from "@/Utils/utils"; + +import * as Notification from "../../Utils/Notifications"; +import Loading from "../Common/Loading"; +import Page from "../Common/Page"; +import SearchByMultipleFields from "../Common/SearchByMultipleFields"; +import FacilitiesSelectDialogue from "../ExternalResult/FacilitiesSelectDialogue"; +import { FacilityModel } from "../Facility/models"; +import { Button } from "../ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../ui/table"; +import { TabbedSections } from "../ui/tabs"; +import { PatientManager } from "./ManagePatients"; +import PatientFilter, { PatientFilterBadges } from "./PatientFilter"; +import { getPatientUrl } from "./Utils"; + +export default function PatientIndex() { + const { t } = useTranslation(); + const [showDialog, setShowDialog] = useState<"create" | "list-discharged">(); + const [selectedFacility, setSelectedFacility] = useState({ + name: "", + }); + const { + qParams, + updateQuery, + advancedFilter, + Pagination, + resultsPerPage, + clearSearch, + } = useFilters({ + limit: 12, + cacheBlacklist: [ + "name", + "patient_no", + "phone_number", + "emergency_phone_number", + ], + }); + + const searchOptions = [ + { + key: "name", + type: "text" as const, + placeholder: "search_by_patient_name", + value: qParams.name || "", + shortcutKey: "n", + }, + { + key: "patient_no", + type: "text" as const, + placeholder: "search_by_patient_no", + value: qParams.patient_no || "", + shortcutKey: "u", + }, + { + key: "phone_number", + type: "phone" as const, + placeholder: "Search_by_phone_number", + value: qParams.phone_number || "", + shortcutKey: "p", + }, + + { + key: "emergency_contact_number", + type: "phone" as const, + placeholder: "search_by_emergency_phone_number", + value: qParams.emergency_phone_number || "", + shortcutKey: "e", + }, + ]; + + const authUser = useAuthUser(); + + const handleSearch = useCallback( + (key: string, value: string) => { + const updatedQuery = { + phone_number: + key === "phone_number" + ? value.length >= 13 || value === "" + ? value + : undefined + : undefined, + name: key === "name" ? value : undefined, + patient_no: key === "patient_no" ? value : undefined, + emergency_phone_number: + key === "emergency_contact_number" + ? value.length >= 13 || value === "" + ? value + : undefined + : undefined, + }; + + updateQuery(updatedQuery); + }, + [updateQuery], + ); + + const getCleanedParams = ( + params: Record, + ) => { + const cleaned: typeof params = {}; + Object.keys(params).forEach((key) => { + if (params[key] !== 0 && params[key] !== "") { + cleaned[key] = params[key]; + } + }); + return cleaned; + }; + + const params = getCleanedParams({ + ...qParams, + page: qParams.page || 1, + limit: resultsPerPage, + is_active: + !qParams.last_consultation__new_discharge_reason && + (qParams.is_active || "True"), + phone_number: qParams.phone_number + ? parsePhoneNumber(qParams.phone_number) + : undefined, + emergency_phone_number: qParams.emergency_phone_number + ? parsePhoneNumber(qParams.emergency_phone_number) + : undefined, + local_body: qParams.lsgBody || undefined, + offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, + last_menstruation_start_date_after: + (qParams.is_antenatal === "true" && + dayjs().subtract(9, "month").format("YYYY-MM-DD")) || + undefined, + }); + + const isValidSearch = searchOptions.some((o) => !!o.value); + + const listingQuery = useQuery(routes.patientList, { + query: params, + prefetch: isValidSearch, + }); + + const { data: permittedFacilities } = useQuery( + routes.getPermittedFacilities, + { + query: { limit: 1 }, + }, + ); + + const onlyAccessibleFacility = + permittedFacilities?.count === 1 ? permittedFacilities.results[0] : null; + + const handleAddPatient = () => { + let facilityId = ""; + const showAllFacilityUsers = ["DistrictAdmin", "StateAdmin"]; + const userCanSeeAllFacilities = showAllFacilityUsers.includes( + authUser.user_type, + ); + const userHomeFacilityId = authUser.home_facility_object?.id; + if (qParams.facility && userCanSeeAllFacilities) + facilityId = qParams.facility; + else if ( + qParams.facility && + !userCanSeeAllFacilities && + userHomeFacilityId !== qParams.facility + ) + Notification.Error({ + msg: t("permission_denied"), + }); + else if (!userCanSeeAllFacilities && userHomeFacilityId) { + facilityId = userHomeFacilityId; + } else if (onlyAccessibleFacility) + facilityId = onlyAccessibleFacility.id || ""; + else if (!userCanSeeAllFacilities && !userHomeFacilityId) { + Notification.Error({ + msg: t("no_home_facility_found"), + }); + return; + } else { + setShowDialog("create"); + return; + } + navigate(`/facility/${facilityId}/register-patient`); + }; + + function AddPatientButton(props: { outline?: boolean }) { + useKeyboardShortcut(["Shift", "P"], handleAddPatient); + return ( + + ); + } + + return ( + } + > + +
+
+
+ +
+ +
+ +
+ {isValidSearch && + !listingQuery.loading && + !listingQuery.data?.results.length && ( +
+ {t("no_records_found")} +
+ {t("to_proceed_with_registration")} + +
+ )} + {isValidSearch && listingQuery.loading && } + {isValidSearch && !!listingQuery.data?.results.length && ( + + + + {t("name")}/IP/OP + + {t("primary_phone_no")} + + + {t("dob")}/{t("age")} + + {t("sex")} + + + + {listingQuery.data?.results.map((patient, i) => ( + navigate(getPatientUrl(patient))} + > + + {patient.name} + +
+ {patient.last_consultation?.patient_no} +
+ + {patient.phone_number} + + + {!!patient.date_of_birth && + dayjs(patient.date_of_birth).format( + "DD-MM-YYYY", + )}{" "} + ({formatPatientAge(patient)}) + + + { + GENDER_TYPES.find((g) => g.id === patient.gender) + ?.text + } + +
+ ))} +
+
+ )} + {listingQuery.data && ( + + )} +
+ ), + }, + { + label: t("all_patients"), + value: "all", + section: , + }, + ]} + /> + + setSelectedFacility(e)} + selectedFacility={selectedFacility} + handleOk={() => { + switch (showDialog) { + case "create": + navigate(`facility/${selectedFacility.id}/register-patient`); + break; + case "list-discharged": + navigate(`facility/${selectedFacility.id}/discharged-patients`); + break; + } + }} + handleCancel={() => { + setShowDialog(undefined); + setSelectedFacility({ name: "" }); + }} + /> +
+ ); +} diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx new file mode 100644 index 00000000000..b89bc33f679 --- /dev/null +++ b/src/components/Patient/PatientRegistration.tsx @@ -0,0 +1,1014 @@ +import careConfig from "@careConfig"; +import { useQuery } from "@tanstack/react-query"; +import { navigate } from "raviger"; +import { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; +import SectionNavigator from "@/CAREUI/misc/SectionNavigator"; + +import useAppHistory from "@/hooks/useAppHistory"; + +import { + BLOOD_GROUPS, + DOMESTIC_HEALTHCARE_SUPPORT_CHOICES, + GENDER_TYPES, + OCCUPATION_TYPES, + RATION_CARD_CATEGORY, + SOCIOECONOMIC_STATUS_CHOICES, +} from "@/common/constants"; +import countryList from "@/common/static/countries.json"; +import { validatePincode } from "@/common/validation"; + +import * as Notification from "@/Utils/Notifications"; +import routes from "@/Utils/request/api"; +import query from "@/Utils/request/query"; +import useMutation from "@/Utils/request/useMutation"; +import { + dateQueryString, + getPincodeDetails, + includesIgnoreCase, + parsePhoneNumber, +} from "@/Utils/utils"; + +import DialogModal from "../Common/Dialog"; +import Loading from "../Common/Loading"; +import Page from "../Common/Page"; +import DuplicatePatientDialog from "../Facility/DuplicatePatientDialog"; +import TransferPatientDialog from "../Facility/TransferPatientDialog"; +import { Button } from "../ui/button"; +import { Checkbox } from "../ui/checkbox"; +import { InputErrors } from "../ui/errors"; +import { Input } from "../ui/input"; +import { Label } from "../ui/label"; +import { RadioGroup, RadioGroupItem } from "../ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "../ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; +import { Textarea } from "../ui/textarea"; +import { PatientModel } from "./models"; + +interface PatientRegistrationPageProps { + facilityId: string; + patientId?: string; +} + +export default function PatientRegistration( + props: PatientRegistrationPageProps, +) { + const { patientId, facilityId } = props; + const { t } = useTranslation(); + const { goBack } = useAppHistory(); + + const [samePhoneNumber, setSamePhoneNumber] = useState(false); + const [sameAddress, setSameAddress] = useState(false); + const [ageDob, setAgeDob] = useState<"dob" | "age">("dob"); + const [showAutoFilledPincode, setShowAutoFilledPincode] = useState(false); + const [form, setForm] = useState>({ + nationality: "India", + phone_number: "+91", + emergency_phone_number: "+91", + }); + const [age, setAge] = useState(); + const [feErrors, setFeErrors] = useState< + Partial> + >({}); + const [suppressDuplicateWarning, setSuppressDuplicateWarning] = + useState(!!patientId); + const [showTransferDialog, setShowTransferDialog] = useState(false); + + const sidebarItems = [ + { label: t("patient__general-info"), id: "general-info" }, + { label: t("social_profile"), id: "social-profile" }, + //{ label: t("volunteer_contact"), id: "volunteer-contact" }, + //{ label: t("patient__insurance-details"), id: "insurance-details" }, + ]; + + const mutationFields: (keyof PatientModel)[] = [ + "name", + "phone_number", + "emergency_phone_number", + "gender", + "blood_group", + "date_of_birth", + "age", + "address", + "permanent_address", + "pincode", + "nationality", + "state", + "district", + "local_body", + "ward", + "village", + "meta_info", + ]; + + const mutationData: Partial = { + ...Object.fromEntries( + Object.entries(form).filter(([key]) => + mutationFields.includes(key as keyof PatientModel), + ), + ), + date_of_birth: + ageDob === "dob" ? dateQueryString(form.date_of_birth) : undefined, + year_of_birth: + ageDob === "age" ? new Date().getFullYear() - (age || 0) : undefined, + is_active: true, + is_antenatal: false, + passport_no: form.nationality === "Indian" ? form.passport_no : undefined, + meta_info: { + ...(form.meta_info as any), + occupation: + form.meta_info?.occupation === "" + ? undefined + : form.meta_info?.occupation, + }, + }; + + const createPatientMutation = useMutation(routes.addPatient, { + body: { ...mutationData, facility: facilityId, ward_old: undefined }, + onResponse: (resp) => { + if (resp.error) { + Notification.Error({ + msg: t("patient_registration_error"), + }); + } else { + Notification.Success({ + msg: t("patient_registration_success"), + }); + navigate( + `/facility/${facilityId}/patient/${resp.data?.id}/consultation`, + ); + } + }, + }); + + const updatePatientMutation = useMutation(routes.updatePatient, { + pathParams: { id: patientId || "" }, + body: { ...mutationData, ward_old: undefined }, + onResponse: (data) => { + if (data.error) { + Notification.Error({ + msg: t("patient_update_error"), + }); + } else { + Notification.Success({ + msg: t("patient_update_success"), + }); + goBack(); + } + }, + }); + + const patientQuery = useQuery({ + queryKey: ["patient", patientId], + queryFn: query(routes.getPatient, { + pathParams: { id: patientId || "" }, + }), + enabled: !!patientId, + }); + + const setAddress = async (args: { + state: (typeof form)["state"]; + district?: (typeof form)["district"]; + local_body?: (typeof form)["local_body"]; + ward?: string; + }) => { + const { state, district, local_body, ward } = args; + setForm((f) => ({ + ...f, + state, + })); + await new Promise((resolve) => setTimeout(resolve, 500)); + const districts = await districtsQuery.refetch(); + + const matchedDistrict = districts.data?.find((d) => d.id === district); + if (!matchedDistrict) return; + setForm((f) => ({ + ...f, + district: matchedDistrict.id, + })); + + if (local_body) { + await new Promise((resolve) => setTimeout(resolve, 500)); + const localBodies = await localBodyQuery.refetch(); + + const matchedLocalBody = localBodies.data?.find( + (lb) => lb.id === local_body, + ); + if (!matchedLocalBody) return; + setForm((f) => ({ + ...f, + local_body: matchedLocalBody.id, + })); + + if (ward) { + await new Promise((resolve) => setTimeout(resolve, 500)); + const wards = await wardsQuery.refetch(); + + const matchedWard = wards.data?.results.find( + (w) => w.id === Number(ward), + ); + if (!matchedWard) return; + setForm((f) => ({ + ...f, + ward: matchedWard.id.toString(), + })); + } + } + }; + + useEffect(() => { + if (patientQuery.data) { + setForm(patientQuery.data); + if (patientQuery.data.year_of_birth) { + const calculatedAge = + new Date().getFullYear() - patientQuery.data.year_of_birth; + setAge(calculatedAge); + setAgeDob("age"); + } + if ( + patientQuery.data.phone_number === + patientQuery.data.emergency_phone_number + ) + setSamePhoneNumber(true); + if (patientQuery.data.address === patientQuery.data.permanent_address) + setSameAddress(true); + setAddress({ + state: patientQuery.data.state, + district: patientQuery.data.district, + local_body: patientQuery.data.local_body, + ward: patientQuery.data.ward, + }); + } + }, [patientQuery.data]); + + const statesQuery = useQuery({ + queryKey: ["states"], + queryFn: query(routes.statesList), + }); + + const districtsQuery = useQuery({ + queryKey: ["districts", form.state], + enabled: !!form.state, + queryFn: query(routes.getDistrictByState, { + pathParams: { id: form.state?.toString() || "" }, + }), + }); + + const localBodyQuery = useQuery({ + queryKey: ["localbodies", form.district], + enabled: !!form.district, + queryFn: query(routes.getLocalbodyByDistrict, { + pathParams: { id: form.district?.toString() || "" }, + }), + }); + + const wardsQuery = useQuery({ + queryKey: ["wards", form.local_body], + enabled: !!form.local_body, + queryFn: query(routes.getWardByLocalBody, { + pathParams: { id: form.local_body?.toString() || "" }, + }), + }); + + const handlePincodeChange = async (value: string) => { + if (!validatePincode(value)) return; + if (form.state && form.district) return; + + const pincodeDetails = await getPincodeDetails( + value, + careConfig.govDataApiKey, + ); + if (!pincodeDetails) return; + + const matchedState = statesQuery.data?.results?.find((state) => { + return includesIgnoreCase(state.name, pincodeDetails.statename); + }); + if (!matchedState) return; + setForm((f) => ({ + ...f, + state: matchedState.id, + })); + await new Promise((resolve) => setTimeout(resolve, 500)); + const districts = await districtsQuery.refetch(); + + const matchedDistrict = districts.data?.find((district) => { + return includesIgnoreCase(district.name, pincodeDetails.districtname); + }); + if (!matchedDistrict) return; + setForm((f) => ({ + ...f, + district: matchedDistrict.id, + })); + + setShowAutoFilledPincode(true); + setTimeout(() => { + setShowAutoFilledPincode(false); + }, 2000); + }; + + useEffect(() => { + const timeout = setTimeout( + () => handlePincodeChange(form.pincode?.toString() || ""), + 1000, + ); + return () => clearTimeout(timeout); + }, [form.pincode]); + + const title = !patientId + ? t("add_details_of_patient") + : t("update_patient_details"); + + const errors = { ...feErrors, ...createPatientMutation.error }; + + const fieldProps = (field: keyof typeof form) => ({ + value: form[field] as string, + onChange: (e: React.ChangeEvent) => + setForm((f) => ({ + ...f, + [field]: e.target.value === "" ? undefined : e.target.value, + })), + errors: errors[field], + }); + + const selectProps = (field: keyof typeof form) => ({ + value: (form[field] as string)?.toString(), + onValueChange: (value: string) => + setForm((f) => ({ ...f, [field]: value })), + }); + + const handleDialogClose = (action: string) => { + if (action === "transfer") { + setShowTransferDialog(true); + } else if (action === "back") { + setShowTransferDialog(false); + } else { + setSuppressDuplicateWarning(true); + setShowTransferDialog(false); + } + }; + + const handleFormSubmit = (e: React.FormEvent) => { + e.preventDefault(); + const errors: Record = {}; + const requiredFields: Array = [ + "name", + "phone_number", + "emergency_phone_number", + "gender", + "blood_group", + ageDob === "dob" ? "date_of_birth" : "year_of_birth", + "pincode", + "nationality", + "address", + "permanent_address", + ]; + + if (form.nationality === "India") { + requiredFields.push("state", "district", "local_body"); + } + + requiredFields.forEach((field) => { + if (!form[field]) { + errors[field] = errors[field] || []; + errors[field].push(`This field is required`); + } else if ( + ageDob === "dob" && + field === "date_of_birth" && + !/^(19[0-9]{2}|20[0-9]{2}|2100)-(0?[1-9]|1[0-2])-(0?[1-9]|[12]\d|3[01])$/.test( + form[field], + ) + ) { + errors[field] = errors[field] || []; + errors[field].push(`Invalid date format, expected DD-MM-YYYY`); + } + }); + + if (Object.keys(errors).length > 0) { + setFeErrors(errors); + } else { + patientId + ? updatePatientMutation.mutate() + : createPatientMutation.mutate(); + } + }; + + const [debouncedNumber, setDebouncedNumber] = useState(); + + useEffect(() => { + const handler = setTimeout(() => { + if (!patientId || patientQuery.data?.phone_number !== form.phone_number) { + setSuppressDuplicateWarning(false); + } + setDebouncedNumber(form.phone_number); + }, 500); + + return () => { + clearTimeout(handler); + }; + }, [form.phone_number]); + + const patientPhoneSearch = useQuery({ + queryKey: ["patients", "phone-number", debouncedNumber], + queryFn: query(routes.searchPatient, { + queryParams: { + phone_number: parsePhoneNumber(debouncedNumber || "") || "", + }, + }), + enabled: !!parsePhoneNumber(debouncedNumber || ""), + }); + + const duplicatePatients = patientPhoneSearch.data?.results.filter( + (p) => p.patient_id !== patientId, + ); + if (patientId && patientQuery.isLoading) { + return ; + } + + return ( + +
+
+ +
+ {/* */} +
+

+ {t("patient__general-info")} +

+
{t("general_info_detail")}
+
+ +
+ { + setForm((f) => ({ + ...f, + phone_number: e.target.value, + emergency_phone_number: samePhoneNumber + ? e.target.value + : f.emergency_phone_number, + })); + }} + required + label={t("phone_number")} + /> +
+ { + const newValue = !samePhoneNumber; + setSamePhoneNumber(newValue); + if (newValue) { + setForm((f) => ({ + ...f, + emergency_phone_number: f.phone_number, + })); + } + }} + id="same-phone-number" + label={t("use_phone_number_for_emergency")} + /> +
+
+ + {/*
+ */} +
+ + setForm((f) => ({ ...f, gender: Number(value) })) + } + errors={errors["gender"]} + className="flex items-center gap-4" + > + {GENDER_TYPES.map((g) => ( + <> + + + + ))} + +
+ +
+ + setAgeDob(value as typeof ageDob) + } + > + + {[ + ["dob", t("date_of_birth")], + ["age", t("age")], + ].map(([key, label]) => ( + {label} + ))} + + +
+ + setForm((f) => ({ + ...f, + date_of_birth: `${form.date_of_birth?.split("-")[0] || ""}-${form.date_of_birth?.split("-")[1] || ""}-${e.target.value}`, + })) + } + errors={errors["date_of_birth"] ? [""] : undefined} + /> + + setForm((f) => ({ + ...f, + date_of_birth: `${form.date_of_birth?.split("-")[0] || ""}-${e.target.value}-${form.date_of_birth?.split("-")[2] || ""}`, + })) + } + errors={errors["date_of_birth"] ? [""] : undefined} + /> + + setForm((f) => ({ + ...f, + date_of_birth: `${e.target.value}-${form.date_of_birth?.split("-")[1] || ""}-${form.date_of_birth?.split("-")[2] || ""}`, + })) + } + errors={errors["date_of_birth"] ? [""] : undefined} + /> +
+ {errors["date_of_birth"] && ( + + )} +
+ +
+ {t("age_input_warning")} +
+ {t("age_input_warning_bold")} +
+ setAge(Number(e.target.value))} + required + type="number" + label={t("age")} + /> +
+
+
+