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 @@ -75,14 +77,14 @@
const { reCaptchaSiteKey, urls, stateLogo, customLogo, customLogoAlt } =
careConfig;
const customDescriptionHtml = __CUSTOM_DESCRIPTION_HTML__;
const initForm: any = {

Check warning on line 80 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 87 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 @@ -97,6 +99,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 @@ -23,6 +23,7 @@ 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 { LocationAssociationQuestion } from "@/types/location/association";
import { Question } from "@/types/questionnaire/question";
import { QuestionnaireResponse } from "@/types/questionnaire/questionnaireResponse";
import { CreateAppointmentQuestion } from "@/types/scheduling/schedule";
Expand All @@ -32,6 +33,7 @@ interface Props {
patientId: string;
}

// TODO: Ensure that this type is not defined elsewhere.
type ResponseValueType = {
value?:
| string
Expand All @@ -44,7 +46,8 @@ type ResponseValueType = {
| MedicationStatementRequest[]
| SymptomRequest[]
| DiagnosisRequest[]
| CreateAppointmentQuestion;
| CreateAppointmentQuestion[]
| LocationAssociationQuestion[];
value_quantity?: {
value: number;
};
Expand Down
84 changes: 84 additions & 0 deletions src/components/Location/LocationSearch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
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 },
}),
});

return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild disabled={disabled}>
<div
className="w-full h-9 px-3 rounded-md border border-input bg-background text-sm ring-offset-background 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>
);
}
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>
);
}
13 changes: 13 additions & 0 deletions src/components/Questionnaire/QuestionTypes/QuestionInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { ChoiceQuestion } from "./ChoiceQuestion";
import { DateTimeQuestion } from "./DateTimeQuestion";
import { DiagnosisQuestion } from "./DiagnosisQuestion";
import { EncounterQuestion } from "./EncounterQuestion";
import { LocationQuestion } from "./LocationQuestion";
import { MedicationRequestQuestion } from "./MedicationRequestQuestion";
import { MedicationStatementQuestion } from "./MedicationStatementQuestion";
import { NotesInput } from "./NotesInput";
Expand Down Expand Up @@ -167,6 +168,18 @@ export function QuestionInput({
);
}
return null;
case "location_association":
if (encounterId) {
return (
<LocationQuestion
{...commonProps}
facilityId={facilityId}
locationId={patientId}
encounterId={encounterId}
/>
);
}
return null;
Comment on lines +171 to +182
Copy link
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider adding facilityId validation.

While the implementation follows the pattern of other structured question types, it should validate facilityId since it's required for location associations.

Apply this diff to add the validation:

           case "location_association":
-            if (encounterId) {
+            if (encounterId && facilityId) {
               return (
                 <LocationQuestion
                   {...commonProps}
                   facilityId={facilityId}
                   locationId={patientId}
                   encounterId={encounterId}
                 />
               );
             }
+            console.error("Both encounterId and facilityId are required for location associations");
             return null;
📝 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
case "location_association":
if (encounterId) {
return (
<LocationQuestion
{...commonProps}
facilityId={facilityId}
locationId={patientId}
encounterId={encounterId}
/>
);
}
return null;
case "location_association":
if (encounterId && facilityId) {
return (
<LocationQuestion
{...commonProps}
facilityId={facilityId}
locationId={patientId}
encounterId={encounterId}
/>
);
}
console.error("Both encounterId and facilityId are required for location associations");
return null;

}
return null;

Expand Down
29 changes: 16 additions & 13 deletions src/components/Questionnaire/QuestionnaireEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,17 @@ interface QuestionnaireEditorProps {
id?: string;
}

const STRUCTURED_QUESTION_TYPES = [
{ value: "allergy_intolerance", label: "Allergy Intolerance" },
{ value: "medication_request", label: "Medication Request" },
{ value: "medication_statement", label: "Medication Statement" },
{ value: "symptom", label: "Symptom" },
{ value: "diagnosis", label: "Diagnosis" },
{ value: "encounter", label: "Encounter" },
{ value: "appointment", label: "Appointment" },
{ value: "location_association", label: "Location Association" },
] as const;

export default function QuestionnaireEditor({ id }: QuestionnaireEditorProps) {
const navigate = useNavigate();
const [activeTab, setActiveTab] = useState<"edit" | "preview">("edit");
Expand Down Expand Up @@ -832,19 +843,11 @@ function QuestionEditor({
<SelectValue placeholder="Select structured type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="allergy_intolerance">
Allergy Intolerance
</SelectItem>
<SelectItem value="medication_request">
Medication Request
</SelectItem>
<SelectItem value="medication_statement">
Medication Statement
</SelectItem>
<SelectItem value="symptom">Symptom</SelectItem>
<SelectItem value="diagnosis">Diagnosis</SelectItem>
<SelectItem value="encounter">Encounter</SelectItem>
<SelectItem value="appointment">Appointment</SelectItem>
{STRUCTURED_QUESTION_TYPES.map((type) => (
<SelectItem key={type.value} value={type.value}>
{type.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
Expand Down
7 changes: 6 additions & 1 deletion src/components/Questionnaire/QuestionnaireForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -262,13 +262,18 @@ export function QuestionnaireForm({
// Continue with existing submission logic...
const requests: BatchRequest[] = [];
if (encounterId && patientId) {
const context = { patientId, encounterId };
const context = { facilityId, patientId, encounterId };
// First, collect all structured data requests if encounterId is provided
formsWithValidation.forEach((form) => {
form.responses.forEach((response) => {
if (response.structured_type) {
console.log(
"Processing structured response",
response.structured_type,
);
const structuredData = response.values?.[0]?.value;
if (Array.isArray(structuredData) && structuredData.length > 0) {
console.log("Structured data found", structuredData);
const structuredRequests = getStructuredRequests(
response.structured_type,
structuredData,
Expand Down
1 change: 1 addition & 0 deletions src/components/Questionnaire/QuestionnaireSearch.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export function QuestionnaireSearch({
...conditionalAttribute(!!subjectType, {
subject_type: subjectType,
}),
status: "active",
},
}),
});
Expand Down
Loading
Loading