From 306b6e2ad13fe3e42ec2520320fe97553460ad95 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Wed, 8 May 2024 06:18:36 +0530 Subject: [PATCH 1/8] Added facility hubs --- src/Components/Facility/FacilityCreate.tsx | 63 ++++++++++++++++++++++ src/Components/Facility/models.tsx | 12 ++++- src/Locale/en/Common.json | 5 +- src/Redux/api.tsx | 35 ++++++++++++ 4 files changed, 112 insertions(+), 3 deletions(-) diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index 3138f374e4b..9fe5bc548ee 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -156,6 +156,7 @@ export const FacilityCreate = (props: FacilityProps) => { const [stateId, setStateId] = useState(); const [districtId, setDistrictId] = useState(); const [localBodyId, setLocalBodyId] = useState(); + const [hubFacilities, setHubFacilities] = useState([]); const { goBack } = useAppHistory(); const headerText = !facilityId ? "Create Facility" : "Update Facility"; const buttonText = !facilityId ? "Save Facility" : "Update Facility"; @@ -243,6 +244,34 @@ export const FacilityCreate = (props: FacilityProps) => { }, ); + const facilitiesQuery = useQuery(routes.getAllFacilities); + const hubsQuery = useQuery(routes.getFacilityHubs, { + pathParams: { + id: facilityId!, + }, + prefetch: !!facilityId, + onResponse: (res) => { + setHubFacilities(res.data?.results.map((d) => d.hub.id) as string[]); + }, + }); + const createHub = async (hubFacilityId: string) => + await request(routes.createFacilityHub, { + body: { + hub_id: hubFacilityId, + }, + pathParams: { + id: facilityId || "", + }, + }); + const deleteHub = async (hubFacilityId: string) => + await request(routes.deleteFacilityHub, { + pathParams: { + id: facilityId || "", + hub_id: hubFacilityId, + }, + }); + const { data: facilities } = facilitiesQuery; + useQuery(routes.getPermittedFacility, { pathParams: { id: facilityId!, @@ -844,6 +873,40 @@ export const FacilityCreate = (props: FacilityProps) => { required types={["mobile", "landline"]} /> + f.id !== facilityId) || + [] + } + optionLabel={(o) => o.name} + optionValue={(o) => o.id} + value={hubFacilities} + onChange={async (event) => { + if (event.value.length > hubFacilities.length) { + await createHub( + event.value[event.value.length - 1] || "", + ); + } else if (event.value.length < hubFacilities.length) { + console.log( + hubFacilities.find((x) => !event.value.includes(x)), + ); + await deleteHub( + hubsQuery.data?.results.find( + (r) => + r.hub.id === + (hubFacilities.find( + (x) => !event.value.includes(x), + ) || ""), + )?.external_id || "", + ); + } + setHubFacilities(event.value as string[]); + }} + />
>(), }, + getFacilityHubs: { + path: "/api/v1/facility/{id}/hubs/", + method: "GET", + TRes: Type>(), + }, + + updateFacilityHubs: { + path: "/api/v1/facility/{id}/hubs/{hub_id}/", + method: "PATCH", + TRes: Type(), + TBody: Type(), + }, + + getFacilityHub: { + path: "/api/v1/facility/{id}/hubs/{hub_id}/", + method: "GET", + TRes: Type(), + }, + + createFacilityHub: { + path: "/api/v1/facility/{id}/hubs/", + method: "POST", + TRes: Type(), + TBody: Type< + Partial> & { hub_id: string } + >(), + }, + + deleteFacilityHub: { + path: "/api/v1/facility/{id}/hubs/{hub_id}/", + method: "DELETE", + TRes: Type>(), + }, + deleteFacilityCoverImage: { path: "/api/v1/facility/{id}/cover_image/", method: "DELETE", From c9f46e3753ad83ebee9691f8ba89ef5384e72a64 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Wed, 8 May 2024 06:24:54 +0530 Subject: [PATCH 2/8] fixed create --- src/Components/Facility/FacilityCreate.tsx | 69 +++++++++++----------- 1 file changed, 36 insertions(+), 33 deletions(-) diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index 9fe5bc548ee..67810bbc56c 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -873,40 +873,43 @@ export const FacilityCreate = (props: FacilityProps) => { required types={["mobile", "landline"]} /> - f.id !== facilityId) || - [] - } - optionLabel={(o) => o.name} - optionValue={(o) => o.id} - value={hubFacilities} - onChange={async (event) => { - if (event.value.length > hubFacilities.length) { - await createHub( - event.value[event.value.length - 1] || "", - ); - } else if (event.value.length < hubFacilities.length) { - console.log( - hubFacilities.find((x) => !event.value.includes(x)), - ); - await deleteHub( - hubsQuery.data?.results.find( - (r) => - r.hub.id === - (hubFacilities.find( - (x) => !event.value.includes(x), - ) || ""), - )?.external_id || "", - ); + {facilityId && ( + f.id !== facilityId, + ) || [] } - setHubFacilities(event.value as string[]); - }} - /> + optionLabel={(o) => o.name} + optionValue={(o) => o.id} + value={hubFacilities} + onChange={async (event) => { + if (event.value.length > hubFacilities.length) { + await createHub( + event.value[event.value.length - 1] || "", + ); + } else if (event.value.length < hubFacilities.length) { + console.log( + hubFacilities.find((x) => !event.value.includes(x)), + ); + await deleteHub( + hubsQuery.data?.results.find( + (r) => + r.hub.id === + (hubFacilities.find( + (x) => !event.value.includes(x), + ) || ""), + )?.external_id || "", + ); + } + setHubFacilities(event.value as string[]); + }} + /> + )}
Date: Sat, 11 May 2024 03:37:03 +0530 Subject: [PATCH 3/8] changes made according to backend --- src/Components/Facility/FacilityCreate.tsx | 30 ++++++++++++++-------- src/Components/Facility/models.tsx | 13 +++++++--- src/Redux/api.tsx | 4 +-- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index 67810bbc56c..be516a3086f 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -251,17 +251,24 @@ export const FacilityCreate = (props: FacilityProps) => { }, prefetch: !!facilityId, onResponse: (res) => { - setHubFacilities(res.data?.results.map((d) => d.hub.id) as string[]); + setHubFacilities( + res.data?.results.map((d) => d.hub_object.id) as string[], + ); }, }); const createHub = async (hubFacilityId: string) => await request(routes.createFacilityHub, { body: { - hub_id: hubFacilityId, + hub: hubFacilityId, }, pathParams: { id: facilityId || "", }, + onResponse: ({ res }) => { + if (res?.ok) { + hubsQuery.refetch(); + } + }, }); const deleteHub = async (hubFacilityId: string) => await request(routes.deleteFacilityHub, { @@ -269,7 +276,13 @@ export const FacilityCreate = (props: FacilityProps) => { id: facilityId || "", hub_id: hubFacilityId, }, + onResponse: ({ res }) => { + if (res?.ok) { + hubsQuery.refetch(); + } + }, }); + const { data: facilities } = facilitiesQuery; useQuery(routes.getPermittedFacility, { @@ -889,21 +902,16 @@ export const FacilityCreate = (props: FacilityProps) => { value={hubFacilities} onChange={async (event) => { if (event.value.length > hubFacilities.length) { - await createHub( - event.value[event.value.length - 1] || "", - ); + createHub(event.value[event.value.length - 1] || ""); } else if (event.value.length < hubFacilities.length) { - console.log( - hubFacilities.find((x) => !event.value.includes(x)), - ); - await deleteHub( + deleteHub( hubsQuery.data?.results.find( (r) => - r.hub.id === + r.hub_object.id === (hubFacilities.find( (x) => !event.value.includes(x), ) || ""), - )?.external_id || "", + )?.id || "", ); } setHubFacilities(event.value as string[]); diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index dd238e161a8..f78cf43602b 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -78,11 +78,16 @@ export interface FacilityModel { pincode?: string; } +export enum HubRelationship { + TELE_ICU_HUB = 1, +} + export interface FacilityHubModel { - external_id: string; - hub: FacilityNameModel; - spoke: FacilityNameModel; - relationship: unknown; + id: string; + hub?: string; + hub_object: FacilityNameModel; + spoke_object: FacilityNameModel; + relationship: HubRelationship; } export interface CapacityModal { id?: number; diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index f523ca6f856..54f0af76135 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -403,9 +403,7 @@ const routes = { path: "/api/v1/facility/{id}/hubs/", method: "POST", TRes: Type(), - TBody: Type< - Partial> & { hub_id: string } - >(), + TBody: Type>(), }, deleteFacilityHub: { From a947c6a961e7a8b9716f16510332acac39615426 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 26 Aug 2024 10:43:32 +0530 Subject: [PATCH 4/8] updates --- src/Components/Facility/models.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index 76cb5cf3271..f3d8b16ea26 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -83,7 +83,8 @@ export interface FacilityModel { } export enum HubRelationship { - TELE_ICU_HUB = 1, + REGULAR_HUB = 1, + TELE_ICU_HUB = 2, } export interface FacilityHubModel { From 4bf57084ae550d20bf607732fe7bb71bba6383dc Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Fri, 30 Aug 2024 06:22:46 +0530 Subject: [PATCH 5/8] Updated as per changes requested --- src/Common/constants.tsx | 16 +- src/Components/Common/FacilitySelect.tsx | 6 + src/Components/Facility/FacilityBlock.tsx | 29 ++++ src/Components/Facility/FacilityCreate.tsx | 85 ++-------- .../Facility/SpokeFacilityEditor.tsx | 152 ++++++++++++++++++ src/Components/Facility/models.tsx | 32 ++-- src/Components/Form/AutoCompleteAsync.tsx | 6 +- src/Components/Form/ModelCrudEditor.tsx | 147 +++++++++++++++++ src/Locale/en/Common.json | 1 + src/Redux/api.tsx | 37 ++--- 10 files changed, 403 insertions(+), 108 deletions(-) create mode 100644 src/Components/Facility/FacilityBlock.tsx create mode 100644 src/Components/Facility/SpokeFacilityEditor.tsx create mode 100644 src/Components/Form/ModelCrudEditor.tsx diff --git a/src/Common/constants.tsx b/src/Common/constants.tsx index 6e5a9e33e4e..5a9686469f2 100644 --- a/src/Common/constants.tsx +++ b/src/Common/constants.tsx @@ -1,5 +1,8 @@ import { IConfig } from "./hooks/useConfig"; -import { PatientCategory } from "../Components/Facility/models"; +import { + PatientCategory, + SpokeRelationship, +} from "../Components/Facility/models"; import { SortOption } from "../Components/Common/SortDropdown"; import { dateQueryString } from "../Utils/utils"; import { IconName } from "../CAREUI/icons/CareIcon"; @@ -1466,6 +1469,17 @@ export const DEFAULT_ALLOWED_EXTENSIONS = [ "application/vnd.oasis.opendocument.spreadsheet,application/pdf", ]; +export const SPOKE_RELATION_TYPES = [ + { + text: "Regular", + value: SpokeRelationship.REGULAR, + }, + { + text: "Tele ICU", + value: SpokeRelationship.TELE_ICU, + }, +]; + export const HumanBodyPaths = { anterior: [ { diff --git a/src/Components/Common/FacilitySelect.tsx b/src/Components/Common/FacilitySelect.tsx index d718ef3e781..d91b9a7f8fc 100644 --- a/src/Components/Common/FacilitySelect.tsx +++ b/src/Components/Common/FacilitySelect.tsx @@ -23,6 +23,8 @@ interface FacilitySelectProps { selected?: FacilityModel | FacilityModel[] | null; setSelected: (selected: FacilityModel | FacilityModel[] | null) => void; allowNone?: boolean; + placeholder?: string; + filter?: (facilities: FacilityModel) => boolean; } export const FacilitySelect = (props: FacilitySelectProps) => { @@ -44,6 +46,8 @@ export const FacilitySelect = (props: FacilitySelectProps) => { allowNone = false, freeText = false, errors = "", + placeholder, + filter, } = props; const facilitySearch = useCallback( @@ -82,6 +86,7 @@ export const FacilitySelect = (props: FacilitySelectProps) => { return ( { compareBy="id" className={className} error={errors} + filter={filter} /> ); }; diff --git a/src/Components/Facility/FacilityBlock.tsx b/src/Components/Facility/FacilityBlock.tsx new file mode 100644 index 00000000000..d66bd2f8ec1 --- /dev/null +++ b/src/Components/Facility/FacilityBlock.tsx @@ -0,0 +1,29 @@ +import CareIcon from "../../CAREUI/icons/CareIcon"; +import { FacilityModel } from "./models"; + +export default function FacilityBlock(props: { facility: FacilityModel }) { + const { facility } = props; + + return ( +
+
+ {facility.read_cover_image_url ? ( + + ) : ( + <> + + + )} +
+
+ {facility.name} +

+ {facility.address} {facility.local_body_object?.name} +

+
+
+ ); +} diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index a47013b6954..fc03d8b7e8c 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -63,6 +63,7 @@ import routes from "../../Redux/api.js"; import useQuery from "../../Utils/request/useQuery.js"; import { RequestResult } from "../../Utils/request/types.js"; import useAuthUser from "../../Common/hooks/useAuthUser"; +import SpokeFacilityEditor from "./SpokeFacilityEditor.js"; const Loading = lazy(() => import("../Common/Loading")); @@ -161,7 +162,6 @@ export const FacilityCreate = (props: FacilityProps) => { const [stateId, setStateId] = useState(); const [districtId, setDistrictId] = useState(); const [localBodyId, setLocalBodyId] = useState(); - const [hubFacilities, setHubFacilities] = useState([]); const { goBack } = useAppHistory(); const headerText = !facilityId ? "Create Facility" : "Update Facility"; const buttonText = !facilityId ? "Save Facility" : "Update Facility"; @@ -249,48 +249,7 @@ export const FacilityCreate = (props: FacilityProps) => { }, ); - const facilitiesQuery = useQuery(routes.getAllFacilities); - const hubsQuery = useQuery(routes.getFacilityHubs, { - pathParams: { - id: facilityId!, - }, - prefetch: !!facilityId, - onResponse: (res) => { - setHubFacilities( - res.data?.results.map((d) => d.hub_object.id) as string[], - ); - }, - }); - const createHub = async (hubFacilityId: string) => - await request(routes.createFacilityHub, { - body: { - hub: hubFacilityId, - }, - pathParams: { - id: facilityId || "", - }, - onResponse: ({ res }) => { - if (res?.ok) { - hubsQuery.refetch(); - } - }, - }); - const deleteHub = async (hubFacilityId: string) => - await request(routes.deleteFacilityHub, { - pathParams: { - id: facilityId || "", - hub_id: hubFacilityId, - }, - onResponse: ({ res }) => { - if (res?.ok) { - hubsQuery.refetch(); - } - }, - }); - - const { data: facilities } = facilitiesQuery; - - useQuery(routes.getPermittedFacility, { + const facilityQuery = useQuery(routes.getPermittedFacility, { pathParams: { id: facilityId!, }, @@ -891,38 +850,14 @@ export const FacilityCreate = (props: FacilityProps) => { required types={["mobile", "landline"]} /> - {facilityId && ( - f.id !== facilityId, - ) || [] - } - optionLabel={(o) => o.name} - optionValue={(o) => o.id} - value={hubFacilities} - onChange={async (event) => { - if (event.value.length > hubFacilities.length) { - createHub(event.value[event.value.length - 1] || ""); - } else if (event.value.length < hubFacilities.length) { - deleteHub( - hubsQuery.data?.results.find( - (r) => - r.hub_object.id === - (hubFacilities.find( - (x) => !event.value.includes(x), - ) || ""), - )?.id || "", - ); - } - setHubFacilities(event.value as string[]); - }} - /> - )} +
+

Spoke Facilities

+ {facilityId && ( + + )} +
& { id: string }; +} + +export default function SpokeFacilityEditor(props: SpokeFacilityEditorProps) { + const { facility } = props; + + //const { t } = useTranslation(); + + const spokesQuery = useQuery(routes.getFacilitySpokes, { + pathParams: { + id: facility.id, + }, + }); + + const spokes = spokesQuery.data?.results; + + const createSpoke = (body: FacilitySpokeRequest) => + request(routes.createFacilitySpoke, { + body, + pathParams: { + id: facility.id, + }, + onResponse: ({ res }) => { + if (res?.ok) { + spokesQuery.refetch(); + } + }, + }); + + const deleteSpoke = (spokeFacilityId: string) => + request(routes.deleteFacilitySpoke, { + pathParams: { + id: facility.id, + spoke_id: spokeFacilityId, + }, + onResponse: ({ res }) => { + if (res?.ok) { + spokesQuery.refetch(); + } + }, + }); + + const updateSpoke = (spokeFacilityId: string, body: FacilitySpokeRequest) => + request(routes.updateFacilitySpokes, { + pathParams: { + id: facility.id, + spoke_id: spokeFacilityId, + }, + body, + onResponse: ({ res }) => { + if (res?.ok) { + spokesQuery.refetch(); + } + }, + }); + + const FormRender = ( + item: FacilitySpokeModel | FacilitySpokeRequest, + setItem: (item: FacilitySpokeModel | FacilitySpokeRequest) => void, + processing: boolean, + ) => { + const [selectedFacility, setSelectedFacility] = useState(); + + useEffect(() => { + setItem({ ...item, spoke: selectedFacility?.id }); + }, [selectedFacility]); + + return ( + <> + {"id" in item ? ( +
+ +
+ ) : ( + + v && !Array.isArray(v) && setSelectedFacility(v) + } + errors="" + className="w-full" + disabled={processing} + filter={(f) => + !!f.id && + !spokes?.flatMap((s) => s.spoke_object.id).includes(f.id) + } + /> + )} + v.text} + optionValue={(v) => v.value} + value={item.relationship} + onChange={(v) => setItem({ ...item, relationship: v.value })} + errorClassName="hidden" + className="shrink-0" + disabled={processing} + /> + + ); + }; + + return ( + <> + + items={spokes} + onCreate={createSpoke} + onUpdate={updateSpoke} + onDelete={deleteSpoke} + loading={spokesQuery.loading} + errors={{}} + emptyText={"No Spokes"} + empty={{ + spoke: "", + relationship: SpokeRelationship.REGULAR, + }} + createText="Add Spoke" + > + {FormRender} + + + ); +} diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index e93e196ef21..c0179334e89 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -80,20 +80,32 @@ export interface FacilityModel { local_body?: number; ward?: number; pincode?: string; + latitude?: string; + longitude?: string; + kasp_empanelled?: boolean; + patient_count?: string; + bed_count?: string; } -export enum HubRelationship { - REGULAR_HUB = 1, - TELE_ICU_HUB = 2, +export enum SpokeRelationship { + REGULAR = 1, + TELE_ICU = 2, } -export interface FacilityHubModel { +export interface FacilitySpokeModel { id: string; - hub?: string; hub_object: FacilityNameModel; spoke_object: FacilityNameModel; - relationship: HubRelationship; + relationship: SpokeRelationship; +} + +export interface FacilitySpokeRequest { + spoke?: string; + relationship?: SpokeRelationship; } + +export interface FacilitySpokeErrors {} + export interface CapacityModal { id?: number; room_type?: number; @@ -572,13 +584,7 @@ export type IUserFacilityRequest = { facility: string; }; -export type FacilityRequest = Omit & { - latitude?: string; - longitude?: string; - kasp_empanelled?: boolean; - patient_count?: string; - bed_count?: string; -}; +export type FacilityRequest = Omit; export type InventorySummaryResponse = { id: string; diff --git a/src/Components/Form/AutoCompleteAsync.tsx b/src/Components/Form/AutoCompleteAsync.tsx index 9b4c1f876e4..66fbdab27bc 100644 --- a/src/Components/Form/AutoCompleteAsync.tsx +++ b/src/Components/Form/AutoCompleteAsync.tsx @@ -35,6 +35,7 @@ interface Props { required?: boolean; onBlur?: () => void; onFocus?: () => void; + filter?: (data: any) => boolean; } const AutoCompleteAsync = (props: Props) => { @@ -55,6 +56,7 @@ const AutoCompleteAsync = (props: Props) => { disabled = false, required = false, error, + filter, } = props; const [data, setData] = useState([]); const [query, setQuery] = useState(""); @@ -68,7 +70,9 @@ const AutoCompleteAsync = (props: Props) => { () => debounce(async (query: string) => { setLoading(true); - const data = (await fetchData(query)) || []; + const data = ((await fetchData(query)) || [])?.filter((d: any) => + filter ? filter(d) : true, + ); if (showNOptions !== undefined) { setData(data.slice(0, showNOptions)); diff --git a/src/Components/Form/ModelCrudEditor.tsx b/src/Components/Form/ModelCrudEditor.tsx new file mode 100644 index 00000000000..aec69d1c464 --- /dev/null +++ b/src/Components/Form/ModelCrudEditor.tsx @@ -0,0 +1,147 @@ +import { useEffect, useState } from "react"; +import { classNames } from "../../Utils/utils"; +import ButtonV2 from "../Common/components/ButtonV2"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +interface Identifier { + id: string; +} + +export interface ModelCrudEditorProps { + onCreate?: (req: TReq) => Promise; + onUpdate?: (itemId: string, req: TReq) => Promise; + onDelete?: (itemId: string) => Promise; + items?: TRes[]; + children: ( + item: TRes | TReq, + setItem: (item: TRes | TReq) => void, + processing: boolean, + errors?: TErr, + ) => React.ReactNode; + loading: boolean; + errors: TErr; + emptyText?: React.ReactNode; + empty: TReq; + createText?: React.ReactNode; +} + +export default function ModelCrudEditor( + props: ModelCrudEditorProps, +) { + const { + onCreate, + onUpdate, + onDelete, + items, + children, + loading, + errors, + emptyText, + empty, + createText, + } = props; + const [creating, setCreating] = useState(false); + const [updating, setUpdating] = useState(null); + + const handleUpdate = async (itemId: string, item: TReq) => { + if (!onUpdate) return; + setUpdating(itemId); + await onUpdate(itemId, item); + setUpdating(null); + }; + + const handleDelete = async (itemId: string) => { + if (!onDelete) return; + setUpdating(itemId); + await onDelete(itemId); + setUpdating(null); + }; + + const handleCreate = async (item: TReq) => { + if (!onCreate) return; + setCreating(true); + await onCreate(item); + setCreating(false); + }; + + type FormProps = + | { + type: "creating"; + } + | { + type: "updating"; + item: TRes; + }; + + const Form = (props: FormProps) => { + const [item, setItem] = useState( + props.type === "creating" ? empty : props.item, + ); + const processing = + props.type === "creating" ? creating : props.item.id === updating; + + useEffect(() => { + if ( + props.type === "updating" && + JSON.stringify(item) !== JSON.stringify(props.item) + ) { + const timeout = setTimeout(() => { + handleUpdate(props.item.id, item as TReq); + }, 1000); + return () => clearTimeout(timeout); + } + }, [item]); + + return ( + <> + {children(item, setItem, processing, errors)} + {props.type === "creating" && ( + handleCreate(item as TReq)} + > + {createText || "Create"} + + )} + {props.type === "updating" && onDelete && ( + + )} + + ); + }; + + return ( +
+
    + {items?.map((item, i) => ( +
  • +
    +
  • + ))} + + {items?.length === 0 && ( +
    + {emptyText} +
    + )} +
+
+ +
+
+ ); +} diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index 7a85b767559..8b263366934 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -174,6 +174,7 @@ "report": "Report", "treating_doctor": "Treating Doctor", "hubs": "Hub Facilities", + "spokes": "Spoke Facilities", "ration_card__NO_CARD": "Non-card holder", "ration_card__BPL": "BPL", "ration_card__APL": "APL", diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 5541a51acbf..668a3fa9791 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -54,9 +54,10 @@ import { DistrictModel, DoctorModal, DupPatientModel, - FacilityHubModel, FacilityModel, FacilityRequest, + FacilitySpokeModel, + FacilitySpokeRequest, IFacilityNotificationRequest, IFacilityNotificationResponse, InventoryItemsModel, @@ -365,7 +366,7 @@ const routes = { getPermittedFacility: { path: "/api/v1/facility/{id}/", method: "GET", - TRes: Type(), + TRes: Type(), }, getAnyFacility: { @@ -388,34 +389,34 @@ const routes = { TBody: Type>(), }, - getFacilityHubs: { - path: "/api/v1/facility/{id}/hubs/", + getFacilitySpokes: { + path: "/api/v1/facility/{id}/spokes/", method: "GET", - TRes: Type>(), + TRes: Type>(), }, - updateFacilityHubs: { - path: "/api/v1/facility/{id}/hubs/{hub_id}/", + updateFacilitySpokes: { + path: "/api/v1/facility/{id}/spokes/{spoke_id}/", method: "PATCH", - TRes: Type(), - TBody: Type(), + TRes: Type(), + TBody: Type(), }, - getFacilityHub: { - path: "/api/v1/facility/{id}/hubs/{hub_id}/", + getFacilitySpoke: { + path: "/api/v1/facility/{id}/spokes/{spoke_id}/", method: "GET", - TRes: Type(), + TRes: Type(), }, - createFacilityHub: { - path: "/api/v1/facility/{id}/hubs/", + createFacilitySpoke: { + path: "/api/v1/facility/{id}/spokes/", method: "POST", - TRes: Type(), - TBody: Type>(), + TRes: Type(), + TBody: Type>(), }, - deleteFacilityHub: { - path: "/api/v1/facility/{id}/hubs/{hub_id}/", + deleteFacilitySpoke: { + path: "/api/v1/facility/{id}/spokes/{spoke_id}/", method: "DELETE", TRes: Type>(), }, From 31603bd1e05ee96e9d74b76ac8ff35c79d7f4f74 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Fri, 30 Aug 2024 23:44:30 +0530 Subject: [PATCH 6/8] updates --- src/Components/Form/ModelCrudEditor.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Components/Form/ModelCrudEditor.tsx b/src/Components/Form/ModelCrudEditor.tsx index aec69d1c464..9ab1b6958ce 100644 --- a/src/Components/Form/ModelCrudEditor.tsx +++ b/src/Components/Form/ModelCrudEditor.tsx @@ -67,6 +67,7 @@ export default function ModelCrudEditor( type FormProps = | { type: "creating"; + item: TReq; } | { type: "updating"; @@ -74,9 +75,7 @@ export default function ModelCrudEditor( }; const Form = (props: FormProps) => { - const [item, setItem] = useState( - props.type === "creating" ? empty : props.item, - ); + const [item, setItem] = useState(props.item); const processing = props.type === "creating" ? creating : props.item.id === updating; @@ -140,7 +139,7 @@ export default function ModelCrudEditor( )}
- +
); From 889e075ef0842a3b81b67fd206b114a713a70c44 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Sat, 31 Aug 2024 22:34:46 +0530 Subject: [PATCH 7/8] added create validations --- src/Components/Facility/SpokeFacilityEditor.tsx | 2 ++ src/Components/Form/ModelCrudEditor.tsx | 3 +++ 2 files changed, 5 insertions(+) diff --git a/src/Components/Facility/SpokeFacilityEditor.tsx b/src/Components/Facility/SpokeFacilityEditor.tsx index 27ea2d6ce9a..2af9da2b3d6 100644 --- a/src/Components/Facility/SpokeFacilityEditor.tsx +++ b/src/Components/Facility/SpokeFacilityEditor.tsx @@ -106,6 +106,7 @@ export default function SpokeFacilityEditor(props: SpokeFacilityEditorProps) { disabled={processing} filter={(f) => !!f.id && + facility.id !== f.id && !spokes?.flatMap((s) => s.spoke_object.id).includes(f.id) } /> @@ -144,6 +145,7 @@ export default function SpokeFacilityEditor(props: SpokeFacilityEditorProps) { relationship: SpokeRelationship.REGULAR, }} createText="Add Spoke" + allowCreate={(item) => !item.relationship || !item.spoke} > {FormRender} diff --git a/src/Components/Form/ModelCrudEditor.tsx b/src/Components/Form/ModelCrudEditor.tsx index 9ab1b6958ce..6ca2e70d0c9 100644 --- a/src/Components/Form/ModelCrudEditor.tsx +++ b/src/Components/Form/ModelCrudEditor.tsx @@ -23,6 +23,7 @@ export interface ModelCrudEditorProps { emptyText?: React.ReactNode; empty: TReq; createText?: React.ReactNode; + allowCreate?: (item: TReq) => boolean; } export default function ModelCrudEditor( @@ -39,6 +40,7 @@ export default function ModelCrudEditor( emptyText, empty, createText, + allowCreate, } = props; const [creating, setCreating] = useState(false); const [updating, setUpdating] = useState(null); @@ -100,6 +102,7 @@ export default function ModelCrudEditor( type="button" className="w-full py-3 md:w-auto" loading={creating} + disabled={allowCreate?.(item as TReq)} onClick={() => handleCreate(item as TReq)} > {createText || "Create"} From 8d580c5a78013e066ac5a6afe960d87c539edd05 Mon Sep 17 00:00:00 2001 From: Shivank Kacker Date: Mon, 2 Sep 2024 18:36:38 +0530 Subject: [PATCH 8/8] fixed responsiveness and added preview on facility home page --- src/Components/Facility/FacilityBlock.tsx | 11 +++++++--- src/Components/Facility/FacilityCreate.tsx | 2 +- src/Components/Facility/FacilityHome.tsx | 22 +++++++++++++++++++ .../Facility/SpokeFacilityEditor.tsx | 12 +++++----- src/Components/Form/ModelCrudEditor.tsx | 12 ++++++---- src/Locale/en/Common.json | 1 + 6 files changed, 46 insertions(+), 14 deletions(-) diff --git a/src/Components/Facility/FacilityBlock.tsx b/src/Components/Facility/FacilityBlock.tsx index d66bd2f8ec1..64c0a24d78d 100644 --- a/src/Components/Facility/FacilityBlock.tsx +++ b/src/Components/Facility/FacilityBlock.tsx @@ -1,3 +1,4 @@ +import { Link } from "raviger"; import CareIcon from "../../CAREUI/icons/CareIcon"; import { FacilityModel } from "./models"; @@ -5,8 +6,12 @@ export default function FacilityBlock(props: { facility: FacilityModel }) { const { facility } = props; return ( -
-
+ +
{facility.read_cover_image_url ? (
-
+ ); } diff --git a/src/Components/Facility/FacilityCreate.tsx b/src/Components/Facility/FacilityCreate.tsx index fc03d8b7e8c..678a356556d 100644 --- a/src/Components/Facility/FacilityCreate.tsx +++ b/src/Components/Facility/FacilityCreate.tsx @@ -851,7 +851,7 @@ export const FacilityCreate = (props: FacilityProps) => { types={["mobile", "landline"]} />
-

Spoke Facilities

+

{t("spokes")}

{facilityId && ( { }); }; + const spokesQuery = useQuery(routes.getFacilitySpokes, { + pathParams: { + id: facilityId, + }, + silent: true, + }); + if (isLoading) { return ; } @@ -293,6 +301,20 @@ export const FacilityHome = ({ facilityId }: Props) => { />
+ {!!spokesQuery.data?.results.length && ( +
+
+

+ {t("spokes")} +

+
+ {spokesQuery.data?.results.map((spoke) => ( + + ))} +
+
+
+ )}
diff --git a/src/Components/Facility/SpokeFacilityEditor.tsx b/src/Components/Facility/SpokeFacilityEditor.tsx index 2af9da2b3d6..197d68da2bf 100644 --- a/src/Components/Facility/SpokeFacilityEditor.tsx +++ b/src/Components/Facility/SpokeFacilityEditor.tsx @@ -1,4 +1,3 @@ -//import { useTranslation } from "react-i18next"; import routes from "../../Redux/api"; import request from "../../Utils/request/request"; import useQuery from "../../Utils/request/useQuery"; @@ -15,6 +14,7 @@ import { FacilitySelect } from "../Common/FacilitySelect"; import { useEffect, useState } from "react"; import { SPOKE_RELATION_TYPES } from "../../Common/constants"; import FacilityBlock from "./FacilityBlock"; +import { useTranslation } from "react-i18next"; export interface SpokeFacilityEditorProps { facility: Omit & { id: string }; @@ -23,7 +23,7 @@ export interface SpokeFacilityEditorProps { export default function SpokeFacilityEditor(props: SpokeFacilityEditorProps) { const { facility } = props; - //const { t } = useTranslation(); + const { t } = useTranslation(); const spokesQuery = useQuery(routes.getFacilitySpokes, { pathParams: { @@ -85,7 +85,7 @@ export default function SpokeFacilityEditor(props: SpokeFacilityEditorProps) { }, [selectedFacility]); return ( - <> +
{"id" in item ? (
@@ -94,7 +94,7 @@ export default function SpokeFacilityEditor(props: SpokeFacilityEditorProps) { setItem({ ...item, relationship: v.value })} errorClassName="hidden" - className="shrink-0" + className="w-full shrink-0 md:w-auto" disabled={processing} /> - +
); }; diff --git a/src/Components/Form/ModelCrudEditor.tsx b/src/Components/Form/ModelCrudEditor.tsx index 6ca2e70d0c9..3ce4b15eba3 100644 --- a/src/Components/Form/ModelCrudEditor.tsx +++ b/src/Components/Form/ModelCrudEditor.tsx @@ -2,6 +2,7 @@ import { useEffect, useState } from "react"; import { classNames } from "../../Utils/utils"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import { useTranslation } from "react-i18next"; interface Identifier { id: string; @@ -29,6 +30,8 @@ export interface ModelCrudEditorProps { export default function ModelCrudEditor( props: ModelCrudEditorProps, ) { + const { t } = useTranslation(); + const { onCreate, onUpdate, @@ -94,7 +97,7 @@ export default function ModelCrudEditor( }, [item]); return ( - <> +
{children(item, setItem, processing, errors)} {props.type === "creating" && ( ( )} - +
); }; diff --git a/src/Locale/en/Common.json b/src/Locale/en/Common.json index 8b263366934..3c8ed6258f2 100644 --- a/src/Locale/en/Common.json +++ b/src/Locale/en/Common.json @@ -175,6 +175,7 @@ "treating_doctor": "Treating Doctor", "hubs": "Hub Facilities", "spokes": "Spoke Facilities", + "add_spoke" : "Add Spoke Facility", "ration_card__NO_CARD": "Non-card holder", "ration_card__BPL": "BPL", "ration_card__APL": "APL",