Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix state & district autofill & multiple other small issues in Patient Registration page #9711

Merged
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
4dd0e93
Autofill the State & District
yash-learner Jan 3, 2025
645bd96
Merge branch 'develop' into issues/9668/fix-auto-fill
yash-learner Jan 3, 2025
9cb67df
Show asterisk properly and pass error message
yash-learner Jan 3, 2025
d1042e4
Merge branch 'develop' into issues/9668/fix-auto-fill
yash-learner Jan 3, 2025
c15621d
Remove console logs
yash-learner Jan 3, 2025
67073f8
Inform parent about deletion & handle it
yash-learner Jan 3, 2025
5ba881e
Add cursor style to address input when disabled
yash-learner Jan 3, 2025
3b77133
Use organization type instead of any
yash-learner Jan 3, 2025
f58e7b5
Merge branch 'develop' into issues/9668/fix-auto-fill
yash-learner Jan 3, 2025
bdd739f
Add custom hook for organization data fetching
yash-learner Jan 6, 2025
6905db3
Add custom hook to fetch state and district from pincode
yash-learner Jan 6, 2025
45a3d9f
Refactor pincode handling to use custom hook for state and district r…
yash-learner Jan 6, 2025
ca922bb
Merge branch 'develop' into issues/9668/fix-auto-fill
yash-learner Jan 6, 2025
0ca6b73
Add error handling messages for organization and pincode fetching
yash-learner Jan 6, 2025
c6b1783
Update condition for showing auto-filled pincode in PatientRegistrati…
yash-learner Jan 6, 2025
d3fb25d
Merge branch 'develop' into issues/9668/fix-auto-fill
Jacobjeevan Jan 14, 2025
8ee3d60
switch to query debounce for phone
Jacobjeevan Jan 14, 2025
1d3740e
minor fixes
Jacobjeevan Jan 14, 2025
e4b9514
coderabbit suggestions
Jacobjeevan Jan 14, 2025
22687f3
minor changes
Jacobjeevan Jan 15, 2025
5558a67
rm select
Jacobjeevan Jan 15, 2025
0b5a74d
added useEffect for error alerts
Jacobjeevan Jan 15, 2025
e150868
rm type from useQuery (redundant)
Jacobjeevan Jan 15, 2025
ebee745
Merge branch 'develop' into issues/9668/fix-auto-fill
nihal467 Jan 16, 2025
8d1f0c8
Merge branch 'develop' into issues/9668/fix-auto-fill
Jacobjeevan Jan 16, 2025
060f3e3
fixed the cypress failure
nihal467 Jan 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions public/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1452,6 +1452,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.",
Expand Down Expand Up @@ -1557,7 +1558,9 @@
"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_must_be_6_digits": "Pincode must be a 6-digit number",
"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",
Expand Down
77 changes: 57 additions & 20 deletions src/components/Patient/PatientRegistration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useTranslation } from "react-i18next";
import { toast } from "sonner";
import { z } from "zod";

import CareIcon from "@/CAREUI/icons/CareIcon";
import SectionNavigator from "@/CAREUI/misc/SectionNavigator";

import { Button } from "@/components/ui/button";
Expand Down Expand Up @@ -37,6 +38,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,
Expand Down Expand Up @@ -79,7 +81,8 @@ export default function PatientRegistration(

const [suppressDuplicateWarning, setSuppressDuplicateWarning] =
useState(!!patientId);
const [debouncedNumber, setDebouncedNumber] = useState<string>();
const [selectedLevels, setSelectedLevels] = useState<Organization[]>([]);
const [showAutoFilledPincode, setShowAutoFilledPincode] = useState(false);

const formSchema = useMemo(
() =>
Expand Down Expand Up @@ -192,6 +195,28 @@ export default function PatientRegistration(
},
});

const { stateOrg, districtOrg } = useStateAndDistrictFromPincode({
pincode: form.watch("pincode")?.toString() || "",
});

useEffect(() => {
// Fill by pincode for patient registration
if (patientId) return;
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);
}, 5000);
return () => clearTimeout(timer);
}
return () => setShowAutoFilledPincode(false);
}, [stateOrg, districtOrg, patientId]);

