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

Add support for Location Assignment Question #10441

Merged
merged 11 commits into from
Feb 6, 2025
1 change: 1 addition & 0 deletions src/common/constants.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const LocalStorageKeys = {
accessToken: "care_access_token",
refreshToken: "care_refresh_token",
patientTokenKey: "care_patient_token",
loginPreference: "care_login_preference",
};

export interface OptionsType {
Expand Down
9 changes: 8 additions & 1 deletion src/components/Auth/Login.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import careConfig from "@careConfig";
import { useMutation } from "@tanstack/react-query";
import { Link, useQueryParams } from "raviger";
import { useState } from "react";
import { useEffect, useState } from "react";
import ReCaptcha from "react-google-recaptcha";
import { useTranslation } from "react-i18next";
import { isValidPhoneNumber } from "react-phone-number-input";
Expand Down Expand Up @@ -31,6 +31,8 @@

import { useAuthContext } from "@/hooks/useAuthUser";

import { LocalStorageKeys } from "@/common/constants";

import FiltersCache from "@/Utils/FiltersCache";
import ViewCache from "@/Utils/ViewCache";
import routes from "@/Utils/request/api";
Expand Down Expand Up @@ -74,14 +76,14 @@
const { reCaptchaSiteKey, urls, stateLogo, customLogo, customLogoAlt } =
careConfig;
const customDescriptionHtml = __CUSTOM_DESCRIPTION_HTML__;
const initForm: any = {

Check warning on line 79 in src/components/Auth/Login.tsx

View workflow job for this annotation

GitHub Actions / cypress-run (1)

Unexpected any. Specify a different type
username: "",
password: "",
};
const { forgot } = props;
const [params] = useQueryParams();
const { mode } = params;
const initErr: any = {};

Check warning on line 86 in src/components/Auth/Login.tsx

View workflow job for this annotation

GitHub Actions / cypress-run (1)

Unexpected any. Specify a different type
const [form, setForm] = useState(initForm);
const [errors, setErrors] = useState(initErr);
const [isCaptchaEnabled, setCaptcha] = useState(false);
Expand All @@ -96,6 +98,11 @@
const [otpError, setOtpError] = useState<string>("");
const [otpValidationError, setOtpValidationError] = useState<string>("");

// Remember the last login mode
useEffect(() => {
localStorage.setItem(LocalStorageKeys.loginPreference, loginMode);
}, [loginMode]);

// Staff Login Mutation
const staffLoginMutation = useMutation({
mutationFn: async (data: LoginFormData) => {
Expand Down
9 changes: 8 additions & 1 deletion src/components/Common/LoginHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,14 @@ export const LoginHeader = () => {
<Button
variant="ghost"
className="text-sm font-medium hover:bg-gray-100 rounded-full px-6"
onClick={() => navigate("/login?mode=patient")}
onClick={() =>
navigate(
`/login?mode=${
localStorage.getItem(LocalStorageKeys.loginPreference) ??
"patient"
}`,
)
}
>
{t("sign_in")}
</Button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,50 +17,27 @@ import { RESULTS_PER_PAGE_LIMIT } from "@/common/constants";
import routes from "@/Utils/request/api";
import query from "@/Utils/request/query";
import { formatDateTime, properCase } from "@/Utils/utils";
import { AllergyIntoleranceRequest } from "@/types/emr/allergyIntolerance/allergyIntolerance";
import { DiagnosisRequest } from "@/types/emr/diagnosis/diagnosis";
import { Encounter } from "@/types/emr/encounter";
import { MedicationRequest } from "@/types/emr/medicationRequest";
import { MedicationStatementRequest } from "@/types/emr/medicationStatement";
import { SymptomRequest } from "@/types/emr/symptom/symptom";
import { ResponseValue } from "@/types/questionnaire/form";
import { Question } from "@/types/questionnaire/question";
import { QuestionnaireResponse } from "@/types/questionnaire/questionnaireResponse";
import { CreateAppointmentQuestion } from "@/types/scheduling/schedule";

interface Props {
encounter?: Encounter;
patientId: string;
}

type ResponseValueType = {
value?:
| string
| number
| boolean
| Date
| Encounter
| AllergyIntoleranceRequest[]
| MedicationRequest[]
| MedicationStatementRequest[]
| SymptomRequest[]
| DiagnosisRequest[]
| CreateAppointmentQuestion;
value_quantity?: {
value: number;
};
};

interface QuestionResponseProps {
question: Question;
response?: {
values: ResponseValueType[];
values: ResponseValue[];
note?: string;
question_id: string;
};
}

export function formatValue(
value: ResponseValueType["value"],
value: ResponseValue["value"],
type: string,
): string {
if (!value) return "";
Expand Down Expand Up @@ -120,7 +97,7 @@ function QuestionGroup({
}: {
group: Question;
responses: {
values: ResponseValueType[];
values: ResponseValue[];
note?: string;
question_id: string;
}[];
Expand Down
85 changes: 85 additions & 0 deletions src/components/Location/LocationSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";

import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";

import query from "@/Utils/request/query";
import { LocationList } from "@/types/location/location";
import locationApi from "@/types/location/locationApi";

interface LocationSearchProps {
facilityId: string;
mode?: "kind" | "location";
onSelect: (location: LocationList) => void;
disabled?: boolean;
value?: LocationList | null;
}

export function LocationSearch({
facilityId,
mode,
onSelect,
disabled,
value,
}: LocationSearchProps) {
const [open, setOpen] = useState(false);
const [search, setSearch] = useState("");

const { data: locations } = useQuery({
queryKey: ["locations", facilityId, mode, search],
queryFn: query(locationApi.list, {
pathParams: { facility_id: facilityId },
queryParams: { mode, search },
}),
enabled: facilityId !== "preview",
});
rithviknishad marked this conversation as resolved.
Show resolved Hide resolved

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={disabled}>
<div
className="w-full h-9 px-3 rounded-md border text-sm flex items-center justify-between cursor-pointer"
role="combobox"
aria-expanded={open}
>
{value?.name || "Select location..."}
</div>
</PopoverTrigger>
<PopoverContent className="w-[400px] p-0">
<Command>
<CommandInput
placeholder="Search locations..."
value={search}
onValueChange={setSearch}
/>
<CommandEmpty>No locations found.</CommandEmpty>
<CommandGroup>
{locations?.results.map((location) => (
<CommandItem
key={location.id}
value={location.name}
onSelect={() => {
onSelect(location);
setOpen(false);
}}
>
{location.name}
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
);
}
37 changes: 37 additions & 0 deletions src/components/Patient/PatientInfoCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,43 @@ export default function PatientInfoCard(props: PatientInfoCardProps) {
</PopoverContent>
</Popover>

{props.encounter.current_location && (
<Popover>
<PopoverTrigger asChild>
<div>
<Badge
className="capitalize gap-1 py-1 px-2 cursor-pointer hover:bg-secondary-100"
variant="outline"
title={`Current Location: ${props.encounter.current_location.name}`}
>
<Building className="w-4 h-4 text-blue-400" />
{props.encounter.current_location.name}
<ChevronDown className="h-3 w-3 opacity-50" />
</Badge>
</div>
</PopoverTrigger>
<PopoverContent align={"start"} className="w-auto p-2">
<div className="space-y-2">
<h4 className="font-medium text-sm">
Current Location
</h4>
<p className="text-sm text-gray-700">
{props.encounter.current_location.name}
</p>
<p className="text-sm text-gray-500">
{props.encounter.current_location.description}
</p>
</div>
<Button variant="outline">
<Link
href={`/facility/${props.encounter.facility.id}/patient/${props.patient.id}/encounter/${props.encounter.id}/questionnaire/location_association`}
>
Move Patient
</Link>
</Button>
</PopoverContent>
</Popover>
)}
<Popover>
<PopoverTrigger asChild>
<div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ export function AppointmentQuestion({
[
{
type: "appointment",
value: [appointment] as unknown as ResponseValue["value"],
value: [appointment],
},
],
questionnaireResponse.question_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export function DateTimeQuestion({
[
{
type: "dateTime",
value: date.toISOString(),
value: date,
},
],
questionnaireResponse.question_id,
Expand All @@ -75,7 +75,7 @@ export function DateTimeQuestion({
[
{
type: "dateTime",
value: date.toISOString(),
value: date,
},
],
questionnaireResponse.question_id,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ export function EncounterQuestion({
// Create the response value with the encounter request
const responseValue: ResponseValue = {
type: "encounter",
value: [encounterRequest] as unknown as typeof responseValue.value,
value: [encounterRequest],
};

updateQuestionnaireResponseCB(
Expand Down
116 changes: 116 additions & 0 deletions src/components/Questionnaire/QuestionTypes/LocationQuestion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { format } from "date-fns";
import React, { useState } from "react";

import { Input } from "@/components/ui/input";

import { LocationSearch } from "@/components/Location/LocationSearch";

import { LocationAssociationQuestion } from "@/types/location/association";
import { LocationList } from "@/types/location/location";
import {
QuestionnaireResponse,
ResponseValue,
} from "@/types/questionnaire/form";
import { Question } from "@/types/questionnaire/question";

interface LocationQuestionProps {
question: Question;
questionnaireResponse: QuestionnaireResponse;
updateQuestionnaireResponseCB: (
values: ResponseValue[],
questionId: string,
note?: string,
) => void;
disabled?: boolean;
facilityId: string;
locationId: string;
encounterId: string;
}

export function LocationQuestion({
questionnaireResponse,
updateQuestionnaireResponseCB,
disabled,
facilityId,
encounterId,
}: LocationQuestionProps) {
const [selectedLocation, setSelectedLocation] = useState<LocationList | null>(
null,
);

const values =
(questionnaireResponse.values?.[0]
?.value as unknown as LocationAssociationQuestion[]) || [];
Comment on lines +41 to +43
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add type assertion safety check.

The type assertion to LocationAssociationQuestion[] could fail at runtime. Add a type guard for safety.

-  const values =
-    (questionnaireResponse.values?.[0]
-      ?.value as unknown as LocationAssociationQuestion[]) || [];
+  const values = (() => {
+    const value = questionnaireResponse.values?.[0]?.value;
+    if (Array.isArray(value) && value.every(v => 
+      typeof v === 'object' && v !== null && 'location' in v
+    )) {
+      return value as LocationAssociationQuestion[];
+    }
+    return [];
+  })();
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const values =
(questionnaireResponse.values?.[0]
?.value as unknown as LocationAssociationQuestion[]) || [];
const values = (() => {
const value = questionnaireResponse.values?.[0]?.value;
if (
Array.isArray(value) &&
value.every(
v => typeof v === 'object' && v !== null && 'location' in v
)
) {
return value as LocationAssociationQuestion[];
}
return [];
})();


const association = values[0] ?? {};

const handleUpdateAssociation = (
updates: Partial<LocationAssociationQuestion>,
) => {
const newAssociation: LocationAssociationQuestion = {
id: association?.id || null,
encounter: encounterId,
start_datetime: association?.start_datetime || new Date().toISOString(),
end_datetime: null,
status: "active",
location: association?.location || "",
meta: {},
created_by: null,
updated_by: null,
...updates,
};

updateQuestionnaireResponseCB(
[{ type: "location_association", value: [newAssociation] }],
questionnaireResponse.question_id,
);
};

const handleLocationSelect = (location: LocationList) => {
setSelectedLocation(location);
handleUpdateAssociation({ location: location.id });
};

return (
<div className="space-y-4">
<div className="rounded-lg border p-4 space-y-4">
<div>
<label className="text-sm font-medium mb-1 block">
Select Location
</label>
<LocationSearch
mode="kind"
facilityId={facilityId}
onSelect={handleLocationSelect}
disabled={disabled}
value={selectedLocation}
/>
</div>

{selectedLocation && (
<div>
<label className="text-sm font-medium mb-1 block">Start Time</label>
<Input
type="datetime-local"
value={
association?.start_datetime
? format(
new Date(association.start_datetime),
"yyyy-MM-dd'T'HH:mm",
)
: format(new Date(), "yyyy-MM-dd'T'HH:mm")
}
onChange={(e) =>
handleUpdateAssociation({
start_datetime: new Date(e.target.value).toISOString(),
})
}
disabled={disabled}
className="h-9"
/>
</div>
)}
</div>
</div>
);
}
Loading
Loading