diff --git a/public/locale/en.json b/public/locale/en.json index 3fbcfc3de72..56c0472e115 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -1430,6 +1430,7 @@ "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_blood_group": "Please select the blood group", "please_select_breathlessness_level": "Please select Breathlessness Level", "please_select_district": "Please select the district", "please_select_facility_type": "Please select Facility Type", diff --git a/src/components/Patient/PatientRegistration.tsx b/src/components/Patient/PatientRegistration.tsx index 94289124403..19bc05394f1 100644 --- a/src/components/Patient/PatientRegistration.tsx +++ b/src/components/Patient/PatientRegistration.tsx @@ -1,15 +1,25 @@ -import careConfig from "@careConfig"; +import { zodResolver } from "@hookform/resolvers/zod"; import { useMutation, useQuery } from "@tanstack/react-query"; import { navigate, useQueryParams } from "raviger"; -import { Fragment, useEffect, useState } from "react"; +import { useEffect, useMemo, useState } from "react"; +import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; +import { z } from "zod"; import SectionNavigator from "@/CAREUI/misc/SectionNavigator"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group"; import { Select, @@ -33,19 +43,14 @@ 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 { parsePhoneNumber } from "@/Utils/utils"; import OrganizationSelector from "@/pages/Organization/components/OrganizationSelector"; -import { PatientModel, validatePatient } from "@/types/emr/patient"; +import { PatientModel } from "@/types/emr/patient"; import Autocomplete from "../ui/autocomplete"; @@ -54,6 +59,74 @@ interface PatientRegistrationPageProps { patientId?: string; } +export const GENDERS = GENDER_TYPES.map((gender) => gender.id) as [ + (typeof GENDER_TYPES)[number]["id"], +]; + +export const BLOOD_GROUPS = BLOOD_GROUP_CHOICES.map((bg) => bg.id) as [ + (typeof BLOOD_GROUP_CHOICES)[number]["id"], +]; + +const formSchema = z + .object({ + name: z.string().nonempty("Name is required"), + phone_number: z + .string() + .regex(/^\+\d{12}$/, "Phone number must be a 10-digit mobile number"), + same_phone_number: z.boolean(), + emergency_phone_number: z + .string() + .regex( + /^\+\d{12}$/, + "Emergency phone number must be a 10-digit mobile number", + ), + gender: z.enum(GENDERS, { required_error: "Gender is required" }), + blood_group: z.enum(BLOOD_GROUPS, { + required_error: "Blood group is required", + }), + yob_or_dob: z.enum(["dob", "age"]), + date_of_birth: z + .string() + .regex( + /^\d{4}-\d{2}-\d{2}$/, + "Date of birth must be in YYYY-MM-DD format", + ) + .optional(), + year_of_birth: z + .string() + .regex(/^\d{4}$/, "Year of birth must be in YYYY format") + .optional(), + address: z.string().nonempty("Address is required"), + same_address: z.boolean(), + permanent_address: z.string().nonempty("Emergency address is required"), + pincode: z + .number() + .int() + .positive() + .min(100000, "Pincode must be a 6-digit number") + .max(999999, "Pincode must be a 6-digit number"), + nationality: z.string().nonempty("Nationality is required"), + geo_organization: z + .string() + .uuid("Geo organization must be a valid UUID") + .optional(), + }) + .refine((data) => (data.yob_or_dob === "dob" ? !!data.date_of_birth : true), { + message: "Date of birth must be present", + path: ["date_of_birth"], + }) + .refine((data) => (data.yob_or_dob === "age" ? !!data.year_of_birth : true), { + message: "Year of birth must be present", + path: ["year_of_birth"], + }) + .refine( + (data) => (data.nationality === "India" ? !!data.geo_organization : true), + { + message: "Geo organization is required when nationality is India", + path: ["geo_organization"], + }, + ); + export default function PatientRegistration( props: PatientRegistrationPageProps, ) { @@ -62,63 +135,19 @@ export default function PatientRegistration( 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: phone_number || "+91", - emergency_phone_number: "+91", - }); - const [feErrors, setFeErrors] = useState< - Partial> - >({}); const [suppressDuplicateWarning, setSuppressDuplicateWarning] = useState(!!patientId); 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", - "geo_organization", - "gender", - "blood_group", - "date_of_birth", - "age", - "address", - "permanent_address", - "pincode", - "nationality", - "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, - age: ageDob === "age" ? form.age : undefined, - meta_info: { - ...(form.meta_info as any), - occupation: - form.meta_info?.occupation === "" - ? undefined - : form.meta_info?.occupation, + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + nationality: "India", + phone_number: phone_number || "+91", + emergency_phone_number: "+91", + yob_or_dob: "dob", }, - }; + }); const createPatientMutation = useMutation({ mutationFn: mutate(routes.addPatient), @@ -131,7 +160,7 @@ export default function PatientRegistration( query: { phone_number: resp.phone_number, year_of_birth: - ageDob === "dob" + form.getValues("yob_or_dob") === "dob" ? new Date(resp.date_of_birth!).getFullYear() : new Date().getFullYear() - Number(resp.age!), partial_id: resp?.id?.slice(0, 5), @@ -162,86 +191,32 @@ export default function PatientRegistration( }, }); - const patientQuery = useQuery({ - queryKey: ["patient", patientId], - queryFn: query(routes.getPatient, { - pathParams: { id: patientId || "" }, - }), - enabled: !!patientId, - }); - - 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); + function onSubmit(values: z.infer) { + if (patientId) { + updatePatientMutation.mutate({ ...values, ward_old: undefined }); + return; } - }, [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); - }; + createPatientMutation.mutate({ + ...values, + facility: facilityId, + ward_old: undefined, + }); + } - useEffect(() => { - const timeout = setTimeout( - () => handlePincodeChange(form.pincode?.toString() || ""), - 1000, - ); - return () => clearTimeout(timeout); - }, [form.pincode]); + const sidebarItems = [ + { label: t("patient__general-info"), id: "general-info" }, + ]; 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") { navigate(`/facility/${facilityId}/patients`, { query: { - phone_number: form.phone_number, + phone_number: form.getValues("phone_number"), }, }); } else { @@ -249,55 +224,56 @@ export default function PatientRegistration( } }; - 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); + const patientPhoneSearch = useQuery({ + queryKey: ["patients", "phone-number", debouncedNumber], + queryFn: query(routes.searchPatient, { + body: { + phone_number: parsePhoneNumber(debouncedNumber || "") || "", + }, + }), + enabled: !!parsePhoneNumber(debouncedNumber || ""), + }); + + const duplicatePatients = useMemo(() => { + return patientPhoneSearch.data?.results.filter((p) => p.id !== patientId); + }, [patientPhoneSearch.data, patientId]); + + const patientQuery = useQuery({ + queryKey: ["patient", patientId], + queryFn: query(routes.getPatient, { + pathParams: { id: patientId || "" }, + }), + enabled: !!patientId, + }); + + useEffect(() => { + if (patientQuery.data) { + form.reset({ + ...patientQuery.data, + same_phone_number: + patientQuery.data.phone_number === + patientQuery.data.emergency_phone_number, + same_address: + patientQuery.data.address === patientQuery.data.permanent_address, + yob_or_dob: patientQuery.data.date_of_birth ? "dob" : "age", + } as unknown as z.infer); } - }; + }, [patientQuery.data]); // eslint-disable-line react-hooks/exhaustive-deps useEffect(() => { const handler = setTimeout(() => { - if (!patientId || patientQuery.data?.phone_number !== form.phone_number) { + const phoneNumber = form.getValues("phone_number"); + if (!patientId || patientQuery.data?.phone_number !== phoneNumber) { setSuppressDuplicateWarning(false); } - setDebouncedNumber(form.phone_number); + setDebouncedNumber(phoneNumber); }, 500); return () => { clearTimeout(handler); }; - }, [form.phone_number]); - - const patientPhoneSearch = useQuery({ - queryKey: ["patients", "phone-number", debouncedNumber], - queryFn: query(routes.searchPatient, { - body: { - phone_number: parsePhoneNumber(debouncedNumber || "") || "", - }, - }), - enabled: !!parsePhoneNumber(debouncedNumber || ""), - }); + }, [form.watch("phone_number")]); // eslint-disable-line react-hooks/exhaustive-deps - const duplicatePatients = patientPhoneSearch.data?.results.filter( - (p) => p.id !== patientId, - ); if (patientId && patientQuery.isLoading) { return ; } @@ -307,558 +283,470 @@ export default function PatientRegistration(
-
-
-

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

