From 35e07259417d41101b24b6f8049a1cae2c51403a Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Tue, 24 Dec 2024 19:12:48 +0530 Subject: [PATCH] New Patient Search and Register UI (#9400) --- package-lock.json | 4 +- package.json | 4 +- public/locale/en.json | 37 + src/CAREUI/interactive/FiltersSlideover.tsx | 15 +- src/CAREUI/misc/SectionNavigator.tsx | 79 + src/Routers/routes/PatientRoutes.tsx | 14 +- src/Utils/request/api.tsx | 3 +- src/Utils/utils.ts | 4 +- src/components/Assets/AssetTypes.tsx | 3 +- src/components/Common/Breadcrumbs.tsx | 2 +- src/components/Common/Export.tsx | 30 +- .../Common/SearchByMultipleFields.tsx | 22 +- src/components/Common/SortDropdown.tsx | 52 +- .../FacilitiesSelectDialogue.tsx | 21 +- .../ConsultationContext.tsx | 2 +- .../Facility/ConsultationDetails/index.tsx | 2 +- .../Facility/DischargedPatientsList.tsx | 2 +- .../Facility/DuplicatePatientDialog.tsx | 80 +- src/components/Facility/FacilityHome.tsx | 30 +- src/components/Facility/FacilityList.tsx | 3 +- .../Investigations/investigationsTab.tsx | 2 +- src/components/Facility/TreatmentSummary.tsx | 2 +- src/components/Facility/models.tsx | 2 +- src/components/HCX/models.ts | 2 +- src/components/Patient/ManagePatients.tsx | 281 +-- .../PatientDetailsTab/ImmunisationRecords.tsx | 2 +- .../PatientDetailsTab/ShiftingHistory.tsx | 2 +- .../Patient/PatientDetailsTab/index.tsx | 3 +- src/components/Patient/PatientFilter.tsx | 275 ++- src/components/Patient/PatientHome.tsx | 6 +- src/components/Patient/PatientIndex.tsx | 408 ++++ src/components/Patient/PatientInfoCard.tsx | 2 +- src/components/Patient/PatientRegister.tsx | 1765 ----------------- .../Patient/PatientRegistration.tsx | 1045 ++++++++++ src/components/Patient/Utils.ts | 20 +- src/components/Patient/models.tsx | 89 +- src/components/Shifting/ShiftDetails.tsx | 2 +- .../Shifting/ShiftDetailsUpdate.tsx | 2 +- src/components/ui/autocomplete.tsx | 39 +- src/components/ui/checkbox.tsx | 2 +- src/components/ui/errors.tsx | 11 + src/components/ui/input-with-error.tsx | 24 + src/components/ui/input.tsx | 2 +- src/components/ui/tabs.tsx | 24 +- src/components/ui/textarea.tsx | 2 +- src/hooks/useFilters.tsx | 2 +- src/pages/Facility/FacilitiesPage.tsx | 6 +- src/pluginTypes.ts | 7 +- src/types/emr/patient.ts | 140 ++ 49 files changed, 2386 insertions(+), 2192 deletions(-) create mode 100644 src/CAREUI/misc/SectionNavigator.tsx create mode 100644 src/components/Patient/PatientIndex.tsx delete mode 100644 src/components/Patient/PatientRegister.tsx create mode 100644 src/components/Patient/PatientRegistration.tsx create mode 100644 src/components/ui/errors.tsx create mode 100644 src/components/ui/input-with-error.tsx create mode 100644 src/types/emr/patient.ts diff --git a/package-lock.json b/package-lock.json index f5cff2d3376..08e14909e36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,13 +20,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.1", + "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", diff --git a/package.json b/package.json index 7100761eb13..44437fee67c 100644 --- a/package.json +++ b/package.json @@ -58,13 +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.1", + "@radix-ui/react-radio-group": "^1.2.2", "@radix-ui/react-scroll-area": "^1.2.2", "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", diff --git a/public/locale/en.json b/public/locale/en.json index 7288eff59b2..5b692e455ec 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -337,10 +337,13 @@ "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", "age_notice": "Only year of birth will be stored for age.", "all": "All", "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", @@ -635,6 +638,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", @@ -685,6 +689,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", "doc_will_visit_patient": "will visit the patient at the scheduled time.", "doctor_experience_error": "Please enter a valid number between 0 and 100.", @@ -737,8 +742,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", @@ -790,6 +797,9 @@ "expired": "Expired", "expired_on": "Expired On", "expires_on": "Expires On", + "export": "Export", + "export_live_patients": "Export Live Patients", + "exporting": "Exporting", "facilities": "Facilities", "facility": "Facility", "facility_consent_requests_page_title": "Patient Consent List", @@ -843,6 +853,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.", @@ -909,9 +920,11 @@ "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.", + "invalid_date_format": "Invalid date format, expected {{format}}", "invalid_email": "Please enter a valid email address", "invalid_ip_address": "Invalid IP Address", "invalid_link_msg": "It appears that the password reset link you have used is either invalid or expired. Please request a new password reset link.", @@ -962,6 +975,7 @@ "lab_tests": "Lab Tests", "label": "Label", "landline": "Indian landline", + "landmark": "Landmark", "language_selection": "Language Selection", "languages": "Languages", "last_administered": "Last administered", @@ -1058,6 +1072,7 @@ "modified_date": "Modified Date", "modified_on": "Modified On", "monitor": "Monitor", + "month": "Month", "more_details": "More details", "more_info": "More Info", "morning_slots": "Morning Slots", @@ -1093,6 +1108,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", @@ -1210,6 +1226,8 @@ "patient_phone_number": "Patient Phone Number", "patient_profile": "Patient Profile", "patient_profile_created_by": "Patient profile created by", + "patient_records_found": "Patient Records Found", + "patient_records_found_description": "It appears that there are patient records that contain the same phone number as the one you just entered. ", "patient_registration": "Patient Registration", "patient_registration__address": "Address", "patient_registration__age": "Age", @@ -1219,9 +1237,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 Successfully", "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 Sucessfully", "patients": "Patients", "pending": "Pending", "permanent_address": "Permanent Address", @@ -1234,7 +1256,9 @@ "phone_no": "Phone no.", "phone_number": "Phone Number", "phone_number_at_current_facility": "Phone Number of Contact person at current Facility", + "phone_number_min_error": "Phone number must be at least 10 characters long", "pincode": "Pincode", + "pincode_autofill": "State and District auto-filled from Pincode", "please_assign_bed_to_patient": "Please assign a bed to this patient", "please_check_your_messages": "Please check your messages", "please_confirm_password": "Please confirm your new password.", @@ -1242,6 +1266,7 @@ "please_enter_current_password": "Please enter your current password.", "please_enter_new_password": "Please enter your new password.", "please_enter_username": "Please enter the username", + "please_fix_errors": "Please fix the errors in the highlighted fields and try submitting again.", "please_select_a_facility": "Please select a facility", "please_select_breathlessness_level": "Please select Breathlessness Level", "please_select_district": "Please select the district", @@ -1289,6 +1314,7 @@ "preset_updated": "Preset updated", "prev_sessions": "Prev Sessions", "primary_ph_no": "Primary Ph No.", + "primary_phone_no": "Primary ph. no.", "principal": "Principal", "principal_diagnosis": "Principal diagnosis", "print": "Print", @@ -1405,6 +1431,7 @@ "scribe_error": "Could not autofill fields", "search": "Search", "search_by_emergency_contact_phone_number": "Search by Emergency Contact Phone Number", + "search_by_emergency_phone_number": "Search by Emergency Phone Number", "search_by_patient_name": "Search by Patient Name", "search_by_patient_no": "Search by Patient Number", "search_by_phone_number": "Search by Phone Number", @@ -1413,6 +1440,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", @@ -1431,6 +1459,7 @@ "select_policy_to_add_items": "Select a Policy to Add Items", "select_practitioner": "Select Practicioner", "select_register_patient": "Select/Register Patient", + "select_seven_day_period": "Select a seven day period", "select_skills": "Select and add some skills", "select_wards": "Select wards", "self_booked": "Self-booked", @@ -1475,6 +1504,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..!", @@ -1523,6 +1553,7 @@ "titrate_dosage": "Titrate Dosage", "to": "to", "to_be_conducted": "To be conducted", + "to_proceed_with_registration": "To proceed with registration, please create a new patient.", "to_view_available_slots_select_resource_and_date": "To view available slots, select a preferred resource and date.", "today": "Today", "token": "Token", @@ -1551,6 +1582,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", @@ -1610,11 +1642,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 Successfully", "user_details": "User Details", "user_details_update_error": "Error while updating user details", "user_details_update_success": "User details updated successfully", @@ -1674,6 +1709,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", "waitlist": "Waitlist", @@ -1687,6 +1723,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/interactive/FiltersSlideover.tsx b/src/CAREUI/interactive/FiltersSlideover.tsx index 959fd0621e8..6e4feeaf852 100644 --- a/src/CAREUI/interactive/FiltersSlideover.tsx +++ b/src/CAREUI/interactive/FiltersSlideover.tsx @@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; import SlideOver from "@/CAREUI/interactive/SlideOver"; +import { Button } from "@/components/ui/button"; + import ButtonV2 from "@/components/Common/ButtonV2"; import useFilters from "@/hooks/useFilters"; @@ -31,7 +33,7 @@ export default function FiltersSlideover({ setOpen={advancedFilter.setShow} title={
- {t("advanced_filters")} + {t("filters")}
void }) => { const { t } = useTranslation(); return ( - - {t("advanced_filters")} - + {t("filters")} + ); }; diff --git a/src/CAREUI/misc/SectionNavigator.tsx b/src/CAREUI/misc/SectionNavigator.tsx new file mode 100644 index 00000000000..a10ba799a2d --- /dev/null +++ b/src/CAREUI/misc/SectionNavigator.tsx @@ -0,0 +1,79 @@ +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..d8993032e35 100644 --- a/src/Routers/routes/PatientRoutes.tsx +++ b/src/Routers/routes/PatientRoutes.tsx @@ -2,22 +2,24 @@ 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/live": () => , + "/patients/discharged": () => , "/patient/:id": ({ id }) => , "/patient/:id/investigation_reports": ({ id }) => ( ), - "/facility/:facilityId/patient": ({ facilityId }) => ( - + "/facility/:facilityId/patient/create": ({ facilityId }) => ( + ), "/facility/:facilityId/patient/:id": ({ facilityId, id }) => ( @@ -33,7 +35,7 @@ const PatientRoutes: AppRoutes = { ), "/facility/:facilityId/patient/:id/update": ({ facilityId, id }) => ( - + ), "/facility/:facilityId/patient/:patientId/notes": ({ facilityId, diff --git a/src/Utils/request/api.tsx b/src/Utils/request/api.tsx index f50f9e99359..b1e3ecc00e3 100644 --- a/src/Utils/request/api.tsx +++ b/src/Utils/request/api.tsx @@ -62,7 +62,7 @@ import { NotificationData, PNconfigData, } from "@/components/Notifications/models"; -import { DailyRoundsModel, PatientModel } from "@/components/Patient/models"; +import { DailyRoundsModel } from "@/components/Patient/models"; import { CreateFileRequest, CreateFileResponse, @@ -89,6 +89,7 @@ import { } from "@/pages/Patient/Utils"; import { AllergyIntolerance } from "@/types/emr/allergyIntolerance"; import { Observation } from "@/types/emr/observation"; +import { PatientModel } from "@/types/emr/patient"; import { PlugConfig } from "@/types/plugConfig"; import { BatchRequestBody, diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 2868c2c7d91..1a62606d253 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -1,12 +1,11 @@ import { format } from "date-fns"; -import { PatientModel } from "@/components/Patient/models"; - import { AREACODES, IN_LANDLINE_AREA_CODES } from "@/common/constants"; import phoneCodesJson from "@/common/static/countryPhoneAndFlags.json"; import dayjs from "@/Utils/dayjs"; import { Time } from "@/Utils/types"; +import { PatientModel } from "@/types/emr/patient"; interface ApacheParams { age: number; @@ -240,6 +239,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("+")) { diff --git a/src/components/Assets/AssetTypes.tsx b/src/components/Assets/AssetTypes.tsx index 14d1e40afe0..257d2cd7799 100644 --- a/src/components/Assets/AssetTypes.tsx +++ b/src/components/Assets/AssetTypes.tsx @@ -1,9 +1,10 @@ import { IconName } from "@/CAREUI/icons/CareIcon"; import { BedModel } from "@/components/Facility/models"; -import { PatientModel } from "@/components/Patient/models"; import { UserBareMinimum } from "@/components/Users/models"; +import { PatientModel } from "@/types/emr/patient"; + export enum AssetLocationType { OTHER = "OTHER", WARD = "WARD", 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/Export.tsx b/src/components/Common/Export.tsx index 6cd786977ed..2998ce19945 100644 --- a/src/components/Common/Export.tsx +++ b/src/components/Common/Export.tsx @@ -1,6 +1,9 @@ +import { useTranslation } from "react-i18next"; + import CareIcon from "@/CAREUI/icons/CareIcon"; -import ButtonV2 from "@/components/Common/ButtonV2"; +import { Button } from "@/components/ui/button"; + import DropdownMenu, { DropdownItem, DropdownItemProps, @@ -44,12 +47,15 @@ export const ExportMenu = ({ exportItems, }: ExportMenuProps) => { const { isExporting, exportFile } = useExport(); + const { t } = useTranslation(); if (exportItems.length === 1) { const item = exportItems[0]; return ( - { let action = item.action; @@ -63,13 +69,10 @@ export const ExportMenu = ({ exportFile(action, item.filePrefix, item.type, item.parse); } }} - border - ghost - className="py-2.5" > - {isExporting ? "Exporting..." : label} - + {isExporting ? `${t("exporting")}...` : label} + ); } @@ -113,10 +116,12 @@ export const ExportButton = ({ ...props }: ExportButtonProps) => { const { isExporting, exportFile } = useExport(); + const { t } = useTranslation(); return ( <> - { let action = props.action; @@ -130,10 +135,7 @@ export const ExportButton = ({ exportFile(action, props.filenamePrefix, type, parse); } }} - className="tooltip mx-2 p-4 text-lg text-secondary-800 disabled:bg-transparent disabled:text-secondary-500" - variant="secondary" - ghost - circle + className="tooltip gap-2 text-lg text-secondary-800 disabled:bg-transparent disabled:text-secondary-500" > {isExporting ? ( @@ -141,9 +143,9 @@ export const ExportButton = ({ )} - {props.tooltip || "Export"} + {props.tooltip || t("export")} - + ); }; diff --git a/src/components/Common/SearchByMultipleFields.tsx b/src/components/Common/SearchByMultipleFields.tsx index 5302bc0838d..748bef41540 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; @@ -48,6 +47,7 @@ interface SearchByMultipleFieldsProps { buttonClassName?: string; clearSearch?: { value: boolean; params?: string[] }; enableOptionButtons?: boolean; + onFieldChange?: (options: SearchOption) => void; } type EventType = React.ChangeEvent | { value: string }; @@ -61,6 +61,7 @@ const SearchByMultipleFields: React.FC = ({ inputClassName, buttonClassName, clearSearch, + onFieldChange, enableOptionButtons = true, }) => { const { t } = useTranslation(); @@ -68,14 +69,18 @@ const SearchByMultipleFields: React.FC = ({ initialOptionIndex || 0, ); const selectedOption = options[selectedOptionIndex]; - const [searchValue, setSearchValue] = useState( - options[selectedOptionIndex].value || "", - ); + const [searchValue, setSearchValue] = useState(selectedOption.value || ""); const [open, setOpen] = useState(false); const inputRef = useRef(null); const [focusedIndex, setFocusedIndex] = useState(0); const [error, setError] = useState(); + useEffect(() => { + if (!(selectedOption.type === "phone" && searchValue.length < 13)) { + setSearchValue(options[selectedOptionIndex].value); + } + }, [options]); + useEffect(() => { if (clearSearch?.value) { const clearinput = options @@ -96,6 +101,7 @@ const SearchByMultipleFields: React.FC = ({ inputRef.current?.focus(); setError(false); onSearch(option.key, option.value); + onFieldChange?.(options[index]); }, [onSearch], ); @@ -182,7 +188,7 @@ const SearchByMultipleFields: React.FC = ({ = ({ ); @@ -243,7 +249,7 @@ const SearchByMultipleFields: React.FC = ({ {t(option.key)} - {option.label.charAt(0).toUpperCase()} + {option.shortcutKey} ))} @@ -252,7 +258,7 @@ const SearchByMultipleFields: React.FC = ({ - {renderSearchInput} +
{renderSearchInput}
{error && (
diff --git a/src/components/Common/SortDropdown.tsx b/src/components/Common/SortDropdown.tsx index 2e5ccd95015..d1d753a70a1 100644 --- a/src/components/Common/SortDropdown.tsx +++ b/src/components/Common/SortDropdown.tsx @@ -2,7 +2,13 @@ import { useTranslation } from "react-i18next"; import CareIcon from "@/CAREUI/icons/CareIcon"; -import DropdownMenu, { DropdownItem } from "@/components/Common/Menu"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; export interface SortOption { isAscending: boolean; @@ -22,31 +28,31 @@ interface Props { export default function SortDropdownMenu(props: Props) { const { t } = useTranslation(); return ( - } - containerClassName="w-full md:w-auto z-20" + ); } diff --git a/src/components/ExternalResult/FacilitiesSelectDialogue.tsx b/src/components/ExternalResult/FacilitiesSelectDialogue.tsx index 621fe834bc7..d925fedd9fa 100644 --- a/src/components/ExternalResult/FacilitiesSelectDialogue.tsx +++ b/src/components/ExternalResult/FacilitiesSelectDialogue.tsx @@ -1,6 +1,9 @@ import { useTranslation } from "react-i18next"; -import { Cancel, Submit } from "@/components/Common/ButtonV2"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + +import { Button } from "@/components/ui/button"; + import DialogModal from "@/components/Common/Dialog"; import { FacilitySelect } from "@/components/Common/FacilitySelect"; import { FacilityModel } from "@/components/Facility/models"; @@ -45,13 +48,19 @@ const FacilitiesSelectDialog = (props: Props) => { } />
- - + + {t("cancel")} + +
); diff --git a/src/components/Facility/ConsultationDetails/ConsultationContext.tsx b/src/components/Facility/ConsultationDetails/ConsultationContext.tsx index 174b20536a5..506468a723d 100644 --- a/src/components/Facility/ConsultationDetails/ConsultationContext.tsx +++ b/src/components/Facility/ConsultationDetails/ConsultationContext.tsx @@ -1,9 +1,9 @@ import { ReactNode, createContext, useContext, useState } from "react"; import { ConsultationModel } from "@/components/Facility/models"; -import { PatientModel } from "@/components/Patient/models"; import { PLUGIN_Component } from "@/PluginEngine"; +import { PatientModel } from "@/types/emr/patient"; interface ConsultationContextBase { consultation?: ConsultationModel; diff --git a/src/components/Facility/ConsultationDetails/index.tsx b/src/components/Facility/ConsultationDetails/index.tsx index 7a95e7da381..88ca3f10660 100644 --- a/src/components/Facility/ConsultationDetails/index.tsx +++ b/src/components/Facility/ConsultationDetails/index.tsx @@ -23,7 +23,6 @@ import DoctorVideoSlideover from "@/components/Facility/DoctorVideoSlideover"; import PatientNotesSlideover from "@/components/Facility/PatientNotesSlideover"; import { ConsultationModel } from "@/components/Facility/models"; import PatientInfoCard from "@/components/Patient/PatientInfoCard"; -import { PatientModel } from "@/components/Patient/models"; import useAuthUser from "@/hooks/useAuthUser"; import { useCareAppConsultationTabs } from "@/hooks/useCareApps"; @@ -41,6 +40,7 @@ import { keysOf, relativeTime, } from "@/Utils/utils"; +import { PatientModel } from "@/types/emr/patient"; import { ConsultationProvider } from "./ConsultationContext"; diff --git a/src/components/Facility/DischargedPatientsList.tsx b/src/components/Facility/DischargedPatientsList.tsx index 4fb8910f7a1..c17367cc058 100644 --- a/src/components/Facility/DischargedPatientsList.tsx +++ b/src/components/Facility/DischargedPatientsList.tsx @@ -24,7 +24,6 @@ import { FILTER_BY_DIAGNOSES_KEYS, } from "@/components/Patient/DiagnosesFilter"; import PatientFilter from "@/components/Patient/PatientFilter"; -import { PatientModel } from "@/components/Patient/models"; import useFilters from "@/hooks/useFilters"; @@ -41,6 +40,7 @@ import { parseOptionId } from "@/common/utils"; import routes from "@/Utils/request/api"; import useTanStackQueryInstead from "@/Utils/request/useQuery"; import { formatPatientAge, humanizeStrings } from "@/Utils/utils"; +import { PatientModel } from "@/types/emr/patient"; const DischargedPatientsList = ({ facility_external_id, diff --git a/src/components/Facility/DuplicatePatientDialog.tsx b/src/components/Facility/DuplicatePatientDialog.tsx index 241750cec6b..111571c20ad 100644 --- a/src/components/Facility/DuplicatePatientDialog.tsx +++ b/src/components/Facility/DuplicatePatientDialog.tsx @@ -1,18 +1,27 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { Cancel, Submit } from "@/components/Common/ButtonV2"; +import CareIcon from "@/CAREUI/icons/CareIcon"; + import DialogModal from "@/components/Common/Dialog"; import { DupPatientModel } from "@/components/Facility/models"; +import { Button } from "../ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "../ui/table"; + interface Props { patientList: Array; handleOk: (action: string) => void; handleCancel: () => void; } -const tdClass = "border border-secondary-400 p-2 text-left"; - const DuplicatePatientDialog = (props: Props) => { const { t } = useTranslation(); const { patientList, handleOk, handleCancel } = props; @@ -20,7 +29,7 @@ const DuplicatePatientDialog = (props: Props) => { return ( {

- It appears that there are patient records that contain the same - phone number as the one you just entered. ( + {t("patient_records_found_description")}( {patientList[0].phone_number})

-
- - - - {["Patient Name and ID", "Gender"].map((heading, i) => ( - - ))} - - - +
+
- {heading} -
+ + + {[`${t("patient_name")} / ID`, t("gender")].map( + (heading, i) => ( + {heading} + ), + )} + + + {patientList.map((patient, i) => { return ( - - - - + + {patient.gender} + ); })} - -
+ +
{patient.name}
ID : {patient.patient_id}
-
{patient.gender}
+ +
@@ -107,17 +112,18 @@ const DuplicatePatientDialog = (props: Props) => {
- - + + {t("close")} + +
); diff --git a/src/components/Facility/FacilityHome.tsx b/src/components/Facility/FacilityHome.tsx index 30b9344cca6..50e2dc2ae63 100644 --- a/src/components/Facility/FacilityHome.tsx +++ b/src/components/Facility/FacilityHome.tsx @@ -43,7 +43,29 @@ import useTanStackQueryInstead from "@/Utils/request/useQuery"; import { getAuthorizationHeader } from "@/Utils/request/utils"; import { sleep } from "@/Utils/utils"; -import { patientRegisterAuth } from "../Patient/PatientRegister"; +import { UserModel } from "../Users/models"; +import { FacilityModel } from "./models"; + +export function canUserRegisterPatient( + authUser: UserModel, + facilityObject: FacilityModel | undefined, + facilityId: string, +) { + // User types that can register a new patient + const privilegedUserTypes = ["DistrictAdmin", "StateAdmin"]; + + return ( + // Allow non privileged users of the same facility + (!privilegedUserTypes.includes(authUser.user_type) && + authUser.home_facility_object?.id === facilityId) || + // allow district admins + (authUser.user_type === "DistrictAdmin" && + authUser.district === facilityObject?.district) || + // allow state admins + (authUser.user_type === "StateAdmin" && + authUser.state === facilityObject?.state) + ); +} type Props = { facilityId: string; @@ -464,13 +486,15 @@ export const FacilityHome = ({ facilityId }: Props) => { {CameraFeedPermittedUserTypes.includes(authUser.user_type) && ( )} - {patientRegisterAuth(authUser, facilityData, facilityId) && ( + {canUserRegisterPatient(authUser, facilityData, facilityId) && ( navigate(`/facility/${facilityId}/patient`)} + onClick={() => + navigate(`/facility/${facilityId}/patient/create`) + } authorizeFor={NonReadOnlyUsers} > diff --git a/src/components/Facility/FacilityList.tsx b/src/components/Facility/FacilityList.tsx index f62f630dcaa..1f1378b2636 100644 --- a/src/components/Facility/FacilityList.tsx +++ b/src/components/Facility/FacilityList.tsx @@ -184,9 +184,8 @@ export const FacilityList = () => { options={[ { key: "facility_district_name", - label: "Facility or District Name", type: "text" as const, - placeholder: "facility_search_placeholder", + placeholder: t("facility_search_placeholder"), value: qParams.search || "", shortcutKey: "f", }, diff --git a/src/components/Facility/Investigations/investigationsTab.tsx b/src/components/Facility/Investigations/investigationsTab.tsx index 1bc4c62011d..c06d9f19314 100644 --- a/src/components/Facility/Investigations/investigationsTab.tsx +++ b/src/components/Facility/Investigations/investigationsTab.tsx @@ -1,9 +1,9 @@ import ViewInvestigationSuggestions from "@/components/Facility/Investigations/InvestigationSuggestions"; import ViewInvestigations from "@/components/Facility/Investigations/ViewInvestigations"; -import { PatientModel } from "@/components/Patient/models"; import routes from "@/Utils/request/api"; import useTanStackQueryInstead from "@/Utils/request/useQuery"; +import { PatientModel } from "@/types/emr/patient"; export interface InvestigationSessionType { session_external_id: string; diff --git a/src/components/Facility/TreatmentSummary.tsx b/src/components/Facility/TreatmentSummary.tsx index b3c91d801eb..3120c5c099f 100644 --- a/src/components/Facility/TreatmentSummary.tsx +++ b/src/components/Facility/TreatmentSummary.tsx @@ -11,13 +11,13 @@ import { } from "@/components/Diagnosis/types"; import { ConsultationModel } from "@/components/Facility/models"; import MedicineRoutes from "@/components/Medicine/routes"; -import { PatientModel } from "@/components/Patient/models"; import { GENDER_TYPES } from "@/common/constants"; import routes from "@/Utils/request/api"; import useTanStackQueryInstead from "@/Utils/request/useQuery"; import { formatDate, formatDateTime, formatPatientAge } from "@/Utils/utils"; +import { PatientModel } from "@/types/emr/patient"; export interface ITreatmentSummaryProps { consultationId: string; diff --git a/src/components/Facility/models.tsx b/src/components/Facility/models.tsx index 437c9985957..8488317ca18 100644 --- a/src/components/Facility/models.tsx +++ b/src/components/Facility/models.tsx @@ -12,7 +12,6 @@ import { DailyRoundsModel, FacilityNameModel, FileUploadModel, - PatientModel, } from "@/components/Patient/models"; import { EncounterSymptom } from "@/components/Symptoms/types"; import { UserBareMinimum } from "@/components/Users/models"; @@ -28,6 +27,7 @@ import { } from "@/common/constants"; import { FeatureFlag } from "@/Utils/featureFlags"; +import { PatientModel } from "@/types/emr/patient"; export interface LocalBodyModel { id: number; diff --git a/src/components/HCX/models.ts b/src/components/HCX/models.ts index 416b3ec6abe..359ee8b886f 100644 --- a/src/components/HCX/models.ts +++ b/src/components/HCX/models.ts @@ -1,4 +1,4 @@ -import { PatientModel } from "@/components/Patient/models"; +import { PatientModel } from "@/types/emr/patient"; export type HCXPolicyPriority = "Immediate" | "Normal" | "Deferred"; export type HCXPolicyStatus = diff --git a/src/components/Patient/ManagePatients.tsx b/src/components/Patient/ManagePatients.tsx index 4862d4fc3e2..83018e1406a 100644 --- a/src/components/Patient/ManagePatients.tsx +++ b/src/components/Patient/ManagePatients.tsx @@ -4,13 +4,10 @@ import { ReactNode, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; 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"; import useAuthUser from "@/hooks/useAuthUser"; import useFilters from "@/hooks/useFilters"; @@ -50,6 +47,7 @@ import { getDiagnosesByIds } from "../Diagnosis/utils"; import FacilitiesSelectDialogue from "../ExternalResult/FacilitiesSelectDialogue"; import DoctorVideoSlideover from "../Facility/DoctorVideoSlideover"; import { FacilityModel, PatientCategory } from "../Facility/models"; +import { Button } from "../ui/button"; import { DIAGNOSES_FILTER_LABELS, DiagnosesFilterKey, @@ -343,13 +341,6 @@ export const PatientManager = () => { }, ); - const { data: permittedFacilities } = useTanStackQueryInstead( - routes.getPermittedFacilities, - { - query: { limit: 1 }, - }, - ); - const LastAdmittedToTypeBadges = () => { const badge = (key: string, value: string | undefined, id: string) => { return ( @@ -741,15 +732,12 @@ export const PatientManager = () => { ); } - const onlyAccessibleFacility = - permittedFacilities?.count === 1 ? permittedFacilities.results[0] : null; - const searchOptions = [ { key: "name", label: "Name", type: "text" as const, - placeholder: "search_by_patient_name", + placeholder: t("search_by_patient_name"), value: qParams.name || "", shortcutKey: "n", }, @@ -757,7 +745,7 @@ export const PatientManager = () => { key: "patient_no", label: "IP/OP No", type: "text" as const, - placeholder: "search_by_patient_no", + placeholder: t("search_by_patient_no"), value: qParams.patient_no || "", shortcutKey: "u", }, @@ -765,15 +753,15 @@ export const PatientManager = () => { key: "phone_number", label: "Phone Number", type: "phone" as const, - placeholder: "Search_by_phone_number", + placeholder: t("search_by_phone_number"), value: qParams.phone_number || "", shortcutKey: "p", }, { - key: "emergency_contact_number", + key: "emergency_phone_number", label: "Emergency Contact Phone Number", type: "phone" as const, - placeholder: "search_by_emergency_phone_number", + placeholder: t("search_by_emergency_phone_number"), value: qParams.emergency_phone_number || "", shortcutKey: "e", }, @@ -781,22 +769,24 @@ export const PatientManager = () => { 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, - }; + const updatedQuery: Record = {}; + + switch (key) { + case "phone_number": + case "emergency_contact_number": + if (value.length >= 13 || value === "") { + updatedQuery[key] = value; + } else { + updatedQuery[key] = ""; + } + break; + case "name": + case "patient_no": + updatedQuery[key] = value; + break; + default: + break; + } updateQuery(updatedQuery); }, @@ -804,169 +794,86 @@ export const PatientManager = () => { ); return ( - -
- +
+
+ {!!params.facility && ( +
-
- { - 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; - } + +

Doctor Connect

+ + )} - Notification.Warn({ - msg: t("select_facility_for_discharged_patients_warning"), - }); - setShowDialog("list-discharged"); - } - }} - currentTab={tabValue} - /> - {!!params.facility && ( - advancedFilter.setShow(true)} /> + +
+ {!isExportAllowed ? ( + + ) : ( + { + const query = { + ...params, + csv: true, + facility: qParams.facility, + is_active: "True", + }; + 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 && ( + + {t("select_seven_day_period")} + + )}
- } - > +
+ setSelectedFacility(e)} @@ -1009,7 +916,7 @@ export const PatientManager = () => { className="w-full" />
-
+
{ setShow={setShowDoctors} />
- + ); }; diff --git a/src/components/Patient/PatientDetailsTab/ImmunisationRecords.tsx b/src/components/Patient/PatientDetailsTab/ImmunisationRecords.tsx index b16059abe46..943f7fc456d 100644 --- a/src/components/Patient/PatientDetailsTab/ImmunisationRecords.tsx +++ b/src/components/Patient/PatientDetailsTab/ImmunisationRecords.tsx @@ -6,7 +6,6 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import { Button } from "@/components/ui/button"; import { PatientProps } from "@/components/Patient/PatientDetailsTab"; -import { PatientModel } from "@/components/Patient/models"; import { UserModel } from "@/components/Users/models"; import useAuthUser from "@/hooks/useAuthUser"; @@ -15,6 +14,7 @@ import { ADMIN_USER_TYPES } from "@/common/constants"; import * as Notification from "@/Utils/Notifications"; import { formatDateTime } from "@/Utils/utils"; +import { PatientModel } from "@/types/emr/patient"; export const ImmunisationRecords = (props: PatientProps) => { const { patientData, facilityId, id } = props; diff --git a/src/components/Patient/PatientDetailsTab/ShiftingHistory.tsx b/src/components/Patient/PatientDetailsTab/ShiftingHistory.tsx index 6bd1bb5bbb7..71083a948f9 100644 --- a/src/components/Patient/PatientDetailsTab/ShiftingHistory.tsx +++ b/src/components/Patient/PatientDetailsTab/ShiftingHistory.tsx @@ -5,7 +5,6 @@ import CareIcon from "@/CAREUI/icons/CareIcon"; import ButtonV2 from "@/components/Common/ButtonV2"; import { PatientProps } from "@/components/Patient/PatientDetailsTab"; -import { PatientModel } from "@/components/Patient/models"; import { formatFilter } from "@/components/Resource/ResourceCommons"; import ShiftingTable from "@/components/Shifting/ShiftingTable"; @@ -14,6 +13,7 @@ import useFilters from "@/hooks/useFilters"; import { NonReadOnlyUsers } from "@/Utils/AuthorizeFor"; import routes from "@/Utils/request/api"; import useTanStackQueryInstead from "@/Utils/request/useQuery"; +import { PatientModel } from "@/types/emr/patient"; const ShiftingHistory = (props: PatientProps) => { const { patientData, facilityId, id } = props; diff --git a/src/components/Patient/PatientDetailsTab/index.tsx b/src/components/Patient/PatientDetailsTab/index.tsx index 0439c1529a4..59fcbb7c2df 100644 --- a/src/components/Patient/PatientDetailsTab/index.tsx +++ b/src/components/Patient/PatientDetailsTab/index.tsx @@ -5,7 +5,8 @@ import PatientNotes from "@/components/Patient/PatientDetailsTab//Notes"; import ShiftingHistory from "@/components/Patient/PatientDetailsTab//ShiftingHistory"; import { Demography } from "@/components/Patient/PatientDetailsTab/Demography"; import { Updates } from "@/components/Patient/PatientDetailsTab/patientUpdates"; -import { PatientModel } from "@/components/Patient/models"; + +import { PatientModel } from "@/types/emr/patient"; import { Appointments } from "./Appointments"; import { ResourceRequests } from "./ResourceRequests"; 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/PatientHome.tsx b/src/components/Patient/PatientHome.tsx index 7595cb5126b..8675f224bd3 100644 --- a/src/components/Patient/PatientHome.tsx +++ b/src/components/Patient/PatientHome.tsx @@ -15,10 +15,7 @@ import Page from "@/components/Common/Page"; import UserAutocomplete from "@/components/Common/UserAutocompleteFormField"; import { patientTabs } from "@/components/Patient/PatientDetailsTab"; import { isPatientMandatoryDataFilled } from "@/components/Patient/Utils"; -import { - AssignedToObjectModel, - PatientModel, -} from "@/components/Patient/models"; +import { AssignedToObjectModel } from "@/components/Patient/models"; import { SkillModel, UserBareMinimum } from "@/components/Users/models"; import useAuthUser from "@/hooks/useAuthUser"; @@ -45,6 +42,7 @@ import { isPostPartum, relativeDate, } from "@/Utils/utils"; +import { PatientModel } from "@/types/emr/patient"; export const parseOccupation = (occupation: string | undefined) => { return OCCUPATION_TYPES.find((i) => i.value === occupation)?.text; diff --git a/src/components/Patient/PatientIndex.tsx b/src/components/Patient/PatientIndex.tsx new file mode 100644 index 00000000000..f6d7ad0fc6d --- /dev/null +++ b/src/components/Patient/PatientIndex.tsx @@ -0,0 +1,408 @@ +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 { Button } from "@/components/ui/button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table"; +import { SectionTabs } from "@/components/ui/tabs"; + +import Loading from "@/components/Common/Loading"; +import Page from "@/components/Common/Page"; +import SearchByMultipleFields from "@/components/Common/SearchByMultipleFields"; +import FacilitiesSelectDialogue from "@/components/ExternalResult/FacilitiesSelectDialogue"; +import { FacilityModel } from "@/components/Facility/models"; +import { PatientManager } from "@/components/Patient/ManagePatients"; +import PatientFilter, { + PatientFilterBadges, +} from "@/components/Patient/PatientFilter"; +import { getPatientUrl } from "@/components/Patient/Utils"; + +import useAuthUser from "@/hooks/useAuthUser"; +import useFilters from "@/hooks/useFilters"; + +import { GENDER_TYPES } from "@/common/constants"; + +import * as Notification from "@/Utils/Notifications"; +import routes from "@/Utils/request/api"; +import useQuery from "@/Utils/request/useQuery"; +import { formatPatientAge, parsePhoneNumber } from "@/Utils/utils"; + +export default function PatientIndex(props: { + tab?: "live" | "discharged" | "search"; +}) { + const { t } = useTranslation(); + const { tab = "search" } = props; + 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: t("search_by_patient_name"), + value: qParams.name || "", + shortcutKey: "n", + }, + { + key: "patient_no", + type: "text" as const, + placeholder: t("search_by_patient_no"), + value: qParams.patient_no || "", + shortcutKey: "u", + }, + { + key: "phone_number", + type: "phone" as const, + placeholder: t("search_by_phone_number"), + value: qParams.phone_number || "", + shortcutKey: "p", + }, + + { + key: "emergency_phone_number", + type: "phone" as const, + placeholder: t("search_by_emergency_phone_number"), + value: qParams.emergency_phone_number || "", + shortcutKey: "e", + }, + ]; + + const authUser = useAuthUser(); + + const handleSearch = useCallback( + (key: string, value: string) => { + const updatedQuery: Record = {}; + + switch (key) { + case "phone_number": + case "emergency_phone_number": + if (value.length >= 13 || value === "") { + updatedQuery[key] = value; + } else { + updatedQuery[key] = ""; + } + break; + case "name": + case "patient_no": + updatedQuery[key] = value; + break; + default: + break; + } + + 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}/patient/create`); + }; + + function AddPatientButton(props: { outline?: boolean }) { + useKeyboardShortcut(["Shift", "P"], handleAddPatient); + return ( + + ); + } + + return ( + } + > + { + if (value === "discharged") { + // for a user that has access to just one facility, or if the user is filtering by one facility, take them to the dedicated facility discharge page + const id = qParams.facility || onlyAccessibleFacility?.id; + if (id) { + navigate(`facility/${id}/discharged-patients`); + return; + } + + // only state admins can view all discharged patients + if ( + authUser.user_type === "StateAdmin" || + authUser.user_type === "StateReadOnlyAdmin" + ) { + navigate("/patients/discharged?is_active=false"); + return; + } + + // for other users, ask what facility they would like to view discharged patients of + setShowDialog("list-discharged"); + } else if (value === "search") { + navigate("/patients"); + } else if (value === "live") { + navigate("/patients/live"); + } + }} + tabs={[ + { + label: t("search_patients"), + value: "search", + }, + { + label: t("all_patients"), + value: "live", + }, + { + label: t("discharged_patients"), + value: "discharged", + }, + ]} + /> + {tab === "search" ? ( +
+
+
+
+ +
+ +
+ { + updateQuery({ + name: "", + patient_no: "", + phone_number: "", + emergency_phone_number: "", + }); + }} + onSearch={handleSearch} + clearSearch={clearSearch} + className="w-full" + /> +
+ {isValidSearch && + (!listingQuery.loading && !listingQuery.data?.results.length ? ( +
+ {t("no_records_found")} +
+ {t("to_proceed_with_registration")} + +
+ ) : listingQuery.loading ? ( + + ) : ( + !!listingQuery.data?.results.length && ( + + + + {t("name")}/IP/OP + + {t("primary_phone_no")} + + + {t("dob")}/{t("age")} + + {t("sex")} + + + + {listingQuery.data?.results.map((patient) => ( + 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 && ( + + )} +
+ ) : ( + + )} + + setSelectedFacility(e)} + selectedFacility={selectedFacility} + handleOk={() => { + switch (showDialog) { + case "create": + navigate(`/facility/${selectedFacility.id}/patient/create`); + break; + case "list-discharged": + navigate(`/facility/${selectedFacility.id}/discharged-patients`); + break; + } + }} + handleCancel={() => { + setShowDialog(undefined); + setSelectedFacility({ name: "" }); + }} + /> +
+ ); +} diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index ed02abaecf0..3504a5b13ef 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -17,7 +17,6 @@ import { ConsultationModel, PatientCategory, } from "@/components/Facility/models"; -import { PatientModel } from "@/components/Patient/models"; import { SkillModel } from "@/components/Users/models"; import useAuthUser from "@/hooks/useAuthUser"; @@ -45,6 +44,7 @@ import { formatPatientAge, humanizeStrings, } from "@/Utils/utils"; +import { PatientModel } from "@/types/emr/patient"; const formatSkills = (arr: SkillModel[]) => { const skills = arr.map((skill) => skill.skill_object.name); diff --git a/src/components/Patient/PatientRegister.tsx b/src/components/Patient/PatientRegister.tsx deleted file mode 100644 index 15780de28ff..00000000000 --- a/src/components/Patient/PatientRegister.tsx +++ /dev/null @@ -1,1765 +0,0 @@ -import careConfig from "@careConfig"; -import { navigate } from "raviger"; -import { useCallback, useEffect, useReducer, useState } from "react"; -import { useTranslation } from "react-i18next"; - -import CareIcon from "@/CAREUI/icons/CareIcon"; - -import AccordionV2 from "@/components/Common/AccordionV2"; -import ButtonV2 from "@/components/Common/ButtonV2"; -import CollapseV2 from "@/components/Common/CollapseV2"; -import ConfirmDialog from "@/components/Common/ConfirmDialog"; -import DialogModal from "@/components/Common/Dialog"; -import Loading from "@/components/Common/Loading"; -import PageTitle from "@/components/Common/PageTitle"; -import Spinner from "@/components/Common/Spinner"; -import DuplicatePatientDialog from "@/components/Facility/DuplicatePatientDialog"; -import TransferPatientDialog from "@/components/Facility/TransferPatientDialog"; -import { - DistrictModel, - DupPatientModel, - FacilityModel, - LocalBodyModel, - WardModel, -} from "@/components/Facility/models"; -import { - FieldError, - PhoneNumberValidator, - RequiredFieldValidator, -} from "@/components/Form/FieldValidators"; -import Form from "@/components/Form/Form"; -import AutocompleteFormField from "@/components/Form/FormFields/Autocomplete"; -import CheckBoxFormField from "@/components/Form/FormFields/CheckBoxFormField"; -import DateFormField from "@/components/Form/FormFields/DateFormField"; -import { - FieldErrorText, - FieldLabel, -} from "@/components/Form/FormFields/FormField"; -import PhoneNumberFormField from "@/components/Form/FormFields/PhoneNumberFormField"; -import RadioFormField from "@/components/Form/FormFields/RadioFormField"; -import { SelectFormField } from "@/components/Form/FormFields/SelectFormField"; -import TextAreaFormField from "@/components/Form/FormFields/TextAreaFormField"; -import TextFormField from "@/components/Form/FormFields/TextFormField"; -import SelectMenuV2 from "@/components/Form/SelectMenuV2"; -import InsuranceDetailsBuilder from "@/components/HCX/InsuranceDetailsBuilder"; -import { HCXPolicyModel } from "@/components/HCX/models"; -import HCXPolicyValidator from "@/components/HCX/validators"; -import { - Occupation, - PatientMeta, - PatientModel, -} from "@/components/Patient/models"; -import { UserModel } from "@/components/Users/models"; - -import useAppHistory from "@/hooks/useAppHistory"; -import useAuthUser from "@/hooks/useAuthUser"; -import useDebounce from "@/hooks/useDebounce"; - -import { - BLOOD_GROUPS, - DOMESTIC_HEALTHCARE_SUPPORT_CHOICES, - GENDER_TYPES, - MEDICAL_HISTORY_CHOICES, - OCCUPATION_TYPES, - RATION_CARD_CATEGORY, - SOCIOECONOMIC_STATUS_CHOICES, - VACCINES, -} from "@/common/constants"; -import countryList from "@/common/static/countries.json"; -import { statusType, useAbortableEffect } from "@/common/utils"; -import { validateName, validatePincode } from "@/common/validation"; - -import { PLUGIN_Component } from "@/PluginEngine"; -import { RestoreDraftButton } from "@/Utils/AutoSave"; -import * as Notification from "@/Utils/Notifications"; -import { usePubSub } from "@/Utils/pubsubContext"; -import routes from "@/Utils/request/api"; -import request from "@/Utils/request/request"; -import useTanStackQueryInstead from "@/Utils/request/useQuery"; -import { - compareBy, - dateQueryString, - getPincodeDetails, - includesIgnoreCase, - parsePhoneNumber, - scrollTo, -} from "@/Utils/utils"; - -import ErrorPage from "../ErrorPages/DefaultErrorPage"; - -export type PatientForm = PatientModel & - PatientMeta & { age?: number; is_postpartum?: boolean }; - -interface PatientRegisterProps extends PatientModel { - facilityId: string; -} - -interface medicalHistoryModel { - id?: number; - disease: string | number; - details: string; -} - -const medicalHistoryChoices = MEDICAL_HISTORY_CHOICES.reduce( - (acc: Array<{ [x: string]: string }>, cur) => [ - ...acc, - { [`medical_history_${cur.id}`]: "" }, - ], - [], -); -const genderTypes = GENDER_TYPES; -const bloodGroups = [...BLOOD_GROUPS]; -const occupationTypes = OCCUPATION_TYPES; -const vaccines = [...VACCINES]; - -const initForm: any = { - name: "", - age: "", - year_of_birth: "", - gender: "", - phone_number: "+91", - emergency_phone_number: "+91", - blood_group: "", - is_declared_positive: "false", - date_declared_positive: new Date(), - date_of_birth: null, - medical_history: [], - nationality: "India", - passport_no: "", - state: "", - district: "", - local_body: "", - ward: "", - address: "", - permanent_address: "", - sameAddress: true, - village: "", - allergies: "", - pincode: "", - present_health: "", - date_of_return: null, - is_antenatal: "false", - date_of_test: null, - treatment_plan: false, - ongoing_medication: "", - designation_of_health_care_worker: "", - instituion_of_health_care_worker: "", - covin_id: "", - is_vaccinated: "false", - number_of_doses: "0", - vaccine_name: null, - last_vaccinated_date: null, - ...medicalHistoryChoices, - ration_card_category: null, -}; - -const initError = Object.assign( - {}, - ...Object.keys(initForm).map((k) => ({ [k]: "" })), -); - -const initialState = { - form: { ...initForm }, - errors: { ...initError }, -}; - -const patientFormReducer = (state = initialState, action: any) => { - switch (action.type) { - case "set_form": { - return { - ...state, - form: action.form, - }; - } - case "set_error": { - return { - ...state, - errors: action.errors, - }; - } - default: - return state; - } -}; -export const parseOccupationFromExt = (occupation: Occupation) => { - const occupationObject = OCCUPATION_TYPES.find( - (item) => item.value === occupation, - ); - return occupationObject?.id; -}; - -export const PatientRegister = (props: PatientRegisterProps) => { - const authUser = useAuthUser(); - const { t } = useTranslation(); - const { goBack } = useAppHistory(); - const { facilityId, id } = props; - const [state, dispatch] = useReducer(patientFormReducer, initialState); - const [showAlertMessage, setAlertMessage] = useState({ - show: false, - message: "", - title: "", - }); - const [isLoading, setIsLoading] = useState(false); - const [formField, setFormField] = useState(); - const [resetNum, setResetNum] = useState(false); - const [isDistrictLoading, setIsDistrictLoading] = useState(false); - const [isLocalbodyLoading, setIsLocalbodyLoading] = useState(false); - const [isWardLoading, setIsWardLoading] = useState(false); - const [districts, setDistricts] = useState([]); - const [localBody, setLocalBody] = useState([]); - const [ward, setWard] = useState([]); - const [ageInputType, setAgeInputType] = useState< - "date_of_birth" | "age" | "alert_for_age" - >("date_of_birth"); - const [statusDialog, setStatusDialog] = useState<{ - show?: boolean; - transfer?: boolean; - patientList: Array; - }>({ patientList: [] }); - const [patientName, setPatientName] = useState(""); - const [showAutoFilledPincode, setShowAutoFilledPincode] = useState(false); - const [insuranceDetails, setInsuranceDetails] = useState( - [], - ); - const [isEmergencyNumberEnabled, setIsEmergencyNumberEnabled] = - useState(false); - const [insuranceDetailsError, setInsuranceDetailsError] = - useState(); - - const { publish } = usePubSub(); - - const headerText = !id ? "Add Details of Patient" : "Update Patient Details"; - const buttonText = !id ? "Add Patient" : "Save Details"; - - useEffect(() => { - const getQueryParams = () => { - const params = new URLSearchParams(window.location.search); - return { - section: params.get("section"), - }; - }; - - const { section } = getQueryParams(); - if (section) { - setTimeout(() => { - const element = document.getElementById(section); - if (element) { - element.scrollIntoView({ behavior: "smooth" }); - } - }, 2000); - } - }, []); - - const fetchDistricts = useCallback(async (id: number) => { - if (id > 0) { - setIsDistrictLoading(true); - const { res, data } = await request(routes.getDistrictByState, { - pathParams: { id }, - }); - if (res?.ok && data) { - setDistricts(data); - } - setIsDistrictLoading(false); - return data ? [...data] : []; - } - }, []); - - const fetchLocalBody = useCallback(async (id: string) => { - if (Number(id) > 0) { - setIsLocalbodyLoading(true); - const { data } = await request(routes.getLocalbodyByDistrict, { - pathParams: { id }, - }); - setIsLocalbodyLoading(false); - setLocalBody(data || []); - } else { - setLocalBody([]); - } - }, []); - - const fetchWards = useCallback(async (id: string) => { - if (Number(id) > 0) { - setIsWardLoading(true); - const { data } = await request(routes.getWardByLocalBody, { - pathParams: { id }, - }); - setIsWardLoading(false); - if (data) { - setWard(data.results); - } - } else { - setWard([]); - } - }, []); - - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const { res, data } = await request(routes.getPatient, { - pathParams: { id: id ? id : 0 }, - }); - - if (!status.aborted) { - if (res?.ok && data) { - setPatientName(data.name || ""); - if (!data.date_of_birth) { - setAgeInputType("age"); - } - const formData = { - ...data, - age: data.year_of_birth - ? new Date().getFullYear() - data.year_of_birth - : "", - nationality: data.nationality ? data.nationality : "India", - gender: data.gender ? data.gender : undefined, - state: data.state ? data.state : "", - district: data.district ? data.district : "", - blood_group: data.blood_group - ? data.blood_group === "UNKNOWN" - ? "UNK" - : data.blood_group - : "", - local_body: data.local_body ? data.local_body : "", - ward: data.ward_object ? data.ward_object.id : undefined, - village: data.village ? data.village : "", - medical_history: [] as number[], - is_antenatal: String(!!data.is_antenatal), - last_menstruation_start_date: data.last_menstruation_start_date, - date_of_delivery: data.date_of_delivery, - is_postpartum: String(!!data.date_of_delivery), - allergies: data.allergies ? data.allergies : "", - pincode: data.pincode ? data.pincode : "", - ongoing_medication: data.ongoing_medication - ? data.ongoing_medication - : "", - - is_declared_positive: data.is_declared_positive - ? String(data.is_declared_positive) - : "false", - designation_of_health_care_worker: - data.designation_of_health_care_worker - ? data.designation_of_health_care_worker - : "", - instituion_of_health_care_worker: - data.instituion_of_health_care_worker - ? data.instituion_of_health_care_worker - : "", - meta_info: data.meta_info ?? {}, - occupation: data.meta_info?.occupation - ? parseOccupationFromExt(data.meta_info.occupation) - : null, - - is_vaccinated: String(data.is_vaccinated), - number_of_doses: data.number_of_doses - ? String(data.number_of_doses) - : "0", - vaccine_name: data.vaccine_name ? data.vaccine_name : null, - last_vaccinated_date: data.last_vaccinated_date - ? data.last_vaccinated_date - : null, - }; - formData.sameAddress = data.address === data.permanent_address; - setIsEmergencyNumberEnabled( - data.phone_number === data.emergency_phone_number, - ); - (data.medical_history ? data.medical_history : []).forEach( - (i: any) => { - const medicalHistory = MEDICAL_HISTORY_CHOICES.find( - (j) => - String(j.text).toLowerCase() === - String(i.disease).toLowerCase(), - ); - if (medicalHistory) { - formData.medical_history.push(Number(medicalHistory.id)); - (formData as any)[`medical_history_${medicalHistory.id}`] = - i.details; - } - }, - ); - dispatch({ - type: "set_form", - form: formData, - }); - Promise.all([ - fetchDistricts(data.state ?? 0), - fetchLocalBody(data.district ? String(data.district) : ""), - fetchWards(data.local_body ? String(data.local_body) : ""), // Convert data.local_body to string - ]); - } else { - goBack(); - } - setIsLoading(false); - } - }, - [id], - ); - - useTanStackQueryInstead(routes.hcx.policies.list, { - query: { - patient: id, - }, - prefetch: !!id, - onResponse: ({ data }) => { - if (data) { - setInsuranceDetails(data.results); - } else { - setInsuranceDetails([]); - } - }, - }); - - const { data: stateData, loading: isStateLoading } = useTanStackQueryInstead( - routes.statesList, - ); - - useAbortableEffect( - (status: statusType) => { - if (id) { - fetchData(status); - } - }, - [dispatch, fetchData], - ); - - const { data: facilityObject } = useTanStackQueryInstead( - routes.getAnyFacility, - { - pathParams: { id: facilityId }, - prefetch: !!facilityId, - }, - ); - - const validateForm = (form: any) => { - const errors: Partial> = {}; - - const insuranceDetailsError = insuranceDetails - .map((policy) => HCXPolicyValidator(policy, careConfig.hcx.enabled)) - .find((error) => !!error); - setInsuranceDetailsError(insuranceDetailsError); - - errors["insurance_details"] = insuranceDetailsError; - - Object.keys(form).forEach((field) => { - let phoneNumber, emergency_phone_number; - switch (field) { - case "name": { - const requiredError = RequiredFieldValidator()(form[field]); - if (requiredError) { - errors[field] = requiredError; - } else if (!validateName(form[field])) { - errors[field] = t("min_char_length_error", { min_length: 3 }); - } - return; - } - case "address": - case "gender": - errors[field] = RequiredFieldValidator()(form[field]); - return; - case "last_menstruation_start_date": - if (form.is_antenatal === "true") { - errors[field] = RequiredFieldValidator()(form[field]); - } - return; - case "date_of_delivery": - if (form.is_postpartum === "true") { - errors[field] = RequiredFieldValidator()(form[field]); - } - return; - case "age": - case "date_of_birth": { - const field = ageInputType === "age" ? "age" : "date_of_birth"; - - errors[field] = RequiredFieldValidator()(form[field]); - if (errors[field]) { - return; - } - - if (field === "age") { - if (form.age < 0) { - errors.age = "Age cannot be less than 0"; - return; - } - - form.date_of_birth = null; - form.year_of_birth = new Date().getFullYear() - form.age; - } - - if (field === "date_of_birth") { - form.age = null; - form.year_of_birth = null; - } - - return; - } - case "permanent_address": - if (!form.sameAddress) { - errors[field] = RequiredFieldValidator()(form[field]); - } - return; - case "local_body": - if (form.nationality === "India" && !Number(form[field])) { - errors[field] = "Please select a localbody"; - } - return; - case "district": - if (form.nationality === "India" && !Number(form[field])) { - errors[field] = "Please select district"; - } - return; - case "state": - if (form.nationality === "India" && !Number(form[field])) { - errors[field] = "Please enter the state"; - } - return; - case "pincode": - if (!validatePincode(form[field])) { - errors[field] = "Please enter valid pincode"; - } - return; - case "passport_no": - if (form.nationality !== "India" && !form[field]) { - errors[field] = "Please enter the passport number"; - } - return; - case "phone_number": - phoneNumber = parsePhoneNumber(form[field]); - if ( - !form[field] || - !phoneNumber || - !PhoneNumberValidator()(phoneNumber) === undefined - ) { - errors[field] = "Please enter valid phone number"; - } - return; - case "emergency_phone_number": - emergency_phone_number = parsePhoneNumber(form[field]); - if ( - !form[field] || - !emergency_phone_number || - !PhoneNumberValidator()(emergency_phone_number) === undefined - ) { - errors[field] = "Please enter valid phone number"; - } - return; - case "blood_group": - if (!form[field]) { - errors[field] = "Please select a blood group"; - } - return; - case "is_vaccinated": - if (form.is_vaccinated === "true") { - if (form.number_of_doses === "0") { - errors["number_of_doses"] = - "Please fill the number of doses taken"; - } - if (form.vaccine_name === null || form.vaccine_name === "Select") { - errors["vaccine_name"] = "Please select vaccine name"; - } - - if (!form.last_vaccinated_date) { - errors["last_vaccinated_date"] = - "Please enter last vaccinated date"; - } - } - return; - case "medical_history": - if (!form[field].length) { - errors[field] = "Please fill the medical history"; - } - return; - default: - return; - } - }); - - const firstError = Object.keys(errors).find((e) => errors[e]); - if (firstError) { - scrollTo(firstError); - } - - return errors; - }; - - const handlePincodeChange = async (e: any, setField: any) => { - if (!validatePincode(e.value)) return; - - const pincodeDetails = await getPincodeDetails( - e.value, - careConfig.govDataApiKey, - ); - if (!pincodeDetails) return; - - const matchedState = stateData?.results?.find((state) => { - return includesIgnoreCase(state.name, pincodeDetails.statename); - }); - if (!matchedState) return; - - const fetchedDistricts = await fetchDistricts(matchedState.id); - if (!fetchedDistricts) return; - - const matchedDistrict = fetchedDistricts.find((district) => { - return includesIgnoreCase(district.name, pincodeDetails.districtname); - }); - if (!matchedDistrict) return; - - setField({ name: "state", value: matchedState.id }); - setField({ name: "district", value: matchedDistrict.id.toString() }); // Convert matchedDistrict.id to string - - fetchLocalBody(matchedDistrict.id.toString()); // Convert matchedDistrict.id to string - setShowAutoFilledPincode(true); - setTimeout(() => { - setShowAutoFilledPincode(false); - }, 2000); - }; - - const handleSubmit = async (formData: any) => { - setIsLoading(true); - const medical_history: Array = []; - formData.medical_history.forEach((id: number) => { - const medData = MEDICAL_HISTORY_CHOICES.find((i) => i.id === id); - if (medData) { - const details = formData[`medical_history_${medData.id}`]; - medical_history.push({ - disease: medData.text, - details: details ? details : "", - }); - } - }); - const data = { - phone_number: parsePhoneNumber(formData.phone_number), - emergency_phone_number: parsePhoneNumber(formData.emergency_phone_number), - date_of_birth: - ageInputType === "date_of_birth" - ? dateQueryString(formData.date_of_birth) - : null, - year_of_birth: ageInputType === "age" ? formData.year_of_birth : null, - date_of_test: formData.date_of_test ? formData.date_of_test : undefined, - date_declared_positive: - JSON.parse(formData.is_declared_positive) && - formData.date_declared_positive - ? formData.date_declared_positive - : null, - covin_id: - formData.is_vaccinated === "true" ? formData.covin_id : undefined, - is_vaccinated: formData.is_vaccinated, - number_of_doses: - formData.is_vaccinated === "true" - ? Number(formData.number_of_doses) - : Number("0"), - vaccine_name: - formData.vaccine_name && - formData.vaccine_name !== "Select" && - formData.is_vaccinated === "true" - ? formData.vaccine_name - : null, - last_vaccinated_date: - formData.is_vaccinated === "true" - ? formData.last_vaccinated_date - ? formData.last_vaccinated_date - : null - : null, - name: formData.name, - pincode: formData.pincode ? formData.pincode : undefined, - gender: Number(formData.gender), - nationality: formData.nationality, - is_antenatal: formData.is_antenatal, - last_menstruation_start_date: - formData.is_antenatal === "true" - ? dateQueryString(formData.last_menstruation_start_date) - : null, - date_of_delivery: - formData.is_postpartum === "true" - ? dateQueryString(formData.date_of_delivery) - : null, - passport_no: - formData.nationality !== "India" ? formData.passport_no : undefined, - state: formData.nationality === "India" ? formData.state : undefined, - district: - formData.nationality === "India" ? formData.district : undefined, - local_body: - formData.nationality === "India" ? formData.local_body : undefined, - ward: formData.ward, - meta_info: { - ...formData.meta_info, - occupation: formData.occupation ?? null, - }, - village: formData.village, - address: formData.address ? formData.address : undefined, - permanent_address: formData.sameAddress - ? formData.address - : formData.permanent_address - ? formData.permanent_address - : undefined, - present_health: formData.present_health - ? formData.present_health - : undefined, - allergies: formData.allergies, - ongoing_medication: formData.ongoing_medication, - is_declared_positive: JSON.parse(formData.is_declared_positive), - designation_of_health_care_worker: - formData.designation_of_health_care_worker, - instituion_of_health_care_worker: - formData.instituion_of_health_care_worker, - blood_group: formData.blood_group ? formData.blood_group : undefined, - medical_history, - is_active: true, - ration_card_category: formData.ration_card_category, - }; - const { res, data: requestData } = id - ? await request(routes.updatePatient, { - pathParams: { id }, - body: data, - }) - : await request(routes.addPatient, { - body: { ...data, facility: facilityId }, - }); - if (res?.ok && requestData) { - publish("patient:upsert", requestData); - - await Promise.all( - insuranceDetails.map(async (obj) => { - const policy = { - ...obj, - patient: requestData.id, - insurer_id: obj.insurer_id || undefined, - insurer_name: obj.insurer_name || undefined, - }; - policy.id - ? await request(routes.hcx.policies.update, { - pathParams: { external_id: policy.id }, - body: policy, - }) - : await request(routes.hcx.policies.create, { - body: policy, - }); - }), - ); - - dispatch({ type: "set_form", form: initForm }); - if (!id) { - setAlertMessage({ - show: true, - message: `Please note down patient name: ${formData.name} and patient ID: ${requestData.id}`, - title: "Patient Added Successfully", - }); - navigate( - `/facility/${facilityId}/patient/${requestData.id}/consultation`, - ); - } else { - Notification.Success({ - msg: "Patient updated successfully", - }); - goBack(); - } - } - setIsLoading(false); - }; - - const handleMedicalCheckboxChange = (e: any, id: number, field: any) => { - const values = field("medical_history").value ?? []; - if (e.value) { - values.push(id); - } else { - values.splice(values.indexOf(id), 1); - } - - if (id !== 1 && values.includes(1)) { - values.splice(values.indexOf(1), 1); - } else if (id === 1) { - values.length = 0; - values.push(1); - } - - field("medical_history").onChange({ - name: "medical_history", - value: values, - }); - }; - - const duplicateCheck = useDebounce(async (phoneNo: string) => { - if ( - phoneNo && - PhoneNumberValidator()(parsePhoneNumber(phoneNo) ?? "") === undefined - ) { - const query = { - phone_number: parsePhoneNumber(phoneNo), - }; - const { res, data } = await request(routes.searchPatient, { - query, - }); - if (res?.ok && data?.results) { - const duplicateList = !id - ? data.results - : data.results.filter( - (item: DupPatientModel) => item.patient_id !== id, - ); - if (duplicateList.length) { - setStatusDialog({ - show: true, - patientList: duplicateList, - }); - } - } - } else { - setStatusDialog({ - show: false, - patientList: [], - }); - } - }, 300); - - const handleDialogClose = (action: string) => { - if (action === "transfer") { - setStatusDialog({ ...statusDialog, show: false, transfer: true }); - } else if (action === "back") { - setStatusDialog({ ...statusDialog, show: true, transfer: false }); - } else { - setStatusDialog({ show: false, transfer: false, patientList: [] }); - } - }; - - const renderMedicalHistory = (id: number, title: string, field: any) => { - const checkboxField = `medical_history_check_${id}`; - const textField = `medical_history_${id}`; - return ( -
-
- handleMedicalCheckboxChange(e, id, field)} - name={checkboxField} - label={id !== 1 ? title : "NONE"} - /> -
- {id !== 1 && (field("medical_history").value ?? []).includes(id) && ( -
- -
- )} -
- ); - }; - - if (isLoading) { - return ; - } - - if ( - !isLoading && - facilityId && - facilityObject && - !patientRegisterAuth(authUser, facilityObject, facilityId) - ) { - return ; - } - - return ( - - defaults={id ? state.form : initForm} - validate={validateForm} - onSubmit={handleSubmit} - submitLabel={buttonText} - onCancel={() => goBack()} - className="bg-transparent px-1 py-2 md:px-2" - onDraftRestore={(newState) => { - dispatch({ type: "set_state", state: newState }); - Promise.all([ - fetchDistricts(newState.form.state ?? 0), - fetchLocalBody(newState.form.district?.toString() ?? ""), - fetchWards(newState.form.local_body?.toString() ?? ""), - duplicateCheck(newState.form.phone_number ?? ""), - ]); - }} - noPadding - hideRestoreDraft - > - {(field) => { - if (!formField) setFormField(field); - if (resetNum) { - field("phone_number").onChange({ - name: "phone_number", - value: "+91", - }); - setResetNum(false); - } - return ( -
- {statusDialog.show && ( - { - handleDialogClose("close"); - setResetNum(true); - }} - /> - )} - {statusDialog.transfer && ( - { - setResetNum(true); - handleDialogClose("close"); - }} - title="Patient Transfer Form" - className="max-w-md md:min-w-[600px]" - > - handleDialogClose("close")} - handleCancel={() => { - setResetNum(true); - handleDialogClose("close"); - }} - facilityId={facilityId} - /> - - )} - { - id - ? navigate(`/facility/${facilityId}/patient/${id}`) - : navigate(`/facility/${facilityId}`); - }} - componentRight={} - crumbsReplacements={{ - [facilityId]: { name: facilityObject?.name }, - [id ?? "????"]: { name: patientName }, - }} - /> -
-
-
- {" "} - Please enter the correct date of birth for the patient -
-

- Each patient in the system is uniquely identifiable by the - number and date of birth. Adding incorrect date of birth can - result in duplication of patient records. -

-
- {showAlertMessage.show && ( - goBack()} - onClose={() => goBack()} - variant="primary" - action="Ok" - show - /> - )} - -
-

- Personal Details -

-
-
- { - if (!id) duplicateCheck(event.value); - field("phone_number").onChange(event); - if (isEmergencyNumberEnabled) { - field("emergency_phone_number").onChange({ - name: field("emergency_phone_number").name, - value: event.value, - }); - } - }} - types={["mobile", "landline"]} - /> - { - setIsEmergencyNumberEnabled(value); - value - ? field("emergency_phone_number").onChange({ - name: field("emergency_phone_number").name, - value: field("phone_number").value, - }) - : field("emergency_phone_number").onChange({ - name: field("emergency_phone_number").name, - value: initForm.emergency_phone_number, - }); - }} - /> -
-
- -
-
- -
-
- - {ageInputType === "age" ? "Age" : "Date of Birth"} - -
- o.text} - optionValue={(o) => - o.value === "date_of_birth" ? "date_of_birth" : "age" - } - value={ageInputType} - onChange={(v) => { - if (v === "age" && ageInputType === "date_of_birth") { - setAgeInputType("alert_for_age"); - return; - } - setAgeInputType(v); - }} - /> -
- {ageInputType !== "age" ? ( -
- -
- ) : ( -
- - {field("age").value !== "" && ( - <> - - Year of Birth: - - - YOB: - - - {new Date().getFullYear() - - field("age").value} - - - )} -

- } - placeholder="Enter the age" - type="number" - min={0} - /> -
- )} -
-
- -
- -
- While entering a patient's age is an option, - please note that only the year of birth will be - captured from this information. -
- - Recommended only when the patient's date of birth - is unknown - -
- } - action="Confirm" - variant="warning" - show={ageInputType == "alert_for_age"} - onClose={() => setAgeInputType("date_of_birth")} - onConfirm={() => setAgeInputType("age")} - /> -
-
-
- { - field("gender").onChange(e); - if (e.value !== "2") { - field("is_antenatal").onChange({ - name: "is_antenatal", - value: "false", - }); - - field("is_postpartum").onChange({ - name: "is_postpartum", - value: "false", - }); - } - }} - optionLabel={(o: any) => o.text} - optionValue={(o: any) => o.id} - /> -
- - { -
- option.label} - optionValue={(option) => option.value} - /> -
- } -
- - { -
- -
- } -
- - option.label} - optionValue={(option) => option.value} - /> - - - - -
- -
-
- - -
- -
- { - field("pincode").onChange(e); - handlePincodeChange(e, field("pincode").onChange); - }} - /> - {showAutoFilledPincode && ( -
- - - State and District auto-filled from Pincode - -
- )} -
-
- -
-
- o} - optionValue={(o) => o} - /> -
- {field("nationality").value === "India" ? ( - <> -
- {isStateLoading ? ( - - ) : ( - o.name} - optionValue={(o: any) => o.id} - onChange={(e: any) => { - field("state").onChange(e); - field("district").onChange({ - name: "district", - value: undefined, - }); - field("local_body").onChange({ - name: "local_body", - value: undefined, - }); - field("ward").onChange({ - name: "ward", - value: undefined, - }); - fetchDistricts(e.value); - fetchLocalBody("0"); - fetchWards("0"); - }} - /> - )} -
- -
- {isDistrictLoading ? ( -
- -
- ) : ( - o.name} - optionValue={(o: any) => o.id} - onChange={(e: any) => { - field("district").onChange(e); - field("local_body").onChange({ - name: "local_body", - value: undefined, - }); - field("ward").onChange({ - name: "ward", - value: undefined, - }); - fetchLocalBody(String(e.value)); - fetchWards("0"); - }} - /> - )} -
- -
- {isLocalbodyLoading ? ( -
- -
- ) : ( - o.name} - optionValue={(o) => o.id} - onChange={(e) => { - field("local_body").onChange(e); - field("ward").onChange({ - name: "ward", - value: undefined, - }); - fetchWards(String(e.value)); - }} - /> - )} -
-
- {isWardLoading ? ( -
- -
- ) : ( - { - return { - id: e.id, - name: e.number + ": " + e.name, - }; - })} - placeholder={ - field("local_body").value - ? "Choose Ward" - : "Select Localbody First" - } - disabled={!field("local_body").value} - optionLabel={(o: any) => o.name} - optionValue={(o: any) => o.id} - onChange={(e: any) => { - field("ward").onChange(e); - }} - /> - )} -
- - ) : ( -
- -
- )} -
-
- {field("nationality").value === "India" && ( -
- - } - title={ -

- Social Profile -

- } - expanded - > -
-
- o.text} - optionValue={(o) => o.id} - /> - t(`ration_card__${o}`)} - optionValue={(o) => o} - /> - t(`SOCIOECONOMIC_STATUS__${o}`)} - optionValue={(o) => o} - value={field("meta_info").value?.socioeconomic_status} - onChange={({ name, value }) => - field("meta_info").onChange({ - name: "meta_info", - value: { - ...(field("meta_info").value ?? {}), - [name]: value, - }, - }) - } - /> - - t(`DOMESTIC_HEALTHCARE_SUPPORT__${o}`) - } - optionValue={(o) => o} - value={ - field("meta_info").value - ?.domestic_healthcare_support - } - onChange={({ name, value }) => - field("meta_info").onChange({ - name: "meta_info", - value: { - ...(field("meta_info").value ?? {}), - [name]: value, - }, - }) - } - /> -
-
-
-
- )} -
- - } - title={ -

- COVID Details -

- } - > -
-
-
- option.label} - optionValue={(option) => option.value} - /> -
-
-
- - { -
-
- -
-
- option.label} - optionValue={(option) => option.value} - /> -
-
- o} - optionValue={(o) => o} - /> -
-
- -
-
- } -
-
-
-
- option.label} - optionValue={(option) => option.value} - /> - -
- -
-
-
-
- -
-
-
-
-
-
-

- Medical History -

-
-
- -
- -
- -
-
- - Any medical history? (Comorbidities) - -
- {MEDICAL_HISTORY_CHOICES.map((i) => { - return renderMedicalHistory( - i.id as number, - i.text, - field, - ); - })} -
- -
- -
- -
- -
- o} - optionValue={(o: any) => o} - /> -
-
-
-
-
-

- Insurance Details -

- - setInsuranceDetails([ - ...insuranceDetails, - { - id: "", - subscriber_id: "", - policy_id: "", - insurer_id: "", - insurer_name: "", - }, - ]) - } - data-testid="add-insurance-button" - > - - Add Insurance Details - -
- setInsuranceDetails(value)} - error={insuranceDetailsError} - gridView - /> -
-
-
- ); - }} - - ); -}; - -export function patientRegisterAuth( - authUser: UserModel, - facilityObject: FacilityModel | undefined, - facilityId: string, -) { - const showAllFacilityUsers = ["DistrictAdmin", "StateAdmin"]; - if ( - !showAllFacilityUsers.includes(authUser.user_type) && - authUser.home_facility_object?.id === facilityId - ) { - return true; - } - if ( - authUser.user_type === "DistrictAdmin" && - authUser.district === facilityObject?.district - ) { - return true; - } - if ( - authUser.user_type === "StateAdmin" && - authUser.state === facilityObject?.state - ) { - return true; - } - - return false; -} diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx new file mode 100644 index 00000000000..697b48f32b4 --- /dev/null +++ b/src/components/Patient/PatientRegistration.tsx @@ -0,0 +1,1045 @@ +import careConfig from "@careConfig"; +import { useMutation, useQuery } from "@tanstack/react-query"; +import { navigate } from "raviger"; +import { Fragment, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import CareIcon from "@/CAREUI/icons/CareIcon"; +import SectionNavigator from "@/CAREUI/misc/SectionNavigator"; + +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { InputErrors } from "@/components/ui/errors"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { Textarea } from "@/components/ui/textarea"; + +import DialogModal from "@/components/Common/Dialog"; +import Loading from "@/components/Common/Loading"; +import Page from "@/components/Common/Page"; +import DuplicatePatientDialog from "@/components/Facility/DuplicatePatientDialog"; +import TransferPatientDialog from "@/components/Facility/TransferPatientDialog"; + +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 mutate from "@/Utils/request/mutate"; +import query from "@/Utils/request/query"; +import { + dateQueryString, + getPincodeDetails, + includesIgnoreCase, + parsePhoneNumber, +} from "@/Utils/utils"; +import { PatientModel, validatePatient } from "@/types/emr/patient"; + +import Autocomplete from "../ui/autocomplete"; +import InputWithError from "../ui/input-with-error"; + +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(true); + 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 [feErrors, setFeErrors] = useState< + Partial> + >({}); + const [suppressDuplicateWarning, setSuppressDuplicateWarning] = + useState(!!patientId); + const [showTransferDialog, setShowTransferDialog] = useState(false); + const [debouncedNumber, setDebouncedNumber] = useState(); + + 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", + "ration_card_category", + ]; + + 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" ? form.year_of_birth : 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({ + mutationFn: mutate(routes.addPatient), + onSuccess: (resp: PatientModel) => { + Notification.Success({ + msg: t("patient_registration_success"), + }); + navigate(`/facility/${facilityId}/patient/${resp.id}/consultation`); + }, + onError: () => { + Notification.Error({ + msg: t("patient_registration_error"), + }); + }, + }); + + const updatePatientMutation = useMutation({ + mutationFn: mutate(routes.updatePatient, { + pathParams: { id: patientId || "" }, + }), + onSuccess: () => { + Notification.Success({ + msg: t("patient_update_success"), + }); + goBack(); + }, + onError: () => { + Notification.Error({ + msg: t("patient_update_error"), + }); + }, + }); + + 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 && !patientQuery.data.date_of_birth) { + 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 as unknown as string[]), + }; + + 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, + })), + }); + + 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 validate = validatePatient(form, ageDob === "dob"); + if (typeof validate !== "object") { + patientId + ? updatePatientMutation.mutate({ ...mutationData, ward_old: undefined }) + : createPatientMutation.mutate({ + ...mutationData, + facility: facilityId, + ward_old: undefined, + }); + } else { + const firstErrorField = document.querySelector("[data-input-error]"); + if (firstErrorField) { + firstErrorField.scrollIntoView({ behavior: "smooth", block: "center" }); + } + Notification.Error({ + msg: t("please_fix_errors"), + }); + setFeErrors(validate); + } + }; + + 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 ( + +
+
+ +
+ {/* + // This will need to be updated + */} +
+

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

+
{t("general_info_detail")}
+
+ + + +
+ + { + if (e.target.value.length > 13) return; + setForm((f) => ({ + ...f, + phone_number: e.target.value, + emergency_phone_number: samePhoneNumber + ? e.target.value + : f.emergency_phone_number, + })); + }} + /> + +
+ + { + const newValue = !samePhoneNumber; + setSamePhoneNumber(newValue); + if (newValue) { + setForm((f) => ({ + ...f, + emergency_phone_number: f.phone_number, + })); + } + }} + id="same-phone-number" + /> + + +
+
+ + { + if (e.target.value.length > 13) return; + setForm((f) => ({ + ...f, + emergency_phone_number: e.target.value, + })); + }} + disabled={samePhoneNumber} + /> + + {/*
+ */} +
+ + + setForm((f) => ({ ...f, gender: Number(value) })) + } + 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}`, + })) + } + /> + +
+
+ + + setForm((f) => ({ + ...f, + date_of_birth: `${form.date_of_birth?.split("-")[0] || ""}-${e.target.value}-${form.date_of_birth?.split("-")[2] || ""}`, + })) + } + /> + +
+
+ + + setForm((f) => ({ + ...f, + date_of_birth: `${e.target.value}-${form.date_of_birth?.split("-")[1] || ""}-${form.date_of_birth?.split("-")[2] || ""}`, + })) + } + /> + +
+
+ {errors["date_of_birth"] && ( + + )} +
+ +
+ {t("age_input_warning")} +
+ {t("age_input_warning_bold")} +
+
+ + + setForm((f) => ({ + ...f, + year_of_birth: e.target.value + ? new Date().getFullYear() - Number(e.target.value) + : undefined, + })) + } + type="number" + /> + + {form.year_of_birth && ( +
+ {t("year_of_birth")} : {form.year_of_birth} +
+ )} +
+
+
+
+ +