function onSubmit(values: z.infer<typeof formSchema>) {
if (patientId) {
updatePatient({ ...values, ward_old: undefined });
Expand Down Expand Up @@ -226,13 +251,13 @@ export default function PatientRegistration(
};

const patientPhoneSearch = useQuery({
queryKey: ["patients", "phone-number", debouncedNumber],
queryFn: query(routes.searchPatient, {
queryKey: ["patients", "phone-number", form.watch("phone_number")],
queryFn: query.debounced(routes.searchPatient, {
body: {
phone_number: parsePhoneNumber(debouncedNumber || "") || "",
phone_number: parsePhoneNumber(form.watch("phone_number") || "") || "",
},
}),
enabled: !!parsePhoneNumber(debouncedNumber || ""),
enabled: !!parsePhoneNumber(form.watch("phone_number") || ""),
});

const duplicatePatients = useMemo(() => {
Expand All @@ -249,6 +274,9 @@ export default function PatientRegistration(

useEffect(() => {
if (patientQuery.data) {
setSelectedLevels([
patientQuery.data.geo_organization as unknown as Organization,
]);
form.reset({
...patientQuery.data,
same_phone_number:
Expand All @@ -257,27 +285,19 @@ export default function PatientRegistration(
same_address:
patientQuery.data.address === patientQuery.data.permanent_address,
age_or_dob: patientQuery.data.date_of_birth ? "dob" : "age",
age: !patientQuery.data.date_of_birth
? patientQuery.data.age
: undefined,
date_of_birth: patientQuery.data.date_of_birth
? patientQuery.data.date_of_birth
: undefined,
geo_organization: (
patientQuery.data.geo_organization as unknown as Organization
)?.id,
} as unknown as z.infer<typeof formSchema>);
}
}, [patientQuery.data]); // eslint-disable-line react-hooks/exhaustive-deps

useEffect(() => {
const handler = setTimeout(() => {
const phoneNumber = form.getValues("phone_number");
if (!patientId || patientQuery.data?.phone_number !== phoneNumber) {
setSuppressDuplicateWarning(false);
}
setDebouncedNumber(phoneNumber);
}, 500);

return () => {
clearTimeout(handler);
};
}, [form.watch("phone_number")]); // eslint-disable-line react-hooks/exhaustive-deps

if (patientId && patientQuery.isLoading) {
return <Loading />;
}
Expand Down Expand Up @@ -683,6 +703,22 @@ export default function PatientRegistration(
/>
</FormControl>
<FormMessage />
{showAutoFilledPincode && (
<div
role="status"
aria-live="polite"
className="flex items-center"
>
<CareIcon
icon="l-check-circle"
className="mr-2 text-sm text-green-500"
aria-hidden="true"
/>
<span className="text-sm text-primary-500">
{t("pincode_autofill")}
</span>
</div>
)}
</FormItem>
)}
/>
Expand Down Expand Up @@ -722,6 +758,7 @@ export default function PatientRegistration(
<OrganizationSelector
{...field}
required={true}
selected={selectedLevels}
value={form.watch("geo_organization")}
onChange={(value) =>
form.setValue("geo_organization", value)
Expand Down Expand Up @@ -757,7 +794,7 @@ export default function PatientRegistration(
</div>
{!patientPhoneSearch.isLoading &&
!!duplicatePatients?.length &&
!!parsePhoneNumber(debouncedNumber || "") &&
!!parsePhoneNumber(form.watch("phone_number") || "") &&
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codebase verification

Phone number parsing implementation needs improvement

The current implementation has several limitations that could affect robustness:

  • The minimum length check of 12 digits is too restrictive for valid international numbers
  • Special case handling of "+91" should be generalized
  • Limited validation of number format and country codes
🔗 Analysis chain

Verify phone number parsing edge cases.

The phone number parsing should be tested with various formats to ensure robust handling.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Description: Search for phone number parsing usage and validation
rg -A 2 "parsePhoneNumber" 

Length of output: 4594


Script:

#!/bin/bash
# Get the implementation of parsePhoneNumber function
ast-grep --pattern 'export const parsePhoneNumber = $_'

Length of output: 944

!suppressDuplicateWarning && (
<DuplicatePatientDialog
patientList={duplicatePatients}
Expand Down
44 changes: 44 additions & 0 deletions src/hooks/useOrganization.ts
Original file line number Diff line number Diff line change
@@ -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[] }>({
Jacobjeevan marked this conversation as resolved.
Show resolved Hide resolved
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,
};
}
72 changes: 72 additions & 0 deletions src/hooks/useStateAndDistrictFromPincode.ts
Original file line number Diff line number Diff line change
@@ -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: pincode !== "" && 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,
};
}
Jacobjeevan marked this conversation as resolved.
Show resolved Hide resolved
36 changes: 33 additions & 3 deletions src/pages/Organization/components/OrganizationSelector.tsx
Original file line number Diff line number Diff line change
@@ -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";

Expand All @@ -17,6 +17,8 @@ interface OrganizationSelectorProps {
onChange: (value: string) => void;
required?: boolean;
authToken?: string;
selected?: Organization[];
errorMessage?: string;
}

interface AutoCompleteOption {
Expand All @@ -26,7 +28,7 @@ interface AutoCompleteOption {

// TODO: Rename to GovtOrganizationSelector
export default function OrganizationSelector(props: OrganizationSelectorProps) {
const { onChange, required } = props;
const { onChange, required, selected } = props;
const [selectedLevels, setSelectedLevels] = useState<Organization[]>([]);
const [searchQuery, setSearchQuery] = useState("");

Expand All @@ -38,6 +40,22 @@ export default function OrganizationSelector(props: OrganizationSelectorProps) {
}
: {};

useEffect(() => {
if (selected && selected.length > 0) {
let currentOrg = selected[0];
if (currentOrg.level_cache === 0) {
setSelectedLevels(selected);
} else {
const levels: Organization[] = [];
while (currentOrg && currentOrg.level_cache >= 0) {
levels.unshift(currentOrg);
currentOrg = currentOrg.parent as unknown as Organization;
}
setSelectedLevels(levels);
}
}
}, [selected]);

const { data: getAllOrganizations } = useQuery({
queryKey: ["organizations-root", searchQuery],
queryFn: query.debounced(organizationApi.list, {
Expand Down Expand Up @@ -101,7 +119,19 @@ 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("");
}
}
};

const lastLevel = selectedLevels[selectedLevels.length - 1];
Expand Down
Loading