Skip to content

Commit

Permalink
🛠️ Fixes issue with treating physician field being disabled when a se…
Browse files Browse the repository at this point in the history
…arch text entered yields no results; 🛠️ Migrate `UserAutocompleteFormField` to use `useQuery` (#8274)

* Add tests to replicate the issue

* refactor name formatting to use utility fn

* Upgrade UserAutocompleteFormField to use useQuery and have dedicated subcomponents based on linked facility or users api query

* remove unused import

* fix types

* update cypress

* fix issue with mergeQuery options and cleanup

* fix cypress syntax error

* add id for autocomplete input

* update test

* fix cypress

* skip explicit clearing

* remove test
  • Loading branch information
rithviknishad authored Aug 9, 2024
1 parent f5721b9 commit 18e9888
Show file tree
Hide file tree
Showing 24 changed files with 217 additions and 191 deletions.
11 changes: 2 additions & 9 deletions src/Components/ABDM/ABDMFacilityRecords.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,8 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) {
consent.expiry,
) < new Date()
? "EXPIRED"
: consent.consent_artefacts?.[0]?.status ??
consent.status}
: (consent.consent_artefacts?.[0]?.status ??
consent.status)}
</td>

<td className="px-3 py-4 text-center text-sm">
Expand All @@ -102,13 +102,6 @@ export default function ABDMFacilityRecords({ facilityId }: IProps) {
: "-"}
</td>

{/* <td className="px-3 py-4 text-center text-sm">
{`${consent.requester?.first_name} ${consent.requester?.last_name}`.trim()}
<p className="text-secondary-600">
({consent.requester.username})
</p>
</td> */}

