-
Notifications
You must be signed in to change notification settings - Fork 537
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
Changes from 2 commits
5240247
a1821e3
bc23521
e96f204
31dfe8b
9f08025
fe386ac
cb75dd9
e3ae50b
d3a677f
d97a09e
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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> | ||
); | ||
} |
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[]) || []; | ||
|
||
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> | ||
); | ||
} |
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -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"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
@@ -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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||
return null; | ||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add type assertion safety check.
The type assertion to
LocationAssociationQuestion[]
could fail at runtime. Add a type guard for safety.📝 Committable suggestion