From 81a6257b8a6a7ece993206080cb3e5103fd4b5e3 Mon Sep 17 00:00:00 2001 From: Gigin George Date: Thu, 30 Jan 2025 22:33:16 +0530 Subject: [PATCH] Refactor Facility Settings; Location Organizations --- public/locale/en.json | 4 + ...nizations.tsx => LinkDepartmentsSheet.tsx} | 113 ++++++++++++++--- src/components/Patient/PatientInfoCard.tsx | 8 +- src/components/ui/sidebar/facility-nav.tsx | 2 +- src/pages/Facility/settings/layout.tsx | 3 +- .../settings/locations/LocationForm.tsx | 95 ++++++++------- .../settings/locations/LocationList.tsx | 115 ++++++++---------- .../settings/locations/LocationSheet.tsx | 2 +- .../settings/locations/LocationView.tsx | 93 ++++++++++---- src/types/location/location.ts | 28 ++++- src/types/location/locationApi.ts | 18 +++ 11 files changed, 325 insertions(+), 156 deletions(-) rename src/components/Patient/{ManageEncounterOrganizations.tsx => LinkDepartmentsSheet.tsx} (63%) diff --git a/public/locale/en.json b/public/locale/en.json index 12a37eabacb..b4baf2c5f19 100644 --- a/public/locale/en.json +++ b/public/locale/en.json @@ -296,6 +296,7 @@ "add_files": "Add Files", "add_insurance_details": "Add Insurance Details", "add_location": "Add Location", + "add_location_description": "Create a Location such as Rooms/Beds", "add_new_beds": "Add New Bed(s)", "add_new_facility": "Add New Facility", "add_new_patient": "Add New Patient", @@ -764,6 +765,8 @@ "edit_cover_photo": "Edit Cover Photo", "edit_facility": "Edit Facility", "edit_history": "Edit History", + "edit_location": "Edit Location", + "edit_location_description": "Edit the Location to make any changes", "edit_policy": "Edit Insurance Policy", "edit_policy_description": "Add or edit patient's insurance details", "edit_prescriptions": "Edit Prescriptions", @@ -1792,6 +1795,7 @@ "search_by": "Search by", "search_by_emergency_contact_phone_number": "Search by Emergency Contact Phone Number", "search_by_emergency_phone_number": "Search by Emergency Phone Number", + "search_by_name": "Search by Name", "search_by_patient_name": "Search by Patient Name", "search_by_patient_no": "Search by Patient Number", "search_by_phone_number": "Search by Phone Number", diff --git a/src/components/Patient/ManageEncounterOrganizations.tsx b/src/components/Patient/LinkDepartmentsSheet.tsx similarity index 63% rename from src/components/Patient/ManageEncounterOrganizations.tsx rename to src/components/Patient/LinkDepartmentsSheet.tsx index e284180d8b9..12f2aa84853 100644 --- a/src/components/Patient/ManageEncounterOrganizations.tsx +++ b/src/components/Patient/LinkDepartmentsSheet.tsx @@ -17,17 +17,77 @@ import { import routes from "@/Utils/request/api"; import mutate from "@/Utils/request/mutate"; import FacilityOrganizationSelector from "@/pages/Facility/settings/organizations/components/FacilityOrganizationSelector"; -import { Encounter } from "@/types/emr/encounter"; +import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; +import locationApi from "@/types/location/locationApi"; interface Props { - encounter: Encounter; + entityType: "encounter" | "location"; + entityId: string; + currentOrganizations: FacilityOrganization[]; facilityId: string; trigger?: React.ReactNode; onUpdate?: () => void; } -export default function ManageEncounterOrganizations({ - encounter, +type MutationRoute = + | typeof routes.encounter.addOrganization + | typeof routes.encounter.removeOrganization + | typeof locationApi.addOrganization + | typeof locationApi.removeOrganization; + +interface EncounterPathParams { + encounterId: string; +} + +interface LocationPathParams { + facility_id: string; + id: string; +} + +type PathParams = EncounterPathParams | LocationPathParams; + +interface MutationParams { + route: MutationRoute; + pathParams: PathParams; + queryKey: string[]; +} + +function getMutationParams( + entityType: "encounter" | "location", + entityId: string, + facilityId: string, + isAdd: boolean, +): MutationParams { + if (entityType === "encounter") { + return { + route: isAdd + ? routes.encounter.addOrganization + : routes.encounter.removeOrganization, + pathParams: { encounterId: entityId } as EncounterPathParams, + queryKey: ["encounter", entityId], + }; + } + return { + route: isAdd ? locationApi.addOrganization : locationApi.removeOrganization, + pathParams: { facility_id: facilityId, id: entityId } as LocationPathParams, + queryKey: ["location", entityId], + }; +} + +function getInvalidateQueries( + entityType: "encounter" | "location", + entityId: string, +) { + if (entityType === "encounter") { + return ["encounter", entityId]; + } + return ["location", entityId, "organizations"]; +} + +export default function LinkDepartmentsSheet({ + entityType, + entityId, + currentOrganizations, facilityId, trigger, onUpdate, @@ -37,13 +97,21 @@ export default function ManageEncounterOrganizations({ const queryClient = useQueryClient(); const { mutate: addOrganization, isPending: isAdding } = useMutation({ - mutationFn: (organizationId: string) => - mutate(routes.encounter.addOrganization, { - pathParams: { encounterId: encounter.id }, + mutationFn: (organizationId: string) => { + const { route, pathParams } = getMutationParams( + entityType, + entityId, + facilityId, + true, + ); + return mutate(route, { + pathParams, body: { organization: organizationId }, - })({ organization: organizationId }), + })({ organization: organizationId }); + }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["encounter", encounter.id] }); + const invalidateQueries = getInvalidateQueries(entityType, entityId); + queryClient.invalidateQueries({ queryKey: invalidateQueries }); toast.success("Organization added successfully"); setSelectedOrg(""); setOpen(false); @@ -58,13 +126,26 @@ export default function ManageEncounterOrganizations({ }); const { mutate: removeOrganization, isPending: isRemoving } = useMutation({ - mutationFn: (organizationId: string) => - mutate(routes.encounter.removeOrganization, { - pathParams: { encounterId: encounter.id }, + mutationFn: (organizationId: string) => { + const { route, pathParams } = getMutationParams( + entityType, + entityId, + facilityId, + false, + ); + return mutate(route, { + pathParams, body: { organization: organizationId }, - })({ organization: organizationId }), + })({ organization: organizationId }); + }, onSuccess: () => { - queryClient.invalidateQueries({ queryKey: ["encounter", encounter.id] }); + const { queryKey } = getMutationParams( + entityType, + entityId, + facilityId, + false, + ); + queryClient.invalidateQueries({ queryKey }); toast.success("Organization removed successfully"); onUpdate?.(); }, @@ -118,7 +199,7 @@ export default function ManageEncounterOrganizations({ {t("current_organizations")}
- {encounter.organizations.map((org) => ( + {currentOrganizations.map((org) => (
))} - {encounter.organizations.length === 0 && ( + {currentOrganizations.length === 0 && (

{t("no_organizations_added_yet")}

diff --git a/src/components/Patient/PatientInfoCard.tsx b/src/components/Patient/PatientInfoCard.tsx index d76d6b6fda2..4ec59158b5f 100644 --- a/src/components/Patient/PatientInfoCard.tsx +++ b/src/components/Patient/PatientInfoCard.tsx @@ -39,7 +39,7 @@ import { formatDateTime, formatPatientAge } from "@/Utils/utils"; import { Encounter, completedEncounterStatus } from "@/types/emr/encounter"; import { Patient } from "@/types/emr/newPatient"; -import ManageEncounterOrganizations from "./ManageEncounterOrganizations"; +import LinkDepartmentsSheet from "./LinkDepartmentsSheet"; export interface PatientInfoCardProps { patient: Patient; @@ -269,8 +269,10 @@ export default function PatientInfoCard(props: PatientInfoCardProps) { )} - diff --git a/src/components/ui/sidebar/facility-nav.tsx b/src/components/ui/sidebar/facility-nav.tsx index 63facf3505b..281b86f4591 100644 --- a/src/components/ui/sidebar/facility-nav.tsx +++ b/src/components/ui/sidebar/facility-nav.tsx @@ -39,7 +39,7 @@ function generateFacilityLinks( { name: t("users"), url: `${baseUrl}/users`, icon: "d-people" }, { name: t("settings"), - url: `${baseUrl}/settings`, + url: `${baseUrl}/settings/general`, icon: "l-setting", }, ]; diff --git a/src/pages/Facility/settings/layout.tsx b/src/pages/Facility/settings/layout.tsx index 4ab54c1d9d2..9ba1d35ec39 100644 --- a/src/pages/Facility/settings/layout.tsx +++ b/src/pages/Facility/settings/layout.tsx @@ -16,7 +16,6 @@ interface SettingsLayoutProps { } const getRoutes = (facilityId: string) => ({ - "/": () => , "/general": () => , "/departments": () => , "/departments/:id": ({ id }: { id: string }) => ( @@ -72,7 +71,7 @@ export function SettingsLayout({ facilityId }: SettingsLayoutProps) { {tab.label} diff --git a/src/pages/Facility/settings/locations/LocationForm.tsx b/src/pages/Facility/settings/locations/LocationForm.tsx index 0ea8b973552..122e9adf323 100644 --- a/src/pages/Facility/settings/locations/LocationForm.tsx +++ b/src/pages/Facility/settings/locations/LocationForm.tsx @@ -1,5 +1,6 @@ import { zodResolver } from "@hookform/resolvers/zod"; -import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; @@ -25,11 +26,12 @@ import { import { Textarea } from "@/components/ui/textarea"; import mutate from "@/Utils/request/mutate"; +import query from "@/Utils/request/query"; import { - LocationList, LocationWrite, OperationalStatus, Status, + locationFormOptions, } from "@/types/location/location"; import locationApi from "@/types/location/locationApi"; @@ -58,6 +60,7 @@ const formSchema = z.object({ mode: z.enum(["instance", "kind"] as const), parent: z.string().optional().nullable(), organizations: z.array(z.string()).default([]), + availability_status: z.enum(["available", "unavailable"] as const), }); type FormValues = z.infer; @@ -65,45 +68,63 @@ type FormValues = z.infer; interface Props { facilityId: string; onSuccess?: () => void; - location?: LocationList; + locationId?: string; parentId?: string; } +const defaultValues: FormValues = { + name: "", + description: "", + status: "active", + operational_status: "O", + form: "ro", + mode: "instance", + parent: null, + organizations: [], + availability_status: "available", +}; + export default function LocationForm({ facilityId, onSuccess, - location, + locationId, parentId, }: Props) { const { t } = useTranslation(); const queryClient = useQueryClient(); - // Initialize form with either edit values or create defaults + const { data: location, isLoading } = useQuery({ + queryKey: ["location", locationId], + queryFn: query(locationApi.get, { + pathParams: { facility_id: facilityId, id: locationId }, + }), + enabled: !!locationId, + }); + const form = useForm({ resolver: zodResolver(formSchema), - defaultValues: location - ? { - name: location.name, - description: location.description, - status: location.status, - operational_status: location.operational_status, - form: location.form, - mode: location.mode, - parent: parentId || null, - organizations: [], - } - : { - name: "", - description: "", - status: "active", - operational_status: "O", - form: "ro", - mode: "instance", - parent: parentId || null, - organizations: [], - }, + defaultValues: { + ...defaultValues, + parent: parentId || null, + }, }); + useEffect(() => { + if (location) { + form.reset({ + name: location.name, + description: location.description, + status: location.status, + operational_status: location.operational_status, + form: location.form, + mode: location.mode, + parent: parentId || null, + organizations: [], + availability_status: location.availability_status || "available", + }); + } + }, [location, form, parentId]); + const { mutate: submitForm, isPending } = useMutation({ mutationFn: location?.id ? mutate(locationApi.update, { @@ -137,24 +158,6 @@ export default function LocationForm({ submitForm(locationData); } - const locationFormOptions = [ - { value: "si", label: "Site" }, - { value: "bu", label: "Building" }, - { value: "wi", label: "Wing" }, - { value: "wa", label: "Ward" }, - { value: "lvl", label: "Level" }, - { value: "co", label: "Corridor" }, - { value: "ro", label: "Room" }, - { value: "bd", label: "Bed" }, - { value: "ve", label: "Vehicle" }, - { value: "ho", label: "House" }, - { value: "ca", label: "Cabinet" }, - { value: "rd", label: "Road" }, - { value: "area", label: "Area" }, - { value: "jdn", label: "Jurisdiction" }, - { value: "vi", label: "Virtual" }, - ]; - const statusOptions: { value: Status; label: string }[] = [ { value: "active", label: "Active" }, { value: "inactive", label: "Inactive" }, @@ -178,6 +181,10 @@ export default function LocationForm({ { value: "kind", label: "Kind" }, ]; + if (locationId && isLoading) { + return
Loading...
; + } + return (
diff --git a/src/pages/Facility/settings/locations/LocationList.tsx b/src/pages/Facility/settings/locations/LocationList.tsx index 41430c65ca4..5b20c8b2451 100644 --- a/src/pages/Facility/settings/locations/LocationList.tsx +++ b/src/pages/Facility/settings/locations/LocationList.tsx @@ -14,7 +14,10 @@ import Pagination from "@/components/Common/Pagination"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; import query from "@/Utils/request/query"; -import { LocationList as LocationListType } from "@/types/location/location"; +import { + LocationList as LocationListType, + getLocationFormLabel, +} from "@/types/location/location"; import locationApi from "@/types/location/locationApi"; import LocationSheet from "./LocationSheet"; @@ -93,69 +96,59 @@ export default function LocationList({ facilityId }: Props) { data.results.map((location: LocationListType) => ( -
-
-
-

- {location.name} -

-
- - {location.form.toUpperCase()} - - - {location.status} - - - {location.availability_status} - +
+
+
+
+
+

+ {location.name} +

+ +
+
+ + {getLocationFormLabel(location.form)} + + + {location.status} + + + {location.availability_status} + +
-
- - -
- {location.description && ( -

- {location.description} -

- )} - {location.has_children && ( -
- - {t("has_child_locations")} -
- )} +
+ +
diff --git a/src/pages/Facility/settings/locations/LocationSheet.tsx b/src/pages/Facility/settings/locations/LocationSheet.tsx index 547717ae93a..fdebda69cfa 100644 --- a/src/pages/Facility/settings/locations/LocationSheet.tsx +++ b/src/pages/Facility/settings/locations/LocationSheet.tsx @@ -45,7 +45,7 @@ export default function LocationSheet({
onOpenChange(false)} /> diff --git a/src/pages/Facility/settings/locations/LocationView.tsx b/src/pages/Facility/settings/locations/LocationView.tsx index b8003173383..52cffb87388 100644 --- a/src/pages/Facility/settings/locations/LocationView.tsx +++ b/src/pages/Facility/settings/locations/LocationView.tsx @@ -1,4 +1,4 @@ -import { useQuery } from "@tanstack/react-query"; +import { useQuery, useQueryClient } from "@tanstack/react-query"; import { Link } from "raviger"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -13,9 +13,10 @@ import { Input } from "@/components/ui/input"; import Page from "@/components/Common/Page"; import Pagination from "@/components/Common/Pagination"; import { CardGridSkeleton } from "@/components/Common/SkeletonLoading"; +import LinkDepartmentsSheet from "@/components/Patient/LinkDepartmentsSheet"; import query from "@/Utils/request/query"; -import { LocationList } from "@/types/location/location"; +import { LocationList, getLocationFormLabel } from "@/types/location/location"; import locationApi from "@/types/location/locationApi"; import LocationSheet from "./LocationSheet"; @@ -27,6 +28,7 @@ interface Props { export default function LocationView({ id, facilityId }: Props) { const { t } = useTranslation(); + const queryClient = useQueryClient(); const [page, setPage] = useState(1); const [searchQuery, setSearchQuery] = useState(""); @@ -43,6 +45,13 @@ export default function LocationView({ id, facilityId }: Props) { }), }); + const { data: locationOrganizations } = useQuery({ + queryKey: ["location", id, "organizations"], + queryFn: query(locationApi.getOrganizations, { + pathParams: { facility_id: facilityId, id }, + }), + }); + const { data: children, isLoading } = useQuery({ queryKey: [ "locations", @@ -77,36 +86,68 @@ export default function LocationView({ id, facilityId }: Props) { setSelectedLocation(null); }; + if (!location) + return ( +
+ +
+ ); + return (
-
-
-

{t("child_locations")}

- {location?.form.toUpperCase()} - - {location?.status} - - {location && "mode" in location && location.mode === "kind" && ( - - )} +
+
+
+

{t("locations")}

+ + {getLocationFormLabel(location?.form)} + + + {location?.status} + + {location && "mode" in location && location.mode === "kind" && ( + + )} +
+
+ { + setSearchQuery(e.target.value); + setPage(1); + }} + className="w-full" + /> +
-
- { - setSearchQuery(e.target.value); - setPage(1); + {locationOrganizations && ( + + + {t("manage_organizations")} + + } + onUpdate={() => { + queryClient.invalidateQueries({ + queryKey: ["location", facilityId, id], + }); }} - className="w-full" /> -
+ )}
{isLoading ? ( diff --git a/src/types/location/location.ts b/src/types/location/location.ts index 42b5ebfbe4e..cfb16dd13c3 100644 --- a/src/types/location/location.ts +++ b/src/types/location/location.ts @@ -1,3 +1,4 @@ +import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; import { Code } from "@/types/questionnaire/code"; export type AvailabilityStatus = "available" | "unavailable"; @@ -32,17 +33,18 @@ export interface LocationBase { description: string; location_type?: Code; form: LocationForm; + mode: LocationMode; + availability_status: AvailabilityStatus; } export interface LocationDetail extends LocationBase { id: string; + organizations: FacilityOrganization[]; } export interface LocationList extends LocationBase { id: string; - mode: LocationMode; has_children: boolean; - availability_status: AvailabilityStatus; } export interface LocationWrite extends LocationBase { @@ -51,3 +53,25 @@ export interface LocationWrite extends LocationBase { organizations: string[]; mode: LocationMode; } + +export const locationFormOptions = [ + { value: "si", label: "Site" }, + { value: "bu", label: "Building" }, + { value: "wi", label: "Wing" }, + { value: "wa", label: "Ward" }, + { value: "lvl", label: "Level" }, + { value: "co", label: "Corridor" }, + { value: "ro", label: "Room" }, + { value: "bd", label: "Bed" }, + { value: "ve", label: "Vehicle" }, + { value: "ho", label: "House" }, + { value: "ca", label: "Cabinet" }, + { value: "rd", label: "Road" }, + { value: "area", label: "Area" }, + { value: "jdn", label: "Jurisdiction" }, + { value: "vi", label: "Virtual" }, +]; + +export const getLocationFormLabel = (value: LocationForm) => { + return locationFormOptions.find((option) => option.value === value)?.label; +}; diff --git a/src/types/location/locationApi.ts b/src/types/location/locationApi.ts index a43f064c2d9..a36d35a8b77 100644 --- a/src/types/location/locationApi.ts +++ b/src/types/location/locationApi.ts @@ -1,5 +1,6 @@ import { HttpMethod, Type } from "@/Utils/request/api"; import { PaginatedResponse } from "@/Utils/request/types"; +import { FacilityOrganization } from "@/types/facilityOrganization/facilityOrganization"; import { LocationDetail, LocationList, LocationWrite } from "./location"; @@ -26,4 +27,21 @@ export default { TRes: Type(), TBody: Type(), }, + getOrganizations: { + path: "/api/v1/facility/{facility_id}/location/{id}/organizations", + method: HttpMethod.GET, + TRes: Type>(), + }, + addOrganization: { + path: "/api/v1/facility/{facility_id}/location/{id}/organizations_add/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type<{ organization: string }>(), + }, + removeOrganization: { + path: "/api/v1/facility/{facility_id}/location/{id}/organizations_remove/", + method: HttpMethod.POST, + TRes: Type(), + TBody: Type<{ organization: string }>(), + }, };