<td className="px-3 py-4 text-center text-sm">
{formatDateTime(
consent.consent_artefacts?.[0]?.from_time ??
Expand Down
4 changes: 2 additions & 2 deletions src/Components/ABDM/ABDMRecordsTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import CareIcon from "../../CAREUI/icons/CareIcon";
import ButtonV2 from "../Common/components/ButtonV2";
import * as Notification from "../../Utils/Notifications.js";
import Loading from "../Common/Loading";
import { classNames } from "../../Utils/utils";
import { classNames, formatName } from "../../Utils/utils";
import { Link } from "raviger";
import routes from "../../Redux/api";
import request from "../../Utils/request/request";
Expand Down Expand Up @@ -75,7 +75,7 @@ function ConsentRequestCard({ consent }: IConsentRequestCardProps) {
}
</h5>
<h6 className="mt-1 leading-6 text-secondary-700">
{consent.requester.first_name} {consent.requester.last_name}
{formatName(consent.requester)}
</h6>
</div>
<div className="flex flex-col items-center">
Expand Down
5 changes: 2 additions & 3 deletions src/Components/Assets/AssetManage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import Pagination from "../Common/Pagination";
import { navigate } from "raviger";
import QRCode from "qrcode.react";
import AssetWarrantyCard from "./AssetWarrantyCard";
import { formatDate, formatDateTime } from "../../Utils/utils";
import { formatDate, formatDateTime, formatName } from "../../Utils/utils";
import Chip from "../../CAREUI/display/Chip";
import CareIcon from "../../CAREUI/icons/CareIcon";
import ButtonV2 from "../Common/components/ButtonV2";
Expand Down Expand Up @@ -148,8 +148,7 @@ const AssetManage = (props: AssetManageProps) => {
</td>
<td className="whitespace-nowrap px-6 py-4 text-center text-sm leading-5 text-secondary-500">
<span className="font-medium text-secondary-900">
{transaction.performed_by.first_name}{" "}
{transaction.performed_by.last_name}
{formatName(transaction.performed_by)}
</span>
</td>
<td className="whitespace-nowrap px-6 py-4 text-right text-sm leading-5 text-secondary-500">
Expand Down
4 changes: 2 additions & 2 deletions src/Components/Common/RelativeDateUserMention.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import CareIcon from "../../CAREUI/icons/CareIcon";
import { formatDateTime, relativeDate } from "../../Utils/utils";
import { formatDateTime, formatName, relativeDate } from "../../Utils/utils";
import { PerformedByModel } from "../HCX/misc";

function RelativeDateUserMention(props: {
Expand Down Expand Up @@ -28,7 +28,7 @@ function RelativeDateUserMention(props: {
}`}
>
<div className="flex flex-col whitespace-normal text-sm font-semibold leading-5 text-white">
<p className="flex justify-center">{`${props.user.first_name} ${props.user.last_name}`}</p>
<p className="flex justify-center">{formatName(props.user)}</p>
<p className="flex justify-center">{`@${props.user.username}`}</p>
<p className="flex justify-center">{props.user.user_type}</p>
</div>
Expand Down
210 changes: 128 additions & 82 deletions src/Components/Common/UserAutocompleteFormField.tsx
Original file line number Diff line number Diff line change
@@ -1,112 +1,158 @@
import { useAsyncOptions } from "../../Common/hooks/useAsyncOptions";
import { getFacilityUsers, getUserList } from "../../Redux/actions";
import { Autocomplete } from "../Form/FormFields/Autocomplete";
import FormField from "../Form/FormFields/FormField";
import {
FormFieldBaseProps,
useFormFieldPropsResolver,
} from "../Form/FormFields/Utils";
import { UserModel } from "../Users/models";
import { isUserOnline } from "../../Utils/utils";
import {
classNames,
formatName,
isUserOnline,
mergeQueryOptions,
} from "../../Utils/utils";
import { UserRole } from "../../Common/constants";
import { useEffect } from "react";
import { useEffect, useState } from "react";
import useQuery from "../../Utils/request/useQuery";
import routes from "../../Redux/api";
import { UserBareMinimum } from "../Users/models";

type Props = FormFieldBaseProps<UserModel> & {
type BaseProps = FormFieldBaseProps<UserBareMinimum> & {
placeholder?: string;
facilityId?: string;
homeFacility?: string;
userType?: UserRole;
showActiveStatus?: boolean;
noResultsError?: string;
};

export default function UserAutocompleteFormField(props: Props) {
type LinkedFacilitySearchProps = BaseProps & {
facilityId: string;
homeFacility?: undefined;
};

type UserSearchProps = BaseProps & {
facilityId?: undefined;
homeFacility?: string;
};

export default function UserAutocomplete(props: UserSearchProps) {
const field = useFormFieldPropsResolver(props);
const { fetchOptions, isLoading, options } = useAsyncOptions<UserModel>(
"id",
{ queryResponseExtractor: (data) => data.results },
);
const [query, setQuery] = useState("");
const [disabled, setDisabled] = useState(false);

let search_filter: {
limit: number;
offset: number;
home_facility?: string;
user_type?: string;
search_text?: string;
} = { limit: 50, offset: 0 };
const { data, loading } = useQuery(routes.userList, {
query: {
home_facility: props.homeFacility,
user_type: props.userType,
search_text: query,
limit: 50,
offset: 0,
},
});

if (props.showActiveStatus && props.userType) {
search_filter = { ...search_filter, user_type: props.userType };
}
useEffect(() => {
if (
loading ||
query ||
!field.required ||
!props.noResultsError ||
!data?.results
) {
return;
}

if (props.homeFacility) {
search_filter = { ...search_filter, home_facility: props.homeFacility };
}
if (data.results.length === 0) {
setDisabled(true);
field.handleChange(undefined as unknown as UserBareMinimum);
}
}, [loading, query, field.required, data?.results, props.noResultsError]);

const getStatusIcon = (option: UserModel) => {
if (!props.showActiveStatus) return null;
return (
<FormField field={field}>
<Autocomplete
id={field.id}
disabled={field.disabled || disabled}
required={field.required as true}
placeholder={(disabled && props.noResultsError) || props.placeholder}
value={field.value}
onChange={field.handleChange}
options={mergeQueryOptions(
field.value ? [field.value] : [],
data?.results ?? [],
(obj) => obj.username,
)}
optionLabel={formatName}
optionIcon={userOnlineDot}
optionDescription={(option) =>
`${option.user_type} - ${option.username}`
}
optionValue={(option) => option}
onQuery={setQuery}
isLoading={loading}
/>
</FormField>
);
}

return (
<div className="mr-6 mt-[2px]">
<svg
className={`h-3 w-3 ${
isUserOnline(option) ? "text-green-500" : "text-secondary-400"
}`}
fill="currentColor"
viewBox="0 0 8 8"
>
<circle cx="4" cy="4" r="4" />
</svg>
</div>
);
};
export const LinkedFacilityUsers = (props: LinkedFacilitySearchProps) => {
const field = useFormFieldPropsResolver(props);

const items = options(field.value && [field.value]);
const [query, setQuery] = useState("");

useEffect(() => {
if (props.required && !isLoading && !items.length && props.noResultsError) {
field.handleChange(undefined as unknown as UserModel);
}
}, [isLoading, items, props.required]);
const { data, loading } = useQuery(routes.getFacilityUsers, {
pathParams: { facility_id: props.facilityId },
query: {
user_type: props.userType,
search_text: query,
limit: 50,
offset: 0,
},
});

const noResultError =
(props.required && !isLoading && !items.length && props.noResultsError) ||
(!query &&
!loading &&
field.required &&
!data?.results?.length &&
props.noResultsError) ||
undefined;

useEffect(() => {
if (noResultError) {
field.handleChange(undefined as unknown as UserBareMinimum);
}
}, [noResultError]);

return (
<FormField field={field}>
<div className="relative">
<Autocomplete
id={field.id}
disabled={field.disabled || !!noResultError}
// Voluntarily casting type as true to ignore type errors.
required={field.required as true}
placeholder={noResultError || props.placeholder}
value={field.value}
onChange={field.handleChange}
options={items}
optionLabel={getUserFullName}
optionIcon={getStatusIcon}
optionDescription={(option) => `${option.user_type}`}
optionValue={(option) => option}
onQuery={(query) =>
fetchOptions(
props.facilityId
? getFacilityUsers(props.facilityId, {
...search_filter,
search_text: query,
})
: getUserList({ ...search_filter, search_text: query }),
)
}
isLoading={isLoading}
/>
</div>
<Autocomplete
id={field.id}
disabled={field.disabled || !!noResultError}
// Voluntarily casting type as true to ignore type errors.
required={field.required as true}
placeholder={noResultError || props.placeholder}
value={field.value}
onChange={field.handleChange}
options={mergeQueryOptions(
field.value ? [field.value] : [],
data?.results ?? [],
(obj) => obj.username,
)}
optionLabel={formatName}
optionIcon={userOnlineDot}
optionDescription={(option) =>
`${option.user_type} - ${option.username}`
}
optionValue={(option) => option}
onQuery={setQuery}
isLoading={loading}
/>
</FormField>
);
}

const getUserFullName = (user: UserModel) => {
const personName = user.first_name + " " + user.last_name;
return personName.trim().length > 0 ? personName : user.username || "";
};

const userOnlineDot = (user: UserBareMinimum) => (
<div
className={classNames(
"mr-4 size-2.5 rounded-full ",
isUserOnline(user) ? "bg-primary-500" : "bg-secondary-400",
)}
/>
);
22 changes: 11 additions & 11 deletions src/Components/Facility/ConsultationForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ import PatientCategorySelect from "../Patient/PatientCategorySelect";
import { SelectFormField } from "../Form/FormFields/SelectFormField";
import TextAreaFormField from "../Form/FormFields/TextAreaFormField";
import TextFormField from "../Form/FormFields/TextFormField";
import UserAutocompleteFormField from "../Common/UserAutocompleteFormField";
import { UserModel } from "../Users/models";
import UserAutocomplete from "../Common/UserAutocompleteFormField";
import { UserBareMinimum } from "../Users/models";

import { navigate } from "raviger";
import useAppHistory from "../../Common/hooks/useAppHistory";
Expand Down Expand Up @@ -90,7 +90,7 @@ type FormDetails = {
referred_by_external?: string;
transferred_from_location?: string;
treating_physician: string;
treating_physician_object: UserModel | null;
treating_physician_object: UserBareMinimum | null;
create_diagnoses: CreateDiagnosis[];
diagnoses: ConsultationDiagnosis[];
symptoms: EncounterSymptom[];
Expand All @@ -107,7 +107,7 @@ type FormDetails = {
is_telemedicine: BooleanStrings;
action?: number;
assigned_to: string;
assigned_to_object: UserModel | null;
assigned_to_object: UserBareMinimum | null;
special_instruction: string;
review_interval: number;
weight: string;
Expand Down Expand Up @@ -386,8 +386,8 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
admitted: data.admitted ? String(data.admitted) : "false",
admitted_to: data.admitted_to ? data.admitted_to : "",
category: data.category
? PATIENT_CATEGORIES.find((i) => i.text === data.category)?.id ??
""
? (PATIENT_CATEGORIES.find((i) => i.text === data.category)?.id ??
"")
: "",
patient_no: data.patient_no ?? "",
OPconsultation: data.consultation_notes,
Expand Down Expand Up @@ -782,7 +782,9 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
}
};

const handleDoctorSelect = (event: FieldChangeEvent<UserModel | null>) => {
const handleDoctorSelect = (
event: FieldChangeEvent<UserBareMinimum | null>,
) => {
if (event.value?.id) {
dispatch({
type: "set_form",
Expand Down Expand Up @@ -1430,7 +1432,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
className="col-span-6"
ref={fieldRef["treating_physician"]}
>
<UserAutocompleteFormField
<UserAutocomplete
name={"treating_physician"}
label={t("treating_doctor")}
placeholder="Attending Doctors Name and Designation"
Expand All @@ -1439,7 +1441,6 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
state.form.treating_physician_object ?? undefined
}
onChange={handleDoctorSelect}
showActiveStatus
userType={"Doctor"}
homeFacility={facilityId}
error={state.errors.treating_physician}
Expand Down Expand Up @@ -1483,8 +1484,7 @@ export const ConsultationForm = ({ facilityId, patientId, id }: Props) => {
className="col-span-6 flex-[2]"
ref={fieldRef["assigned_to"]}
>
<UserAutocompleteFormField
showActiveStatus
<UserAutocomplete
value={state.form.assigned_to_object ?? undefined}
onChange={handleDoctorSelect}
userType={"Doctor"}
Expand Down
Loading

0 comments on commit 18e9888

Please sign in to comment.