-
{t("general_info_detail")}
-
- - -
- {errors["name"] && - errors["name"].map((error, i) => ( -
- {error} -
- ))} -
-
- - { - 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, - })); - }} - /> -
- {errors["phone_number"] && - errors["phone_number"]?.map((error, i) => ( -
- {error} -
- ))} -
-
- { - const newValue = !samePhoneNumber; - setSamePhoneNumber(newValue); - if (newValue) { - setForm((f) => ({ - ...f, - emergency_phone_number: f.phone_number, - })); - } - }} - id="same-phone-number" + + +
+
+

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

+
{t("general_info_detail")}
+
+ + ( + + + {t("name")} + * + + + + + + + )} /> - -
-
- - - { - if (e.target.value.length > 13) return; - setForm((f) => ({ - ...f, - emergency_phone_number: e.target.value, - })); - }} - disabled={samePhoneNumber} - /> -
- {errors["emergency_phone_number"] && - errors["emergency_phone_number"]?.map((error, i) => ( -
- {error} -
- ))} -
- {/*
- */} -
- - - - setForm((f) => ({ ...f, gender: value })) - } - className="flex items-center gap-4" - > - {GENDER_TYPES.map((g) => ( - - - - - ))} - -
- {errors["gender"] && - errors["gender"]?.map((error, i) => ( -
- {error} -
- ))} -
-
- - - -
- {errors["blood_group"] && - errors["blood_group"]?.map((error, i) => ( -
- {error} -
- ))} -
+ ( + + + {t("phone_number")} + * + + + { + form.setValue("phone_number", e.target.value); + if (form.watch("same_phone_number")) { + form.setValue( + "emergency_phone_number", + e.target.value, + ); + } + }} + /> + + + ( + + + { + field.onChange(v); + if (v) { + form.setValue( + "emergency_phone_number", + form.watch("phone_number"), + ); + } + }} + /> + + + {t("use_phone_number_for_emergency")} + + + )} + /> + + + + )} + /> -
- - 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"] && ( -
- {errors["date_of_birth"].map((error, i) => ( -
- {error} -
- ))} -
+ + + + )} -
- -
- {t("age_input_warning")} -
- {t("age_input_warning_bold")} -
-
- - - setForm((f) => ({ - ...f, - age: e.target.value, - year_of_birth: e.target.value - ? new Date().getFullYear() - Number(e.target.value) - : undefined, - })) - } - type="number" + /> + + + form.setValue("yob_or_dob", v as "dob" | "age") + } + > + + {t("date_of_birth")} + {t("age")} + + + ( + + +
+
+
+ {t("day")} + * +
+ + form.setValue( + "date_of_birth", + `${form.watch("date_of_birth")?.split("-")[0]}-${form.watch("date_of_birth")?.split("-")[1]}-${e.target.value}`, + ) + } + /> +
+ +
+
+ {t("month")} + * +
+ + form.setValue( + "date_of_birth", + `${form.watch("date_of_birth")?.split("-")[0]}-${e.target.value}-${form.watch("date_of_birth")?.split("-")[2]}`, + ) + } + /> +
+ +
+
+ {t("year")} + * +
+ + form.setValue( + "date_of_birth", + `${e.target.value}-${form.watch("date_of_birth")?.split("-")[1]}-${form.watch("date_of_birth")?.split("-")[2]}`, + ) + } + /> +
+
+
+ +
+ )} /> -
- {errors["year_of_birth"] && - errors["year_of_birth"]?.map((error, i) => ( -
- {error} -
- ))} + + +
+ {t("age_input_warning")} +
+ {t("age_input_warning_bold")}
- {form.year_of_birth && ( -
- {t("year_of_birth")} : {form.year_of_birth} -
- )} -
-
-
-
- - -