diff --git a/public/locale/en.json b/public/locale/en.json index 53586b56e86..f4c77636522 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1302,6 +1302,7 @@ "organization_forbidden": "You don't have access to any organizations yet.", "organization_not_found": "No Organizations Found", "organizations": "Organizations", + "organizations_fetch_error": "Error while fetching organizations", "origin_facility": "Current facility", "other_details": "Other details", "otp_verification_error": "Failed to verify OTP. Please try again later.", @@ -1392,6 +1393,8 @@ "phone_number_verified": "Phone Number Verified", "pincode": "Pincode", "pincode_autofill": "State and District auto-filled from Pincode", + "pincode_district_auto_fill_error": "Failed to auto-fill district information", + "pincode_state_auto_fill_error": "Failed to auto-fill state and district information", "play": "Play", "play_audio": "Play Audio", "please_assign_bed_to_patient": "Please assign a bed to this patient", diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx index 1dc236f5fb3..1fcc0f15475 100644 --- a/src/components/Patient/PatientRegistration.tsx +++ b/src/components/Patient/PatientRegistration.tsx @@ -1,9 +1,9 @@ -import careConfig from "@careConfig"; import { useMutation, useQuery } from "@tanstack/react-query"; import { navigate, useQueryParams } 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"; @@ -27,6 +27,7 @@ import Page from "@/components/Common/Page"; import DuplicatePatientDialog from "@/components/Facility/DuplicatePatientDialog"; import useAppHistory from "@/hooks/useAppHistory"; +import { useStateAndDistrictFromPincode } from "@/hooks/useStateAndDistrictFromPincode"; import { BLOOD_GROUP_CHOICES, // DOMESTIC_HEALTHCARE_SUPPORT_CHOICES, @@ -34,19 +35,15 @@ import { //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, - parsePhoneNumber, -} from "@/Utils/utils"; +import { dateQueryString, parsePhoneNumber } from "@/Utils/utils"; import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector"; import { PatientModel, validatePatient } from "@/types/emr/patient"; +import { Organization } from "@/types/organization/organization"; import Autocomplete from "../ui/autocomplete"; import InputWithError from "../ui/input-with-error"; @@ -79,6 +76,7 @@ export default function PatientRegistration( const [suppressDuplicateWarning, setSuppressDuplicateWarning] = useState(!!patientId); const [debouncedNumber, setDebouncedNumber] = useState(); + const [selectedLevels, setSelectedLevels] = useState([]); const sidebarItems = [ { label: t("patient__general-info"), id: "general-info" }, @@ -188,32 +186,24 @@ export default function PatientRegistration( } }, [patientQuery.data]); - 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 { statename: _stateName, districtname: _districtName } = - pincodeDetails; - - setShowAutoFilledPincode(true); - setTimeout(() => { - setShowAutoFilledPincode(false); - }, 2000); - }; + const { stateOrg, districtOrg } = useStateAndDistrictFromPincode({ + pincode: form.pincode?.toString() || "", + }); useEffect(() => { - const timeout = setTimeout( - () => handlePincodeChange(form.pincode?.toString() || ""), - 1000, - ); - return () => clearTimeout(timeout); - }, [form.pincode]); + const levels: Organization[] = []; + if (stateOrg) levels.push(stateOrg); + if (districtOrg) levels.push(districtOrg); + setSelectedLevels(levels); + + if (levels.length == 2) { + setShowAutoFilledPincode(true); + const timer = setTimeout(() => { + setShowAutoFilledPincode(false); + }, 2000); + return () => clearTimeout(timer); + } + }, [stateOrg, districtOrg]); const title = !patientId ? t("add_details_of_patient") @@ -587,6 +577,7 @@ export default function PatientRegistration( setForm((f) => ({ ...f, permanent_address: e.target.value })) } disabled={sameAddress} + className={sameAddress ? "cursor-not-allowed" : ""} /> {/*
@@ -602,7 +593,7 @@ export default function PatientRegistration( > - {/* {showAutoFilledPincode && ( + {_showAutoFilledPincode && (
- )} */} + )}
@@ -637,12 +628,14 @@ export default function PatientRegistration( <> setForm((f) => ({ ...f, geo_organization: value, })) } + errorMessage={errors.geo_organization?.[0]} /> )} diff --git a/src/hooks/useOrganization.ts b/src/hooks/useOrganization.ts new file mode 100644 index 00000000000..1d1a267e0d9 --- /dev/null +++ b/src/hooks/useOrganization.ts @@ -0,0 +1,44 @@ +import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import query from "@/Utils/request/query"; +import { Organization } from "@/types/organization/organization"; +import organizationApi from "@/types/organization/organizationApi"; + +interface UseOrganizationParams { + orgType?: string; + parentId?: string; + name?: string; + enabled?: boolean; +} + +export function useOrganization({ + orgType = "", + parentId = "", + name = "", + enabled = true, +}: UseOrganizationParams) { + const { data, isLoading, isError } = useQuery<{ results: Organization[] }>({ + queryKey: ["organization", orgType, name, parentId], + queryFn: query(organizationApi.list, { + queryParams: { + org_type: orgType, + parent: parentId, + name, + }, + }), + enabled: enabled && !!name, + select: (res) => ({ results: res.results }), + }); + + const { t } = useTranslation(); + + isError && toast.error(t("organizations_fetch_error")); + + return { + organizations: data?.results || [], + isLoading, + isError, + }; +} diff --git a/src/hooks/useStateAndDistrictFromPincode.ts b/src/hooks/useStateAndDistrictFromPincode.ts new file mode 100644 index 00000000000..11d3ac2109f --- /dev/null +++ b/src/hooks/useStateAndDistrictFromPincode.ts @@ -0,0 +1,72 @@ +import careConfig from "@careConfig"; +import { useQuery } from "@tanstack/react-query"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; + +import { validatePincode } from "@/common/validation"; + +import { getPincodeDetails } from "@/Utils/utils"; + +import { useOrganization } from "./useOrganization"; + +interface UseStateAndDistrictProps { + pincode: string; +} + +export function useStateAndDistrictFromPincode({ + pincode, +}: UseStateAndDistrictProps) { + const { + data: pincodeDetails, + isLoading: isPincodeLoading, + isError: isPincodeError, + } = useQuery({ + queryKey: ["pincode-details", pincode], + queryFn: () => getPincodeDetails(pincode, careConfig.govDataApiKey), + enabled: validatePincode(pincode), + }); + + const { t } = useTranslation(); + + const stateName = pincodeDetails?.statename || ""; + const districtName = pincodeDetails?.districtname || ""; + + const { + organizations: stateOrgs, + isLoading: isStateLoading, + isError: isStateError, + } = useOrganization({ + orgType: "govt", + parentId: "", + name: stateName, + enabled: !!stateName, + }); + + const stateOrg = stateOrgs?.[0]; + + const { + organizations: districtOrgs, + isLoading: isDistrictLoading, + isError: isDistrictError, + } = useOrganization({ + orgType: "govt", + parentId: stateOrg?.id, + name: districtName, + enabled: !!stateOrg?.id && !!districtName, + }); + + isStateError && toast.info(t("pincode_state_auto_fill_error")); + + isDistrictError && + !isStateError && + toast.info(t("pincode_district_auto_fill_error")); + + const districtOrg = districtOrgs[0]; + + return { + stateOrg, + districtOrg, + isLoading: isPincodeLoading || isStateLoading || isDistrictLoading, + isError: isPincodeError || isStateError || isDistrictError, + }; +} diff --git a/src/pages/Organization/components/OrganizationSelector.tsx b/src/pages/Organization/components/OrganizationSelector.tsx index a5880f30872..daf0ec7064c 100644 --- a/src/pages/Organization/components/OrganizationSelector.tsx +++ b/src/pages/Organization/components/OrganizationSelector.tsx @@ -1,6 +1,6 @@ import { useQuery } from "@tanstack/react-query"; import { t } from "i18next"; -import { useState } from "react"; +import { useEffect, useState } from "react"; import CareIcon from "@/CAREUI/icons/CareIcon"; @@ -19,6 +19,8 @@ interface OrganizationSelectorProps { onChange: (value: string) => void; required?: boolean; authToken?: string; + parentSelectedLevels?: Organization[]; + errorMessage?: string; } interface AutoCompleteOption { @@ -28,8 +30,10 @@ interface AutoCompleteOption { // TODO: Rename to GovtOrganizationSelector export default function OrganizationSelector(props: OrganizationSelectorProps) { - const { onChange, required } = props; - const [selectedLevels, setSelectedLevels] = useState([]); + const { onChange, required, parentSelectedLevels } = props; + const [selectedLevels, setSelectedLevels] = useState( + parentSelectedLevels || [], + ); const [searchQuery, setSearchQuery] = useDebouncedState("", 500); const headers = props.authToken @@ -101,9 +105,27 @@ export default function OrganizationSelector(props: OrganizationSelectorProps) { }; const handleEdit = (level: number) => { - setSelectedLevels((prev) => prev.slice(0, level)); + const newLevels = selectedLevels.slice(0, level); + setSelectedLevels(newLevels); + + if (!newLevels.length) { + onChange(""); + } else { + const lastOrg = newLevels[newLevels.length - 1]; + if (!lastOrg.has_children) { + onChange(lastOrg.id); + } else { + onChange(""); + } + } }; + useEffect(() => { + if (parentSelectedLevels) { + setSelectedLevels(parentSelectedLevels); + } + }, [parentSelectedLevels]); + const lastLevel = selectedLevels[selectedLevels.length - 1]; return ( @@ -145,6 +167,8 @@ export default function OrganizationSelector(props: OrganizationSelectorProps) { ? `SYSTEM__govt_org_type__${lastLevel.metadata?.govt_org_children_type || "default"}` : "SYSTEM__govt_org_type__default", )} + required={!!required} + errors={[props.errorMessage || ""]} >