From 67463597abeaf8bb859088c4e38200e0809fd188 Mon Sep 17 00:00:00 2001 From: konavivekramakrishna <101407963+konavivekramakrishna@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:46:31 +0530 Subject: [PATCH 01/58] Replaced useDispatch w. useQuery/request: Resource (src/Components/Resource/**) (#6461) * replaced useDispatch in src/resource with useQuery and request * removed unused actions * fix loading * removed interface import * rm limit and offset * fixed useQuery Hooks * checking cypress failure * changed function name * removed external_id from models * fix naming * fix listfilter * fixed the condition in badgeList --- src/Components/Resource/BadgesList.tsx | 88 ++++------ src/Components/Resource/CommentSection.tsx | 152 ++++++++---------- src/Components/Resource/ListFilter.tsx | 87 ++++------ src/Components/Resource/ListView.tsx | 68 ++------ src/Components/Resource/ResourceBoard.tsx | 76 ++++----- src/Components/Resource/ResourceCreate.tsx | 41 ++--- src/Components/Resource/ResourceDetails.tsx | 50 ++---- .../Resource/ResourceDetailsUpdate.tsx | 96 ++++------- src/Components/Resource/models.ts | 41 +++++ src/Redux/actions.tsx | 21 --- src/Redux/api.tsx | 25 ++- 11 files changed, 305 insertions(+), 440 deletions(-) create mode 100644 src/Components/Resource/models.ts diff --git a/src/Components/Resource/BadgesList.tsx b/src/Components/Resource/BadgesList.tsx index 31b8f1c17ae..6977861d596 100644 --- a/src/Components/Resource/BadgesList.tsx +++ b/src/Components/Resource/BadgesList.tsx @@ -1,63 +1,25 @@ -import { useState, useEffect } from "react"; -import { getAnyFacility } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import { SHIFTING_FILTER_ORDER } from "../../Common/constants"; +import routes from "../../Redux/api"; +import useQuery from "../../Utils/request/useQuery"; + +function useFacilityQuery(facilityId: string | undefined) { + return useQuery(routes.getAnyFacility, { + pathParams: { id: String(facilityId) }, + prefetch: facilityId !== undefined, + }); +} export default function BadgesList(props: any) { const { appliedFilters, FilterBadges } = props; - const [orginFacilityName, setOrginFacilityName] = useState(""); - const [approvingFacilityName, setApprovingFacilityName] = useState(""); - const [assignedFacilityName, setAssignedFacilityName] = useState(""); - const dispatch: any = useDispatch(); - - useEffect(() => { - async function fetchData() { - if (!appliedFilters.origin_facility) return setOrginFacilityName(""); - const res = await dispatch( - getAnyFacility(appliedFilters.origin_facility, "origin_facility_name") - ); - setOrginFacilityName(res?.data?.name); - } - fetchData(); - }, [dispatch, appliedFilters.origin_facility]); - - useEffect(() => { - async function fetchData() { - if (!appliedFilters.approving_facility) - return setApprovingFacilityName(""); - const res = await dispatch( - getAnyFacility( - appliedFilters.approving_facility, - "approving_facility_name" - ) - ); - setApprovingFacilityName(res?.data?.name); - } - fetchData(); - }, [dispatch, appliedFilters.approving_facility]); - - useEffect(() => { - async function fetchData() { - if (!appliedFilters.assigned_facility) return setAssignedFacilityName(""); - const res = await dispatch( - getAnyFacility( - appliedFilters.assigned_facility, - "assigned_facility_name" - ) - ); - setAssignedFacilityName(res?.data?.name); - } - fetchData(); - }, [dispatch, appliedFilters.assigned_facility]); + const originFacility = useFacilityQuery(appliedFilters.origin_facility); + const approvingFacility = useFacilityQuery(appliedFilters.approving_facility); + const assignedFacility = useFacilityQuery(appliedFilters.assigned_facility); const getDescShiftingFilterOrder = (ordering: any) => { - let desc = ""; - SHIFTING_FILTER_ORDER.map((item: any) => { - if (item.text === ordering) { - desc = item.desc; - } - }); - return desc; + const foundItem = SHIFTING_FILTER_ORDER.find( + (item) => item.text === ordering + ); + return foundItem ? foundItem.desc : ""; }; return ( @@ -75,13 +37,25 @@ export default function BadgesList(props: any) { }), ...dateRange("Modified", "modified_date"), ...dateRange("Created", "created_date"), - value("Origin facility", "origin_facility", orginFacilityName), + value( + "Origin facility", + "origin_facility", + appliedFilters.origin_facility ? originFacility?.data?.name || "" : "" + ), value( "Approving facility", "approving_facility", - approvingFacilityName + appliedFilters.approving_facility + ? approvingFacility?.data?.name || "" + : "" + ), + value( + "Assigned facility", + "assigned_facility", + appliedFilters.assigned_facility + ? assignedFacility?.data?.name || "" + : "" ), - value("Assigned facility", "assigned_facility", assignedFacilityName), ]} /> ); diff --git a/src/Components/Resource/CommentSection.tsx b/src/Components/Resource/CommentSection.tsx index 01bd67454f5..25d8142dae7 100644 --- a/src/Components/Resource/CommentSection.tsx +++ b/src/Components/Resource/CommentSection.tsx @@ -1,60 +1,26 @@ -import { useCallback, useState } from "react"; -import { useDispatch } from "react-redux"; -import { statusType, useAbortableEffect } from "../../Common/utils"; -import { getResourceComments, addResourceComments } from "../../Redux/actions"; +import { useState } from "react"; import * as Notification from "../../Utils/Notifications.js"; -import Pagination from "../Common/Pagination"; import { formatDateTime } from "../../Utils/utils"; import CircularProgress from "../Common/components/CircularProgress"; import ButtonV2 from "../Common/components/ButtonV2"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import PaginatedList from "../../CAREUI/misc/PaginatedList"; +import { IComment } from "./models"; +import request from "../../Utils/request/request"; -interface CommentSectionProps { - id: string; -} -const CommentSection = (props: CommentSectionProps) => { - const dispatch: any = useDispatch(); - const initialData: any = []; - const [comments, setComments] = useState(initialData); +const CommentSection = (props: { id: string }) => { const [commentBox, setCommentBox] = useState(""); - const [isLoading, setIsLoading] = useState(true); - - const [currentPage, setCurrentPage] = useState(1); - const [totalCount, setTotalCount] = useState(0); - const [offset, setOffset] = useState(0); - const limit = 8; - - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; - setCurrentPage(page); - setOffset(offset); - }; - - const fetchData = useCallback( - async (status: statusType = { aborted: false }) => { - setIsLoading(true); - const res = await dispatch( - getResourceComments(props.id, { limit, offset }) - ); - if (!status.aborted) { - if (res && res.data) { - setComments(res.data?.results); - setTotalCount(res.data.count); - } - setIsLoading(false); - } - }, - [props.id, dispatch, offset] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] + const { loading, refetch: resourceRefetch } = useQuery( + routes.getResourceComments, + { + pathParams: { id: props.id }, + query: { limit: 8, offset: 0 }, + } ); - const onSubmitComment = () => { + const onSubmitComment = async () => { const payload = { comment: commentBox, }; @@ -64,13 +30,16 @@ const CommentSection = (props: CommentSectionProps) => { }); return; } - dispatch(addResourceComments(props.id, payload)).then((_: any) => { - Notification.Success({ msg: "Comment added successfully" }); - fetchData(); + const { res } = await request(routes.addResourceComments, { + pathParams: { id: props.id }, + body: payload, }); + if (res?.ok) { + Notification.Success({ msg: "Comment added successfully" }); + resourceRefetch(); + } setCommentBox(""); }; - return (
{ Post Your Comment
- {isLoading ? ( + {loading ? ( ) : ( - comments.map((comment: any) => ( -
-
-

{comment.comment}

-
-
- - {formatDateTime(comment.modified_date) || "-"} - -
-
-
- {comment.created_by_object?.first_name?.charAt(0) || "U"} + + {() => ( +
+ + No comments available + + + + + > + {(item) => } + +
+
- - {comment.created_by_object?.first_name || "Unknown"}{" "} - {comment.created_by_object?.last_name} -
-
- )) + )} + )}
- {totalCount > limit && ( -
- -
- )}
); }; export default CommentSection; + +export const Comment = ({ + comment, + created_by_object, + modified_date, +}: IComment) => ( +
+
+

{comment}

+
+
+ + {formatDateTime(modified_date) || "-"} + +
+
+
+ {created_by_object?.first_name?.charAt(0) || "U"} +
+ + {created_by_object?.first_name || "Unknown"}{" "} + {created_by_object?.last_name} + +
+
+); diff --git a/src/Components/Resource/ListFilter.tsx b/src/Components/Resource/ListFilter.tsx index 7a47732db64..afe48eedfd0 100644 --- a/src/Components/Resource/ListFilter.tsx +++ b/src/Components/Resource/ListFilter.tsx @@ -1,8 +1,5 @@ -import { useEffect, useState } from "react"; import { FacilitySelect } from "../Common/FacilitySelect"; import { RESOURCE_FILTER_ORDER } from "../../Common/constants"; -import { getAnyFacility } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import { RESOURCE_CHOICES } from "../../Common/constants"; import useMergeState from "../../Common/hooks/useMergeState"; import { navigate } from "raviger"; @@ -15,6 +12,8 @@ import { DateRange } from "../Common/DateRangeInputV2"; import DateRangeFormField from "../Form/FormFields/DateRangeFormField"; import dayjs from "dayjs"; import { dateQueryString } from "../../Utils/utils"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; const clearFilterState = { origin_facility: "", @@ -37,9 +36,6 @@ const getDate = (value: any) => export default function ListFilter(props: any) { const { filter, onChange, closeFilter } = props; - const [isOriginLoading, setOriginLoading] = useState(false); - const [isResourceLoading, setResourceLoading] = useState(false); - const [isAssignedLoading, setAssignedLoading] = useState(false); const [filterState, setFilterState] = useMergeState({ origin_facility: filter.origin_facility || "", origin_facility_ref: null, @@ -55,55 +51,42 @@ export default function ListFilter(props: any) { ordering: filter.ordering || null, status: filter.status || null, }); - const dispatch: any = useDispatch(); - useEffect(() => { - async function fetchData() { - if (filter.origin_facility) { - setOriginLoading(true); - const res = await dispatch( - getAnyFacility(filter.origin_facility, "origin_facility") - ); - if (res && res.data) { - setFilterState({ origin_facility_ref: res.data }); - } - setOriginLoading(false); + const { loading: orginFacilityLoading } = useQuery(routes.getAnyFacility, { + prefetch: filter.origin_facility !== undefined, + pathParams: { id: filter.origin_facility }, + onResponse: ({ res, data }) => { + if (res && data) { + setFilterState({ + origin_facility_ref: filter.origin_facility === "" ? "" : data, + }); } - } - fetchData(); - }, [dispatch]); + }, + }); - useEffect(() => { - async function fetchData() { - if (filter.approving_facility) { - setResourceLoading(true); - const res = await dispatch( - getAnyFacility(filter.approving_facility, "approving_facility") - ); - if (res && res.data) { - setFilterState({ approving_facility_ref: res.data }); - } - setResourceLoading(false); + const { loading: resourceFacilityLoading } = useQuery(routes.getAnyFacility, { + prefetch: filter.approving_facility !== undefined, + pathParams: { id: filter.approving_facility }, + onResponse: ({ res, data }) => { + if (res && data) { + setFilterState({ + approving_facility_ref: filter.approving_facility === "" ? "" : data, + }); } - } - fetchData(); - }, [dispatch]); + }, + }); - useEffect(() => { - async function fetchData() { - if (filter.assigned_facility) { - setAssignedLoading(true); - const res = await dispatch( - getAnyFacility(filter.assigned_facility, "assigned_facility") - ); - if (res && res.data) { - setFilterState({ assigned_facility_ref: res.data }); - } - setAssignedLoading(false); + const { loading: assignedFacilityLoading } = useQuery(routes.getAnyFacility, { + pathParams: { id: filter.assigned_facility }, + prefetch: filter.assigned_facility !== undefined, + onResponse: ({ res, data }) => { + if (res && data) { + setFilterState({ + assigned_facility_ref: filter.assigned_facility === "" ? "" : data, + }); } - } - fetchData(); - }, [dispatch]); + }, + }); const setFacility = (selected: any, name: string) => { setFilterState({ @@ -178,7 +161,7 @@ export default function ListFilter(props: any) {
Origin facility - {isOriginLoading ? ( + {orginFacilityLoading && filter.origin_facility ? ( ) : ( Resource approving facility - {isResourceLoading ? ( + {filter.approving_facility && resourceFacilityLoading ? ( ) : ( Assigned facility - {isAssignedLoading ? ( + {filter.approving_facility && assignedFacilityLoading ? ( ) : ( import("../Common/Loading")); const PageTitle = lazy(() => import("../Common/PageTitle")); export default function ListView() { - const dispatch: any = useDispatch(); const { qParams, Pagination, FilterBadges, advancedFilter, resultsPerPage } = useFilters({}); - const [data, setData] = useState([]); - const [totalCount, setTotalCount] = useState(0); - const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const onBoardViewBtnClick = () => navigate("/resource/board", { query: qParams }); const appliedFilters = formatFilter(qParams); - const refreshList = () => { - fetchData(); - }; - - const fetchData = () => { - setIsLoading(true); - dispatch( - listResourceRequests( - formatFilter({ - ...qParams, - offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, - }), - "resource-list-call" - ) - ).then((res: any) => { - if (res && res.data) { - setData(res.data.results); - setTotalCount(res.data.count); - } - setIsLoading(false); - }); - }; - - useEffect(() => { - fetchData(); - }, [ - qParams.status, - qParams.facility, - qParams.origin_facility, - qParams.approving_facility, - qParams.assigned_facility, - qParams.emergency, - qParams.created_date_before, - qParams.created_date_after, - qParams.modified_date_before, - qParams.modified_date_after, - qParams.ordering, - qParams.page, - ]); + const { loading, data, refetch } = useQuery(routes.listResourceRequests, { + query: formatFilter({ + ...qParams, + offset: (qParams.page ? qParams.page - 1 : 0) * resultsPerPage, + }), + }); const showResourceCardList = (data: any) => { if (data && !data.length) { @@ -218,14 +178,14 @@ export default function ListView() {
- {isLoading ? ( + {loading ? ( ) : (
- {showResourceCardList(data)} + {data?.results && showResourceCardList(data?.results)}
- +
)}
diff --git a/src/Components/Resource/ResourceBoard.tsx b/src/Components/Resource/ResourceBoard.tsx index 66bf559be0d..217f2941e10 100644 --- a/src/Components/Resource/ResourceBoard.tsx +++ b/src/Components/Resource/ResourceBoard.tsx @@ -1,15 +1,13 @@ import { useState, useEffect } from "react"; -import { useDispatch } from "react-redux"; -import { - listResourceRequests, - downloadResourceRequests, -} from "../../Redux/actions"; +import { downloadResourceRequests } from "../../Redux/actions"; import { navigate } from "raviger"; import { classNames } from "../../Utils/utils"; import { useDrag, useDrop } from "react-dnd"; import { formatDateTime } from "../../Utils/utils"; import { ExportButton } from "../Common/Export"; import dayjs from "../../Utils/dayjs"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; const limit = 14; @@ -118,7 +116,6 @@ const ResourceCard = ({ resource }: any) => {
- {resource.assigned_to_object && (
{ )}
-
@@ -273,13 +258,14 @@ export default function ResourceBoard({
- ) : data?.length > 0 ? ( + ) : data && data?.results.length > 0 ? ( boardFilter(board) ) : (

No requests to show.

)} {!isLoading.board && - data?.length < (totalCount || 0) && + data && + data?.results.length < (data?.count || 0) && (isLoading.more ? (
Loading diff --git a/src/Components/Resource/ResourceCreate.tsx b/src/Components/Resource/ResourceCreate.tsx index cf9e5f6f22a..d0a6c36272b 100644 --- a/src/Components/Resource/ResourceCreate.tsx +++ b/src/Components/Resource/ResourceCreate.tsx @@ -1,8 +1,7 @@ -import { useReducer, useState, useEffect, lazy } from "react"; +import { useReducer, useState, lazy } from "react"; import { FacilitySelect } from "../Common/FacilitySelect"; import * as Notification from "../../Utils/Notifications.js"; -import { useDispatch } from "react-redux"; import { navigate } from "raviger"; import { OptionsType, @@ -11,8 +10,6 @@ import { } from "../../Common/constants"; import { parsePhoneNumber } from "../../Utils/utils"; import { phonePreg } from "../../Common/validation"; - -import { createResource, getAnyFacility } from "../../Redux/actions"; import { Cancel, Submit } from "../Common/components/ButtonV2"; import PhoneNumberFormField from "../Form/FormFields/PhoneNumberFormField"; import { FieldChangeEvent } from "../Form/FormFields/Utils"; @@ -26,6 +23,9 @@ import { FieldLabel } from "../Form/FormFields/FormField"; import Card from "../../CAREUI/display/Card"; import Page from "../Common/components/Page"; import { PhoneNumberValidator } from "../Form/FieldValidators"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import request from "../../Utils/request/request"; const Loading = lazy(() => import("../Common/Loading")); @@ -87,10 +87,7 @@ export default function ResourceCreate(props: resourceProps) { const { goBack } = useAppHistory(); const { facilityId } = props; const { t } = useTranslation(); - - const dispatchAction: any = useDispatch(); const [isLoading, setIsLoading] = useState(false); - const [facilityName, setFacilityName] = useState(""); const resourceFormReducer = (state = initialState, action: any) => { switch (action.type) { @@ -113,18 +110,10 @@ export default function ResourceCreate(props: resourceProps) { const [state, dispatch] = useReducer(resourceFormReducer, initialState); - useEffect(() => { - async function fetchFacilityName() { - if (facilityId) { - const res = await dispatchAction(getAnyFacility(facilityId)); - - setFacilityName(res?.data?.name || ""); - } else { - setFacilityName(""); - } - } - fetchFacilityName(); - }, [dispatchAction, facilityId]); + const { data: facilityData } = useQuery(routes.getAnyFacility, { + prefetch: facilityId !== undefined, + pathParams: { id: String(facilityId) }, + }); const validateForm = () => { const errors = { ...initError }; @@ -184,11 +173,11 @@ export default function ResourceCreate(props: resourceProps) { if (validForm) { setIsLoading(true); - const data = { + const resourceData = { status: "PENDING", category: state.form.category, sub_category: state.form.sub_category, - origin_facility: props.facilityId, + origin_facility: String(props.facilityId), approving_facility: (state.form.approving_facility || {}).id, assigned_facility: (state.form.assigned_facility || {}).id, emergency: state.form.emergency === "true", @@ -202,16 +191,18 @@ export default function ResourceCreate(props: resourceProps) { requested_quantity: state.form.requested_quantity || 0, }; - const res = await dispatchAction(createResource(data)); + const { res, data } = await request(routes.createResource, { + body: resourceData, + }); setIsLoading(false); - if (res && res.data && (res.status == 201 || res.status == 200)) { + if (res?.ok && data) { await dispatch({ type: "set_form", form: initForm }); Notification.Success({ msg: "Resource request created successfully", }); - navigate(`/resource/${res.data.id}`); + navigate(`/resource/${data.id}`); } } }; @@ -224,7 +215,7 @@ export default function ResourceCreate(props: resourceProps) { import("../Common/Loading")); export default function ResourceDetails(props: { id: string }) { - const dispatch: any = useDispatch(); - const initialData: any = {}; - const [data, setData] = useState(initialData); - const [isLoading, setIsLoading] = useState(true); const [isPrintMode, setIsPrintMode] = useState(false); - const [openDeleteResourceDialog, setOpenDeleteResourceDialog] = useState(false); - - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const res = await dispatch(getResourceDetails({ id: props.id })); - if (!status.aborted) { - if (res && res.data) { - setData(res.data); - } else { - navigate("/not-found"); - } - setIsLoading(false); + const { data, loading } = useQuery(routes.getResourceDetails, { + pathParams: { id: props.id }, + onResponse: ({ res, data }) => { + if (!res && !data) { + navigate("/not-found"); } }, - [props.id, dispatch] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] - ); + }); const handleResourceDelete = async () => { setOpenDeleteResourceDialog(true); - - const res = await dispatch(deleteResourceRecord(props.id)); + const { res, data } = await request(routes.deleteResourceRecord, { + pathParams: { id: props.id }, + }); if (res?.status === 204) { Notification.Success({ msg: "Resource record has been deleted successfully.", }); } else { Notification.Error({ - msg: "Error while deleting Resource: " + (res?.data?.detail || ""), + msg: "Error while deleting Resource: " + (data?.detail || ""), }); } @@ -223,7 +203,7 @@ export default function ResourceDetails(props: { id: string }) { ); }; - if (isLoading) { + if (loading || !data) { return ; } diff --git a/src/Components/Resource/ResourceDetailsUpdate.tsx b/src/Components/Resource/ResourceDetailsUpdate.tsx index ce0751e7b2e..c692618a774 100644 --- a/src/Components/Resource/ResourceDetailsUpdate.tsx +++ b/src/Components/Resource/ResourceDetailsUpdate.tsx @@ -1,15 +1,7 @@ import * as Notification from "../../Utils/Notifications.js"; - import { Cancel, Submit } from "../Common/components/ButtonV2"; -import { lazy, useCallback, useEffect, useReducer, useState } from "react"; -import { - getResourceDetails, - getUserList, - updateResource, -} from "../../Redux/actions"; +import { lazy, useReducer, useState } from "react"; import { navigate, useQueryParams } from "raviger"; -import { statusType, useAbortableEffect } from "../../Common/utils"; - import Card from "../../CAREUI/display/Card"; import CircularProgress from "../Common/components/CircularProgress"; import { FacilitySelect } from "../Common/FacilitySelect"; @@ -22,9 +14,11 @@ import { SelectFormField } from "../Form/FormFields/SelectFormField"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import TextFormField from "../Form/FormFields/TextFormField"; import UserAutocompleteFormField from "../Common/UserAutocompleteFormField"; - import useAppHistory from "../../Common/hooks/useAppHistory"; -import { useDispatch } from "react-redux"; +import useQuery from "../../Utils/request/useQuery.js"; +import routes from "../../Redux/api.js"; +import { UserModel } from "../Users/models.js"; +import request from "../../Utils/request/request.js"; const Loading = lazy(() => import("../Common/Loading")); @@ -67,13 +61,9 @@ const initialState = { export const ResourceDetailsUpdate = (props: resourceProps) => { const { goBack } = useAppHistory(); - const dispatchAction: any = useDispatch(); const [qParams, _] = useQueryParams(); const [isLoading, setIsLoading] = useState(true); - const [assignedQuantity, setAssignedQuantity] = useState(0); - const [requestTitle, setRequestTitle] = useState(""); - const [assignedUser, SetAssignedUser] = useState(null); - const [assignedUserLoading, setAssignedUserLoading] = useState(false); + const [assignedUser, SetAssignedUser] = useState(); const resourceFormReducer = (state = initialState, action: any) => { switch (action.type) { case "set_form": { @@ -95,23 +85,13 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { const [state, dispatch] = useReducer(resourceFormReducer, initialState); - useEffect(() => { - async function fetchData() { - if (state.form.assigned_to) { - setAssignedUserLoading(true); - - const res = await dispatchAction( - getUserList({ id: state.form.assigned_to }) - ); - - if (res && res.data && res.data.count) - SetAssignedUser(res.data.results[0]); - - setAssignedUserLoading(false); + const { loading: assignedUserLoading } = useQuery(routes.userList, { + onResponse: ({ res, data }) => { + if (res?.ok && data && data.count) { + SetAssignedUser(data.results[0]); } - } - fetchData(); - }, [dispatchAction, state.form.assigned_to]); + }, + }); const validateForm = () => { const errors = { ...initError }; @@ -147,13 +127,25 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { dispatch({ type: "set_form", form }); }; + const { data: resourceDetails } = useQuery(routes.getResourceDetails, { + pathParams: { id: props.id }, + onResponse: ({ res, data }) => { + if (res && data) { + const d = data; + d["status"] = qParams.status || data.status; + dispatch({ type: "set_form", form: d }); + } + setIsLoading(false); + }, + }); + const handleSubmit = async () => { const validForm = validateForm(); if (validForm) { setIsLoading(true); - const data = { + const resourceData = { category: "OXYGEN", status: state.form.status, origin_facility: state.form.origin_facility_object?.id, @@ -167,14 +159,17 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { assigned_quantity: state.form.status === "PENDING" ? state.form.assigned_quantity - : assignedQuantity, + : resourceDetails?.assigned_quantity || 0, }; - const res = await dispatchAction(updateResource(props.id, data)); + const { res, data } = await request(routes.updateResource, { + pathParams: { id: props.id }, + body: resourceData, + }); setIsLoading(false); - if (res && res.status == 200 && res.data) { - dispatch({ type: "set_form", form: res.data }); + if (res && res.status == 200 && data) { + dispatch({ type: "set_form", form: data }); Notification.Success({ msg: "Resource request updated successfully", }); @@ -186,31 +181,6 @@ export const ResourceDetailsUpdate = (props: resourceProps) => { } }; - const fetchData = useCallback( - async (status: statusType) => { - setIsLoading(true); - const res = await dispatchAction(getResourceDetails({ id: props.id })); - if (!status.aborted) { - if (res && res.data) { - setRequestTitle(res.data.title); - setAssignedQuantity(res.data.assigned_quantity); - const d = res.data; - d["status"] = qParams.status || res.data.status; - dispatch({ type: "set_form", form: d }); - } - setIsLoading(false); - } - }, - [props.id, dispatchAction, qParams.status] - ); - - useAbortableEffect( - (status: statusType) => { - fetchData(status); - }, - [fetchData] - ); - if (isLoading) { return ; } @@ -219,7 +189,7 @@ export const ResourceDetailsUpdate = (props: resourceProps) => {
diff --git a/src/Components/Resource/models.ts b/src/Components/Resource/models.ts new file mode 100644 index 00000000000..f10ac988552 --- /dev/null +++ b/src/Components/Resource/models.ts @@ -0,0 +1,41 @@ +import { PerformedByModel } from "../HCX/misc"; + +export interface IComment { + id: string; + created_by_object: PerformedByModel; + created_date: string; + modified_date: string; + comment: string; + created_by: number; +} + +export interface IResource { + id: string; + title: string; + emergency: boolean; + status?: string; + origin_facility_object: { + name: string; + }; + approving_facility_object: { + name: string; + }; + assigned_facility_object: { + name: string; + }; + assigned_quantity: number; + modified_date: string; + category: any; + sub_category: number; + origin_facility: string; + approving_facility: string; + assigned_facility: string; + reason: string; + refering_facility_contact_name: string; + refering_facility_contact_number: string; + requested_quantity: number; + assigned_to_object: PerformedByModel; + created_by_object: PerformedByModel; + created_date: string; + last_edited_by_object: PerformedByModel; +} diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 1d2d6f4f7b6..15934d0957d 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -803,30 +803,9 @@ export const listMedibaseMedicines = ( }; // Resource -export const createResource = (params: object) => { - return fireRequest("createResource", [], params); -}; -export const updateResource = (id: string, params: object) => { - return fireRequest("updateResource", [id], params); -}; -export const deleteResourceRecord = (id: string) => { - return fireRequest("deleteResourceRecord", [id], {}); -}; -export const listResourceRequests = (params: object, key: string) => { - return fireRequest("listResourceRequests", [], params, null, key); -}; -export const getResourceDetails = (pathParam: object) => { - return fireRequest("getResourceDetails", [], {}, pathParam); -}; export const downloadResourceRequests = (params: object) => { return fireRequest("downloadResourceRequests", [], params); }; -export const getResourceComments = (id: string, params: object) => { - return fireRequest("getResourceComments", [], params, { id }); -}; -export const addResourceComments = (id: string, params: object) => { - return fireRequest("addResourceComments", [], params, { id }); -}; export const listAssets = (params: object) => fireRequest("listAssets", [], params); diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 104fa0c0c75..01c356f6603 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -28,6 +28,7 @@ import { AssetUpdate, } from "../Components/Assets/AssetTypes"; import { + ConsultationModel, FacilityModel, LocationModel, WardModel, @@ -40,10 +41,13 @@ import { ILocalBodyByDistrict, IPartialUpdateExternalResult, } from "../Components/ExternalResult/models"; + import { Prescription } from "../Components/Medicine/models"; -import { PatientModel } from "../Components/Patient/models"; + import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; +import { PatientModel } from "../Components/Patient/models"; +import { IComment, IResource } from "../Components/Resource/models"; /** * A fake function that returns an empty object casted to type T @@ -128,6 +132,8 @@ const routes = { userList: { path: "/api/v1/users/", + method: "GET", + TRes: Type>(), }, userListSkill: { @@ -843,21 +849,31 @@ const routes = { createResource: { path: "/api/v1/resource/", method: "POST", + TRes: Type(), + TBody: Type>(), }, updateResource: { - path: "/api/v1/resource", + path: "/api/v1/resource/{id}", method: "PUT", + TRes: Type(), + TBody: Type>(), }, deleteResourceRecord: { - path: "/api/v1/resource", + path: "/api/v1/resource/{id}", method: "DELETE", + TRes: Type<{ + detail?: string; + }>(), }, listResourceRequests: { path: "/api/v1/resource/", method: "GET", + TRes: Type>(), }, getResourceDetails: { path: "/api/v1/resource/{id}/", + method: "GET", + TRes: Type(), }, downloadResourceRequests: { path: "/api/v1/resource/", @@ -866,10 +882,13 @@ const routes = { getResourceComments: { path: "/api/v1/resource/{id}/comment/", method: "GET", + TRes: Type>(), }, addResourceComments: { path: "/api/v1/resource/{id}/comment/", method: "POST", + TRes: Type(), + TBody: Type>(), }, // Assets endpoints From 343ff7eeb01623e4c521fdb9ba2bc1c4fa895483 Mon Sep 17 00:00:00 2001 From: Ashraf Mohammed <98876115+AshrafMd-1@users.noreply.github.com> Date: Tue, 31 Oct 2023 04:17:38 -0700 Subject: [PATCH 02/58] Fixed bed number coming up two time in patients page. (#6526) * stop displaying bed name two times * resolve tooltip --- src/Components/Patient/ManagePatients.tsx | 26 ++++++++++------------- 1 file changed, 11 insertions(+), 15 deletions(-) diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 61b18faa367..7db0bcceb39 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -519,27 +519,23 @@ export const PatientManager = () => {
{patient?.last_consultation?.current_bed && patient?.last_consultation?.discharge_date === null ? ( -
- +
+ { patient?.last_consultation?.current_bed?.bed_object ?.location_object?.name } - - { - patient?.last_consultation?.current_bed?.bed_object - ?.location_object?.name - } - - {patient?.last_consultation?.current_bed?.bed_object.name} - - { - patient?.last_consultation?.current_bed?.bed_object - ?.name - } - + {patient?.last_consultation?.current_bed?.bed_object?.name} + + + { + patient?.last_consultation?.current_bed?.bed_object + ?.location_object?.name + } +
+ {patient?.last_consultation?.current_bed?.bed_object?.name}
) : patient.last_consultation?.suggestion === "DC" ? ( From b43f8d577e12b5b144ffe6772807eace8968629a Mon Sep 17 00:00:00 2001 From: Onkar Jadhav <56870381+Omkar76@users.noreply.github.com> Date: Tue, 31 Oct 2023 16:51:06 +0530 Subject: [PATCH 03/58] Make header in patient dashboard more responsive. Fixes #6488 (#6515) --- .../Facility/ConsultationDetails/index.tsx | 40 +++++++++---------- 1 file changed, 19 insertions(+), 21 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index cedb1a843d3..202c9200b0a 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -271,7 +271,7 @@ export const ConsultationDetails = (props: any) => { />
-
diff --git a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx index 35a125ae07d..9004dea939d 100644 --- a/src/Components/Form/FormFields/AutocompleteMultiselect.tsx +++ b/src/Components/Form/FormFields/AutocompleteMultiselect.tsx @@ -119,15 +119,17 @@ export const AutocompleteMutliSelect = ( onChange={(event) => setQuery(event.target.value.toLowerCase())} autoComplete="off" /> - -
- {props.isLoading ? ( - - ) : ( - - )} -
-
+ {!props.disabled && ( + +
+ {props.isLoading ? ( + + ) : ( + + )} +
+
+ )}
{value.length !== 0 && (
diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 7db0bcceb39..7a78d94582e 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -16,9 +16,14 @@ import { getAllPatient, getAnyFacility, getDistrict, + getFacilityAssetLocation, getLocalBody, } from "../../Redux/actions"; -import { statusType, useAbortableEffect } from "../../Common/utils"; +import { + statusType, + useAbortableEffect, + parseOptionId, +} from "../../Common/utils"; import { AdvancedFilterButton } from "../../CAREUI/interactive/FiltersSlideover"; import ButtonV2 from "../Common/components/ButtonV2"; @@ -36,7 +41,6 @@ import RecordMeta from "../../CAREUI/display/RecordMeta"; import SearchInput from "../Form/SearchInput"; import SortDropdownMenu from "../Common/SortDropdown"; import SwitchTabs from "../Common/components/SwitchTabs"; -import { parseOptionId } from "../../Common/utils"; import { formatAge, parsePhoneNumber } from "../../Utils/utils.js"; import { useDispatch } from "react-redux"; import useFilters from "../../Common/hooks/useFilters"; @@ -104,6 +108,7 @@ export const PatientManager = () => { const [districtName, setDistrictName] = useState(""); const [localbodyName, setLocalbodyName] = useState(""); const [facilityBadgeName, setFacilityBadge] = useState(""); + const [locationBadgeName, setLocationBadge] = useState(""); const [phone_number, setPhoneNumber] = useState(""); const [phoneNumberError, setPhoneNumberError] = useState(""); const [emergency_phone_number, setEmergencyPhoneNumber] = useState(""); @@ -199,6 +204,8 @@ export const PatientManager = () => { qParams.last_consultation_admitted_bed_type_list || undefined, last_consultation_discharge_reason: qParams.last_consultation_discharge_reason || undefined, + last_consultation_current_bed__location: + qParams.last_consultation_current_bed__location || undefined, srf_id: qParams.srf_id || undefined, number_of_doses: qParams.number_of_doses || undefined, covin_id: qParams.covin_id || undefined, @@ -344,6 +351,7 @@ export const PatientManager = () => { qParams.age_min, qParams.last_consultation_admitted_bed_type_list, qParams.last_consultation_discharge_reason, + qParams.last_consultation_current_bed__location, qParams.facility, qParams.facility_type, qParams.district, @@ -443,12 +451,32 @@ export const PatientManager = () => { [dispatch, qParams.facility] ); + const fetchLocationBadgeName = useCallback( + async (status: statusType) => { + const res = + qParams.last_consultation_current_bed__location && + (await dispatch( + getFacilityAssetLocation( + qParams.facility, + qParams.last_consultation_current_bed__location + ) + )); + + if (!status.aborted) { + setLocationBadge(res?.data?.name); + } + }, + [dispatch, qParams.last_consultation_current_bed__location] + ); + useAbortableEffect( (status: statusType) => { fetchFacilityBadgeName(status); + fetchLocationBadgeName(status); }, - [fetchFacilityBadgeName] + [fetchFacilityBadgeName, fetchLocationBadgeName] ); + const LastAdmittedToTypeBadges = () => { const badge = (key: string, value: any, id: string) => { return ( @@ -936,6 +964,11 @@ export const PatientManager = () => { "last_consultation_medico_legal_case" ), value("Facility", "facility", facilityBadgeName), + value( + "Location", + "last_consultation_current_bed__location", + locationBadgeName + ), badge("Facility Type", "facility_type"), value("District", "district", districtName), ordering(), diff --git a/src/Components/Patient/PatientFilter.tsx b/src/Components/Patient/PatientFilter.tsx index 97f5a6c6b9c..7bc7f7b861f 100644 --- a/src/Components/Patient/PatientFilter.tsx +++ b/src/Components/Patient/PatientFilter.tsx @@ -34,6 +34,7 @@ import FiltersSlideover from "../../CAREUI/interactive/FiltersSlideover"; import AccordionV2 from "../Common/components/AccordionV2"; import { dateQueryString } from "../../Utils/utils"; import dayjs from "dayjs"; +import { LocationSelect } from "../Common/LocationSelect"; const getDate = (value: any) => value && dayjs(value).isValid() && dayjs(value).toDate(); @@ -79,6 +80,8 @@ export default function PatientFilter(props: any) { filter.last_consultation_admitted_bed_type_list ? filter.last_consultation_admitted_bed_type_list.split(",") : [], + last_consultation_current_bed__location: + filter.last_consultation_current_bed__location || "", last_consultation_discharge_reason: filter.last_consultation_discharge_reason || null, srf_id: filter.srf_id || null, @@ -128,6 +131,7 @@ export default function PatientFilter(props: any) { last_consultation_discharge_date_before: "", last_consultation_discharge_date_after: "", last_consultation_admitted_to_list: [], + last_consultation_current_bed__location: "", srf_id: "", number_of_doses: null, covin_id: "", @@ -239,6 +243,7 @@ export default function PatientFilter(props: any) { last_consultation_discharge_date_after, last_consultation_admitted_bed_type_list, last_consultation_discharge_reason, + last_consultation_current_bed__location, number_of_doses, covin_id, srf_id, @@ -256,6 +261,8 @@ export default function PatientFilter(props: any) { district: district || "", lsgBody: lsgBody || "", facility: facility || "", + last_consultation_current_bed__location: + last_consultation_current_bed__location || "", facility_type: facility_type || "", date_declared_positive_before: dateQueryString( date_declared_positive_before @@ -588,13 +595,29 @@ export default function PatientFilter(props: any) { setFacility(obj, "facility")} />
-
+ Location + + setFilterState({ + ...filterState, + last_consultation_current_bed__location: selected, + }) + } + /> +
+
Facility type Date: Tue, 31 Oct 2023 16:52:38 +0530 Subject: [PATCH 05/58] Show only those facilties that aren't linked to user (#6253) * only show unlinked facilties * rename param * fix cypress for updated backend * fix typo --- cypress/e2e/users_spec/user_creation.cy.ts | 9 +-------- src/Components/Common/FacilitySelect.tsx | 3 +++ src/Components/Users/ManageUsers.tsx | 1 + 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/cypress/e2e/users_spec/user_creation.cy.ts b/cypress/e2e/users_spec/user_creation.cy.ts index 5a1e5d874de..bd5002d386f 100644 --- a/cypress/e2e/users_spec/user_creation.cy.ts +++ b/cypress/e2e/users_spec/user_creation.cy.ts @@ -202,14 +202,7 @@ describe("User Creation", () => { // .click() // .type("Dummy Facility 1") // .wait("@getFacilities"); - // cy.get("li[role='option']").first().click(); - // cy.intercept(/\/api\/v1\/users\/\w+\/add_facility\//).as("addFacility"); - // cy.get("button[id='link-facility']").click(); - // cy.wait("@addFacility") - // // .its("response.statusCode") - // // .should("eq", 201) - // .get("span") - // .contains("Facility - User Already has permission to this facility"); + // cy.get("li[role='option']").should("not.exist"); // }); afterEach(() => { diff --git a/src/Components/Common/FacilitySelect.tsx b/src/Components/Common/FacilitySelect.tsx index 400ce115bc5..19494081aa3 100644 --- a/src/Components/Common/FacilitySelect.tsx +++ b/src/Components/Common/FacilitySelect.tsx @@ -6,6 +6,7 @@ import { FacilityModel } from "../Facility/models"; interface FacilitySelectProps { name: string; + exclude_user: string; errors?: string | undefined; className?: string; searchAll?: boolean; @@ -22,6 +23,7 @@ interface FacilitySelectProps { export const FacilitySelect = (props: FacilitySelectProps) => { const { name, + exclude_user, multiple, selected, setSelected, @@ -45,6 +47,7 @@ export const FacilitySelect = (props: FacilitySelectProps) => { search_text: text, all: searchAll, facility_type: facilityType, + exclude_user: exclude_user, district, }; diff --git a/src/Components/Users/ManageUsers.tsx b/src/Components/Users/ManageUsers.tsx index 3b55c40fc70..1771a6ca22f 100644 --- a/src/Components/Users/ManageUsers.tsx +++ b/src/Components/Users/ManageUsers.tsx @@ -706,6 +706,7 @@ function UserFacilities(props: { user: any }) { Date: Fri, 3 Nov 2023 17:50:46 +0530 Subject: [PATCH 06/58] Refactor middleware hostname in Feed component (#6538) --- src/Common/hooks/useMSEplayer.ts | 2 ++ .../Assets/AssetType/ONVIFCamera.tsx | 12 +++++---- .../Facility/Consultations/Feed.tsx | 27 ++++++++++++++----- 3 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/Common/hooks/useMSEplayer.ts b/src/Common/hooks/useMSEplayer.ts index fcbf216ed6a..4d1bb36b9ac 100644 --- a/src/Common/hooks/useMSEplayer.ts +++ b/src/Common/hooks/useMSEplayer.ts @@ -20,6 +20,8 @@ interface UseMSEMediaPlayerOption { export interface ICameraAssetState { id: string; accessKey: string; + middleware_address: string; + location_middleware: string; } export enum StreamStatus { diff --git a/src/Components/Assets/AssetType/ONVIFCamera.tsx b/src/Components/Assets/AssetType/ONVIFCamera.tsx index 4a3e475419e..44d4d372d73 100644 --- a/src/Components/Assets/AssetType/ONVIFCamera.tsx +++ b/src/Components/Assets/AssetType/ONVIFCamera.tsx @@ -53,6 +53,11 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { } }, [facility, facilityId]); + const fallbackMiddleware = + asset?.location_object?.middleware_address || facilityMiddlewareHostname; + + const currentMiddleware = middlewareHostname || fallbackMiddleware; + useEffect(() => { if (asset) { setAssetType(asset?.asset_class); @@ -105,7 +110,7 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { try { setLoadingAddPreset(true); const presetData = await axios.get( - `https://${facilityMiddlewareHostname}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}` + `https://${currentMiddleware}/status?hostname=${config.hostname}&port=${config.port}&username=${config.username}&password=${config.password}` ); const { res } = await request(routes.createAssetBed, { @@ -136,9 +141,6 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { }; if (isLoading || loading || !facility) return ; - const fallbackMiddleware = - asset?.location_object?.middleware_address || facilityMiddlewareHostname; - return (
{["DistrictAdmin", "StateAdmin"].includes(authUser.user_type) && ( @@ -223,7 +225,7 @@ const ONVIFCamera = ({ assetId, facilityId, asset, onUpdated }: Props) => { addPreset={addPreset} isLoading={loadingAddPreset} refreshPresetsHash={refreshPresetsHash} - facilityMiddlewareHostname={facilityMiddlewareHostname} + facilityMiddlewareHostname={currentMiddleware} /> ) : null}
diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 31691c736f4..3448d85a6a2 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -48,8 +48,11 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { const [cameraAsset, setCameraAsset] = useState({ id: "", accessKey: "", + middleware_address: "", + location_middleware: "", }); - const [cameraMiddlewareHostname, setCameraMiddlewareHostname] = useState(""); + const [facilityMiddlewareHostname, setFacilityMiddlewareHostname] = + useState(""); const [cameraConfig, setCameraConfig] = useState({}); const [isLoading, setIsLoading] = useState(true); const [bedPresets, setBedPresets] = useState([]); @@ -66,13 +69,19 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { const res = await dispatch(getPermittedFacility(facilityId)); if (res.status === 200 && res.data) { - setCameraMiddlewareHostname(res.data.middleware_address); + setFacilityMiddlewareHostname(res.data.middleware_address); } }; if (facilityId) fetchFacility(); }, [dispatch, facilityId]); + const fallbackMiddleware = + cameraAsset.location_middleware || facilityMiddlewareHostname; + + const currentMiddleware = + cameraAsset.middleware_address || fallbackMiddleware; + useEffect(() => { if (cameraState) { setCameraState({ @@ -130,6 +139,12 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { setCameraAsset({ id: bedAssets.data.results[0].asset_object.id, accessKey: config[2] || "", + middleware_address: + bedAssets.data.results[0].asset_object?.meta + ?.middleware_hostname, + location_middleware: + bedAssets.data.results[0].asset_object.location_object + ?.middleware_address, }); setCameraConfig(bedAssets.data.results[0].meta); setCameraState({ @@ -170,8 +185,8 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { ); const url = !isIOS - ? `wss://${cameraMiddlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0` - : `https://${cameraMiddlewareHostname}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`; + ? `wss://${currentMiddleware}/stream/${cameraAsset?.accessKey}/channel/0/mse?uuid=${cameraAsset?.accessKey}&channel=0` + : `https://${currentMiddleware}/stream/${cameraAsset?.accessKey}/channel/0/hls/live/index.m3u8?uuid=${cameraAsset?.accessKey}&channel=0`; const { startStream, @@ -182,7 +197,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { : // eslint-disable-next-line react-hooks/rules-of-hooks useMSEMediaPlayer({ config: { - middlewareHostname: cameraMiddlewareHostname, + middlewareHostname: currentMiddleware, ...cameraAsset, }, url, @@ -229,7 +244,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { }); getBedPresets(cameraAsset); } - }, [cameraAsset, cameraMiddlewareHostname]); + }, [cameraAsset, currentMiddleware]); useEffect(() => { let tId: any; From ac405ce4e9c51293d3d0cfb1fc4a6d8322417769 Mon Sep 17 00:00:00 2001 From: Ashesh <3626859+Ashesh3@users.noreply.github.com> Date: Fri, 3 Nov 2023 20:33:34 +0530 Subject: [PATCH 07/58] Show camera feed button only for specific roles (#6540) --- .../Facility/ConsultationDetails/index.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 202c9200b0a..6fda874e3b3 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -335,14 +335,17 @@ export const ConsultationDetails = (props: any) => { > Doctor Connect - {patientData.last_consultation?.id && ( - - Camera Feed - - )} + {patientData.last_consultation?.id && + ["DistrictAdmin", "StateAdmin", "Doctor"].includes( + authUser.user_type + ) && ( + + Camera Feed + + )} )} Date: Fri, 3 Nov 2023 21:43:50 +0530 Subject: [PATCH 08/58] add auto deployment for staging gcp deployment (#6519) --- .github/workflows/deploy.yaml | 42 +++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/.github/workflows/deploy.yaml b/.github/workflows/deploy.yaml index 06b1cf8eff0..e5b3c768c49 100644 --- a/.github/workflows/deploy.yaml +++ b/.github/workflows/deploy.yaml @@ -180,6 +180,48 @@ jobs: rm -rf /tmp/.buildx-cache mv /tmp/.buildx-cache-new /tmp/.buildx-cache + deploy-staging-gcp: + needs: build-production + name: Deploy to staging GCP cluster + runs-on: ubuntu-latest + environment: + name: Staging-GCP + url: https://care-staging.ohc.network/ + steps: + - name: Checkout Kube Config + uses: actions/checkout@v3 + with: + repository: coronasafe/care-staging-gcp + token: ${{ secrets.GIT_ACCESS_TOKEN }} + path: kube + ref: main + + # Setup gcloud CLI + - uses: google-github-actions/setup-gcloud@94337306dda8180d967a56932ceb4ddcf01edae7 + with: + service_account_key: ${{ secrets.GKE_SA_KEY }} + project_id: ${{ secrets.GKE_PROJECT }} + + # Get the GKE credentials so we can deploy to the cluster + - uses: google-github-actions/get-gke-credentials@fb08709ba27618c31c09e014e1d8364b02e5042e + with: + cluster_name: ${{ secrets.GKE_CLUSTER }} + location: ${{ secrets.GKE_ZONE }} + credentials: ${{ secrets.GKE_SA_KEY }} + + - name: install kubectl + uses: azure/setup-kubectl@v3.0 + with: + version: "v1.23.6" + id: install + + - name: Deploy Care Fe Production + run: | + mkdir -p $HOME/.kube/ + cd kube/deployments/ + sed -i -e "s/_BUILD_NUMBER_/${GITHUB_RUN_NUMBER}/g" care-fe.yaml + kubectl apply -f care-fe.yaml + deploy-production-manipur: needs: build-production name: Deploy to GKE Manipur From e30d6bda24251cf44c2af51adc1d38bba349002b Mon Sep 17 00:00:00 2001 From: Ashraf Mohammed <98876115+AshrafMd-1@users.noreply.github.com> Date: Mon, 6 Nov 2023 04:15:16 -0800 Subject: [PATCH 09/58] Redirect to Original URL After Session Expiry and Re-login (#6495) * go back to previous url after session expiration * add query parameters instead of localstorage * resolve cross scripting * use newURL instead of string manipulation * check origin while redirecting * remove cross-site-scripting * convert the redirection into a function * remove else redirection --- src/Components/Auth/Login.tsx | 3 ++- src/Redux/fireRequest.tsx | 2 +- src/Utils/request/handleResponse.ts | 2 +- src/Utils/utils.ts | 25 ++++++++++++++++++++++++- 4 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/Components/Auth/Login.tsx b/src/Components/Auth/Login.tsx index 140a0013fd9..58472c4ff25 100644 --- a/src/Components/Auth/Login.tsx +++ b/src/Components/Auth/Login.tsx @@ -12,6 +12,7 @@ import CircularProgress from "../Common/components/CircularProgress"; import { LocalStorageKeys } from "../../Common/constants"; import ReactMarkdown from "react-markdown"; import rehypeRaw from "rehype-raw"; +import { handleRedirection } from "../../Utils/utils"; export const Login = (props: { forgot?: boolean }) => { const { @@ -109,7 +110,7 @@ export const Login = (props: { forgot?: boolean }) => { window.location.pathname === "/" || window.location.pathname === "/login" ) { - window.location.href = "/facility"; + handleRedirection(); } else { window.location.href = window.location.pathname.toString(); } diff --git a/src/Redux/fireRequest.tsx b/src/Redux/fireRequest.tsx index 3d8c677d47d..892e6bd2ee9 100644 --- a/src/Redux/fireRequest.tsx +++ b/src/Redux/fireRequest.tsx @@ -152,7 +152,7 @@ export const fireRequest = ( if (error.response.status > 400 && error.response.status < 500) { if (error.response.data && error.response.data.detail) { if (error.response.data.code === "token_not_valid") { - window.location.href = "/session-expired"; + window.location.href = `/session-expired?redirect=${window.location.href}`; } Notification.Error({ msg: error.response.data.detail, diff --git a/src/Utils/request/handleResponse.ts b/src/Utils/request/handleResponse.ts index 2ecad95ac88..8698919c869 100644 --- a/src/Utils/request/handleResponse.ts +++ b/src/Utils/request/handleResponse.ts @@ -29,7 +29,7 @@ export default function handleResponse( if (res.status >= 400) { // Invalid token if (!silent && error?.code === "token_not_valid") { - navigate("/session-expired"); + navigate(`/session-expired?redirect=${window.location.href}`); } notify?.Error({ msg: error?.detail || "Something went wrong...!" }); diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index 847304553a8..a96a4c65146 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -107,8 +107,31 @@ export const handleSignOut = (forceReload: boolean) => { Object.values(LocalStorageKeys).forEach((key) => localStorage.removeItem(key) ); + const redirectURL = new URLSearchParams(window.location.search).get( + "redirect" + ); + redirectURL ? navigate(`/?redirect=${redirectURL}`) : navigate("/"); if (forceReload) window.location.href = "/"; - else navigate("/"); +}; + +export const handleRedirection = () => { + const redirectParam = new URLSearchParams(window.location.search).get( + "redirect" + ); + try { + if (redirectParam) { + const redirectURL = new URL(redirectParam); + + if (redirectURL.origin === window.location.origin) { + const newPath = redirectURL.pathname + redirectURL.search; + window.location.href = `${window.location.origin}${newPath}`; + return; + } + } + window.location.href = "/facility"; + } catch { + window.location.href = "/facility"; + } }; /** From fbf8eeed80afa081a2600e73b4de4a0291e80c85 Mon Sep 17 00:00:00 2001 From: Rithvik Nishad Date: Mon, 6 Nov 2023 19:21:53 +0530 Subject: [PATCH 10/58] Group medicine administration by 4 hours + Support for archiving medicines + Administration Activity View + migrate `useDispatch` to `useQuery` (#6396) * fixes #6341; bin medicine administrations by 4:1 hrs * fix comments * fix tooltip clipping * remove warnings * improve how date seperation is shown * popover shows more details * cleanup: move files to correct place * delete old medicine administration table * cleanup * remove useDispatch in bulk administer * fixes #6442; API calls via useQuery for Medicines * cleanup * fix keys * cleanup * update seperator * overlay * temp fix tooltip * fix refetch logic * fix z-index * consistency * fix z-index * fix useIsScrollable dependencies * fix z-index * rewrite scroll overflow behaviour * fix z-index * cleanup API routes * support for archive and fix responsiveness issues * fix styling * fix path missing path params * remove tooltip * swap positions of actions and timestamp in timeline * fix responsiveness * minor spacing * fix responsiveness * fix spacing * hide archive if discontinued * fix responsiveness * fix discontinue refetch not invoked --------- Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- src/CAREUI/display/RecordMeta.tsx | 13 +- src/CAREUI/display/Timeline.tsx | 158 +++++ src/CAREUI/interactive/ScrollOverlay.tsx | 30 + src/CAREUI/interactive/SlideOver.tsx | 2 +- src/Components/Common/Dialog.tsx | 2 +- .../ConsultationMedicinesTab.tsx | 12 +- .../ConsultationUpdatesTab.tsx | 2 - src/Components/Facility/DischargeModal.tsx | 17 +- .../Form/FormFields/PhoneNumberFormField.tsx | 2 +- .../Medicine/AdministerMedicine.tsx | 25 +- .../Medicine/CreatePrescriptionForm.tsx | 35 +- .../Medicine/DiscontinuePrescription.tsx | 25 +- .../Medicine/EditPrescriptionForm.tsx | 13 +- .../Medicine/ManagePrescriptions.tsx | 12 +- .../Medicine/MedicineAdministration.tsx | 74 +- .../AdministrationEventCell.tsx | 123 ++++ .../AdministrationEventSeperator.tsx | 22 + .../AdministrationTable.tsx | 106 +++ .../AdministrationTableRow.tsx | 245 +++++++ .../BulkAdminister.tsx | 47 ++ .../MedicineAdministrationSheet/index.tsx | 158 +++++ .../MedicineAdministrationSheet/utils.ts | 34 + .../Medicine/MedicineAdministrationsTable.tsx | 88 --- .../PrescriptionAdministrationsTable.tsx | 652 ------------------ .../Medicine/PrescriptionBuilder.tsx | 42 +- .../Medicine/PrescriptionDetailCard.tsx | 15 +- .../Medicine/PrescriptionsTable.tsx | 47 +- .../Medicine/PrescrpitionTimeline.tsx | 227 ++++++ .../ResponsiveMedicineTables.tsx | 4 +- src/Components/Medicine/models.ts | 14 +- src/Components/Medicine/routes.ts | 59 ++ src/Redux/actions.tsx | 74 +- src/Redux/api.tsx | 46 +- src/Utils/request/useQuery.ts | 1 + src/Utils/request/utils.ts | 6 +- src/Utils/utils.ts | 4 + 36 files changed, 1389 insertions(+), 1047 deletions(-) create mode 100644 src/CAREUI/display/Timeline.tsx create mode 100644 src/CAREUI/interactive/ScrollOverlay.tsx create mode 100644 src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventCell.tsx create mode 100644 src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx create mode 100644 src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx create mode 100644 src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx create mode 100644 src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx create mode 100644 src/Components/Medicine/MedicineAdministrationSheet/index.tsx create mode 100644 src/Components/Medicine/MedicineAdministrationSheet/utils.ts delete mode 100644 src/Components/Medicine/MedicineAdministrationsTable.tsx delete mode 100644 src/Components/Medicine/PrescriptionAdministrationsTable.tsx create mode 100644 src/Components/Medicine/PrescrpitionTimeline.tsx rename src/Components/{Common/components => Medicine}/ResponsiveMedicineTables.tsx (97%) create mode 100644 src/Components/Medicine/routes.ts diff --git a/src/CAREUI/display/RecordMeta.tsx b/src/CAREUI/display/RecordMeta.tsx index 48cc8d370ad..944ddf27c8f 100644 --- a/src/CAREUI/display/RecordMeta.tsx +++ b/src/CAREUI/display/RecordMeta.tsx @@ -1,5 +1,10 @@ import CareIcon from "../icons/CareIcon"; -import { formatDateTime, isUserOnline, relativeTime } from "../../Utils/utils"; +import { + formatDateTime, + formatName, + isUserOnline, + relativeTime, +} from "../../Utils/utils"; import { ReactNode } from "react"; interface Props { @@ -30,7 +35,7 @@ const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { by - {user.first_name} {user.last_name} + {formatName(user)} {isOnline && (
)} @@ -48,9 +53,7 @@ const RecordMeta = ({ time, user, prefix, className, inlineUser }: Props) => { {user && inlineUser && by} {user && } {user && inlineUser && ( - - {user.first_name} {user.last_name} - + {formatName(user)} )}
); diff --git a/src/CAREUI/display/Timeline.tsx b/src/CAREUI/display/Timeline.tsx new file mode 100644 index 00000000000..7549fbfd69f --- /dev/null +++ b/src/CAREUI/display/Timeline.tsx @@ -0,0 +1,158 @@ +import { createContext, useContext } from "react"; +import { PerformedByModel } from "../../Components/HCX/misc"; +import { classNames, formatName } from "../../Utils/utils"; +import CareIcon, { IconName } from "../icons/CareIcon"; +import RecordMeta from "./RecordMeta"; + +export interface TimelineEvent { + type: TType; + timestamp: string; + by: PerformedByModel | undefined; + icon: IconName; + notes?: string; + cancelled?: boolean; +} + +interface TimelineProps { + className: string; + children: React.ReactNode | React.ReactNode[]; + name: string; +} + +const TimelineContext = createContext(""); + +export default function Timeline({ className, children, name }: TimelineProps) { + return ( +
+
    + + {children} + +
+
+ ); +} + +interface TimelineNodeProps { + event: TimelineEvent; + title?: React.ReactNode; + /** + * Used to add a suffix to the auto-generated title. Will be ignored if `title` is provided. + */ + titleSuffix?: React.ReactNode; + actions?: React.ReactNode; + className?: string; + children?: React.ReactNode; + name?: string; + isLast: boolean; +} + +export const TimelineNode = (props: TimelineNodeProps) => { + const name = useContext(TimelineContext); + + return ( +
  • +
    +
    +
    + +
    +
    +
    + {props.title || ( + +

    + {props.event.by && ( + + {formatName(props.event.by)}{" "} + + )} + {props.titleSuffix + ? props.titleSuffix + : `${props.event.type} the ${props.name || name}.`} +

    + {props.actions && ( + {props.actions} + )} + +
    + )} +
    +
    + +
    + {props.event.notes} + {props.children} +
    +
    +
  • + ); +}; + +interface TimelineNodeTitleProps { + children: React.ReactNode | React.ReactNode[]; + event: TimelineEvent; +} + +export const TimelineNodeTitle = (props: TimelineNodeTitleProps) => { + return ( + <> +
    +
    + +
    + {props.children} +
    + + ); +}; + +export const TimelineNodeActions = (props: { + children: React.ReactNode | React.ReactNode[]; +}) => { + return
    {props.children}
    ; +}; + +interface TimelineNodeNotesProps { + children?: React.ReactNode | React.ReactNode[]; + icon?: IconName; +} + +export const TimelineNodeNotes = ({ + children, + icon = "l-notes", +}: TimelineNodeNotesProps) => { + if (!children) { + return; + } + + return ( +
    + +
    {children}
    +
    + ); +}; diff --git a/src/CAREUI/interactive/ScrollOverlay.tsx b/src/CAREUI/interactive/ScrollOverlay.tsx new file mode 100644 index 00000000000..c49f7223149 --- /dev/null +++ b/src/CAREUI/interactive/ScrollOverlay.tsx @@ -0,0 +1,30 @@ +import useVisibility from "../../Utils/useVisibility"; +import { classNames } from "../../Utils/utils"; + +interface Props { + className?: string; + children: React.ReactNode; + overlay: React.ReactNode; + disableOverlay?: boolean; +} + +export default function ScrollOverlay(props: Props) { + const [bottomIsVisible, ref] = useVisibility(); + const hasScrollContent = !props.disableOverlay && !bottomIsVisible; + + return ( +
    + {props.children} + +
    +
    + {hasScrollContent && props.overlay} +
    +
    + ); +} diff --git a/src/CAREUI/interactive/SlideOver.tsx b/src/CAREUI/interactive/SlideOver.tsx index 34e1c615f94..9fae2588fca 100644 --- a/src/CAREUI/interactive/SlideOver.tsx +++ b/src/CAREUI/interactive/SlideOver.tsx @@ -61,7 +61,7 @@ export default function SlideOver({ {}} > diff --git a/src/Components/Common/Dialog.tsx b/src/Components/Common/Dialog.tsx index ffe17606de2..c1dcf5afb70 100644 --- a/src/Components/Common/Dialog.tsx +++ b/src/Components/Common/Dialog.tsx @@ -26,7 +26,7 @@ const DialogModal = (props: DialogProps) => { return (
    - + { return (
    {/* eslint-disable-next-line i18next/no-literal-string */} - -
    diff --git a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx index 1b89f3980e1..2c9b8631077 100644 --- a/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx +++ b/src/Components/Facility/ConsultationDetails/ConsultationUpdatesTab.tsx @@ -232,7 +232,6 @@ export const ConsultationUpdatesTab = (props: ConsultationTabProps) => {
    {
    { setFacility(selected); const { id, name } = selected || {}; @@ -238,7 +236,7 @@ const DischargeModal = ({ setSelected={(selected) => handleFacilitySelect(selected as FacilityModel) } - selected={facility} + selected={facility ?? null} showAll freeText multiple={false} @@ -284,18 +282,11 @@ const DischargeModal = ({
    Discharge Prescription Medications - +
    Discharge PRN Prescriptions - +
    )} diff --git a/src/Components/Form/FormFields/PhoneNumberFormField.tsx b/src/Components/Form/FormFields/PhoneNumberFormField.tsx index 6aa70f801de..b6af2bfaa24 100644 --- a/src/Components/Form/FormFields/PhoneNumberFormField.tsx +++ b/src/Components/Form/FormFields/PhoneNumberFormField.tsx @@ -138,7 +138,7 @@ const phoneNumberTypeIcons: Record = { const PhoneNumberTypesHelp = ({ types }: { types: PhoneNumberType[] }) => (
    {types.map((type) => ( - + ["prescription"]>; onClose: (success: boolean) => void; } export default function AdministerMedicine({ prescription, ...props }: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const [isLoading, setIsLoading] = useState(false); const [notes, setNotes] = useState(""); const [isCustomTime, setIsCustomTime] = useState(false); @@ -52,13 +52,14 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { onClose={() => props.onClose(false)} onConfirm={async () => { setIsLoading(true); - const res = await dispatch( - props.actions.administer({ + const { res } = await request(MedicineRoutes.administerPrescription, { + pathParams: { consultation, external_id: prescription.id }, + body: { notes, administered_date: isCustomTime ? customTime : undefined, - }) - ); - if (res.status === 201) { + }, + }); + if (res?.ok) { Success({ msg: t("medicines_administered") }); } setIsLoading(false); @@ -67,11 +68,7 @@ export default function AdministerMedicine({ prescription, ...props }: Props) { className="w-full md:max-w-4xl" >
    - +
    ["create"]; onDone: () => void; }) { - const dispatch = useDispatch(); - const [isCreating, setIsCreating] = useState(false); const { t } = useTranslation(); + const consultation = useSlug("consultation"); + const [isCreating, setIsCreating] = useState(false); return ( disabled={isCreating} defaults={props.prescription} onCancel={props.onDone} - onSubmit={async (obj) => { - obj["medicine"] = obj.medicine_object?.id; - delete obj.medicine_object; + onSubmit={async (body) => { + body["medicine"] = body.medicine_object?.id; + delete body.medicine_object; setIsCreating(true); - const res = await dispatch(props.create(obj)); + const { res, error } = await request( + MedicineRoutes.createPrescription, + { + pathParams: { consultation }, + body, + } + ); setIsCreating(false); - if (res.status !== 201) { - return res.data; - } else { - props.onDone(); + + if (!res?.ok) { + return error; } + + Success({ msg: t("Medicine prescribed") }); + props.onDone(); }} noPadding validate={PrescriptionFormValidator()} diff --git a/src/Components/Medicine/DiscontinuePrescription.tsx b/src/Components/Medicine/DiscontinuePrescription.tsx index 38dd66a95a8..f54ba9974fc 100644 --- a/src/Components/Medicine/DiscontinuePrescription.tsx +++ b/src/Components/Medicine/DiscontinuePrescription.tsx @@ -1,22 +1,22 @@ import { useState } from "react"; -import { PrescriptionActions } from "../../Redux/actions"; import ConfirmDialog from "../Common/ConfirmDialog"; import { Prescription } from "./models"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import { Success } from "../../Utils/Notifications"; -import { useDispatch } from "react-redux"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; import { useTranslation } from "react-i18next"; +import request from "../../Utils/request/request"; +import MedicineRoutes from "./routes"; +import useSlug from "../../Common/hooks/useSlug"; interface Props { prescription: Prescription; - actions: ReturnType["prescription"]>; onClose: (discontinued: boolean) => void; } export default function DiscontinuePrescription(props: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const [isDiscontinuing, setIsDiscontinuing] = useState(false); const [discontinuedReason, setDiscontinuedReason] = useState(""); @@ -29,10 +29,13 @@ export default function DiscontinuePrescription(props: Props) { variant="danger" onConfirm={async () => { setIsDiscontinuing(true); - const res = await dispatch( - props.actions.discontinue(discontinuedReason) - ); - if (res.status === 201) { + const { res } = await request(MedicineRoutes.discontinuePrescription, { + pathParams: { consultation, external_id: props.prescription.id }, + body: { + discontinued_reason: discontinuedReason, + }, + }); + if (res?.ok) { Success({ msg: t("prescription_discontinued") }); } setIsDiscontinuing(false); @@ -41,11 +44,7 @@ export default function DiscontinuePrescription(props: Props) { className="w-full md:max-w-4xl" >
    - + { - const discontinue = await request(routes.discontinuePrescription, { - pathParams: { consultation_external_id, external_id: oldObj.id }, + const discontinue = await request(MedicineRoutes.discontinuePrescription, { + pathParams: { + consultation: consultation_external_id, + external_id: oldObj.id, + }, body: { discontinued_reason: discontinued_reason ? `Edit: ${discontinued_reason}` @@ -43,8 +46,8 @@ const handleSubmit = async ( return; } - const { res } = await request(routes.createPrescription, { - pathParams: { consultation_external_id }, + const { res } = await request(MedicineRoutes.createPrescription, { + pathParams: { consultation: consultation_external_id }, body: { ...newObj, // Forcing the medicine to be the same as the old one diff --git a/src/Components/Medicine/ManagePrescriptions.tsx b/src/Components/Medicine/ManagePrescriptions.tsx index 95d5b17d3c2..16e8ffedcb2 100644 --- a/src/Components/Medicine/ManagePrescriptions.tsx +++ b/src/Components/Medicine/ManagePrescriptions.tsx @@ -1,17 +1,11 @@ import { useTranslation } from "react-i18next"; import CareIcon from "../../CAREUI/icons/CareIcon"; import useAppHistory from "../../Common/hooks/useAppHistory"; -import { PrescriptionActions } from "../../Redux/actions"; import ButtonV2 from "../Common/components/ButtonV2"; import Page from "../Common/components/Page"; import PrescriptionBuilder from "./PrescriptionBuilder"; -interface Props { - consultationId: string; -} - -export default function ManagePrescriptions({ consultationId }: Props) { - const actions = PrescriptionActions(consultationId); +export default function ManagePrescriptions() { const { t } = useTranslation(); const { goBack } = useAppHistory(); @@ -23,13 +17,13 @@ export default function ManagePrescriptions({ consultationId }: Props) {

    {t("prescription_medications")}

    - +

    {t("prn_prescriptions")}

    - +
    diff --git a/src/Components/Medicine/MedicineAdministration.tsx b/src/Components/Medicine/MedicineAdministration.tsx index 5d8347ba5a5..d899a3800fb 100644 --- a/src/Components/Medicine/MedicineAdministration.tsx +++ b/src/Components/Medicine/MedicineAdministration.tsx @@ -1,27 +1,27 @@ import { useEffect, useMemo, useState } from "react"; -import { PrescriptionActions } from "../../Redux/actions"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; import { MedicineAdministrationRecord, Prescription } from "./models"; import TextAreaFormField from "../Form/FormFields/TextAreaFormField"; import CheckBoxFormField from "../Form/FormFields/CheckBoxFormField"; import ButtonV2 from "../Common/components/ButtonV2"; import CareIcon from "../../CAREUI/icons/CareIcon"; -import { useDispatch } from "react-redux"; import { Error, Success } from "../../Utils/Notifications"; -import { formatDateTime } from "../../Utils/utils"; +import { classNames, formatDateTime } from "../../Utils/utils"; import { useTranslation } from "react-i18next"; import dayjs from "../../Utils/dayjs"; import TextFormField from "../Form/FormFields/TextFormField"; +import request from "../../Utils/request/request"; +import MedicineRoutes from "./routes"; +import useSlug from "../../Common/hooks/useSlug"; interface Props { prescriptions: Prescription[]; - action: ReturnType["prescription"]; onDone: () => void; } export default function MedicineAdministration(props: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const [shouldAdminister, setShouldAdminister] = useState([]); const [notes, setNotes] = useState( [] @@ -46,34 +46,35 @@ export default function MedicineAdministration(props: Props) { ); }, [props.prescriptions]); - const handleSubmit = () => { - const records: MedicineAdministrationRecord[] = []; - prescriptions.forEach((prescription, i) => { - if (shouldAdminister[i]) { - records.push({ - prescription, - notes: notes[i], - administered_date: isCustomTime[i] ? customTime[i] : undefined, - }); - } - }); + const handleSubmit = async () => { + const administrations = prescriptions + .map((prescription, i) => ({ + prescription, + notes: notes[i], + administered_date: isCustomTime[i] ? customTime[i] : undefined, + })) + .filter((_, i) => shouldAdminister[i]); - Promise.all( - records.map(async ({ prescription, ...record }) => { - const res = await dispatch( - props.action(prescription?.id ?? "").administer(record) - ); - if (res.status !== 201) { - Error({ msg: t("medicines_administered_error") }); - } - }) - ).then(() => { - Success({ msg: t("medicines_administered") }); - props.onDone(); - }); + const ok = await Promise.all( + administrations.map(({ prescription, ...body }) => + request(MedicineRoutes.administerPrescription, { + pathParams: { consultation, external_id: prescription.id }, + body, + }).then(({ res }) => !!res?.ok) + ) + ); + + if (!ok) { + Error({ msg: t("medicines_administered_error") }); + return; + } + + Success({ msg: t("medicines_administered") }); + props.onDone(); }; const selectedCount = shouldAdminister.filter(Boolean).length; + const is_prn = prescriptions.some((obj) => obj.is_prn); return (
    @@ -82,10 +83,14 @@ export default function MedicineAdministration(props: Props) { key={obj.id} prescription={obj} readonly - actions={props.action(obj?.id ?? "")} selected={shouldAdminister[index]} > -
    +
    -
    +
    + dayjs(administration.administered_date).isBetween(start, end) + ) + .sort( + (a, b) => + new Date(a.administered_date!).getTime() - + new Date(b.administered_date!).getTime() + ); + + const hasComment = administered.some((obj) => !!obj.notes); + + if (administered.length) { + return ( + <> + setShowTimeline(false)} + title={ + + } + className="w-full md:max-w-4xl" + show={showTimeline} + > +
    + Administrations between{" "} + {formatTime(start, "HH:mm")} and{" "} + {formatTime(end, "HH:mm")} on{" "} + + {formatDateTime(start, "DD/MM/YYYY")} + +
    + +
    + + + ); + } + + // Check if cell belongs to after prescription.created_date + if (dayjs(start).isAfter(prescription.created_date)) { + return ; + } + + // Check if cell belongs to a discontinued prescription + if ( + prescription.discontinued && + dayjs(end).isAfter(prescription.discontinued_date) + ) { + if (!dayjs(prescription.discontinued_date).isBetween(start, end)) return; + + return ( +
    + + +

    + Discontinued on{" "} + {formatDateTime(prescription.discontinued_date)} +

    +

    + Reason:{" "} + {prescription.discontinued_reason ? ( + {prescription.discontinued_reason} + ) : ( + Not specified + )} +

    +
    +
    + ); + } +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx new file mode 100644 index 00000000000..a83fa38bd9c --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationEventSeperator.tsx @@ -0,0 +1,22 @@ +import { formatDateTime } from "../../../Utils/utils"; + +export default function AdministrationEventSeperator({ date }: { date: Date }) { + // Show date if it's 00:00 + if (date.getHours() === 0) { + return ( +
    + +

    {formatDateTime(date, "DD/MM")}

    +
    +
    + ); + } + + return ( +
    + + {/*

    {formatDateTime(date, "HH")}

    */} +
    +
    + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx new file mode 100644 index 00000000000..9de207146de --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTable.tsx @@ -0,0 +1,106 @@ +import { useTranslation } from "react-i18next"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import useRangePagination from "../../../Common/hooks/useRangePagination"; +import { classNames, formatDateTime } from "../../../Utils/utils"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import { Prescription } from "../models"; +import MedicineAdministrationTableRow from "./AdministrationTableRow"; + +interface Props { + prescriptions: Prescription[]; + pagination: ReturnType; + onRefetch: () => void; +} + +export default function MedicineAdministrationTable({ + pagination, + prescriptions, + onRefetch, +}: Props) { + const { t } = useTranslation(); + + return ( +
    + + + + + + + {pagination.slots?.map(({ start }, index) => ( + <> + + + + + + + + + {prescriptions.map((obj) => ( + + ))} + +
    +
    + {t("medicine")} + +

    Dosage &

    +

    {!prescriptions[0]?.is_prn ? "Frequency" : "Indicator"}

    +
    +
    +
    + + + + + {formatDateTime( + start, + start.getHours() === 0 ? "DD/MM" : "h a" + )} + + + ))} + + + + +
    +
    + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx new file mode 100644 index 00000000000..30ab68dcf0b --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/AdministrationTableRow.tsx @@ -0,0 +1,245 @@ +import { useTranslation } from "react-i18next"; +import { Prescription } from "../models"; +import { useState } from "react"; +import useQuery from "../../../Utils/request/useQuery"; +import MedicineRoutes from "../routes"; +import { classNames, formatDateTime } from "../../../Utils/utils"; +import useSlug from "../../../Common/hooks/useSlug"; +import DiscontinuePrescription from "../DiscontinuePrescription"; +import AdministerMedicine from "../AdministerMedicine"; +import DialogModal from "../../Common/Dialog"; +import PrescriptionDetailCard from "../PrescriptionDetailCard"; +import ButtonV2, { Cancel, Submit } from "../../Common/components/ButtonV2"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import EditPrescriptionForm from "../EditPrescriptionForm"; +import AdministrationEventSeperator from "./AdministrationEventSeperator"; +import AdministrationEventCell from "./AdministrationEventCell"; + +interface Props { + prescription: Prescription; + intervals: { start: Date; end: Date }[]; + refetch: () => void; +} + +export default function MedicineAdministrationTableRow({ + prescription, + ...props +}: Props) { + const { t } = useTranslation(); + const consultation = useSlug("consultation"); + // const [showActions, setShowActions] = useState(false); + const [showDetails, setShowDetails] = useState(false); + const [showEdit, setShowEdit] = useState(false); + const [showAdminister, setShowAdminister] = useState(false); + const [showDiscontinue, setShowDiscontinue] = useState(false); + + const { data, loading } = useQuery(MedicineRoutes.listAdministrations, { + pathParams: { consultation }, + query: { + prescription: prescription.id, + administered_date_after: formatDateTime( + props.intervals[0].start, + "YYYY-MM-DD" + ), + administered_date_before: formatDateTime( + props.intervals[props.intervals.length - 1].end, + "YYYY-MM-DD" + ), + archived: false, + }, + key: `${prescription.last_administered_on}`, + }); + + return ( + + {showDiscontinue && ( + { + setShowDiscontinue(false); + if (success) { + props.refetch(); + } + }} + /> + )} + {showAdminister && ( + { + setShowAdminister(false); + if (success) { + props.refetch(); + } + }} + /> + )} + {showDetails && ( + setShowDetails(false)} + className="w-full md:max-w-4xl" + show + > +
    + +
    + setShowDetails(false)} + label={t("close")} + /> + setShowDiscontinue(true)} + > + + {t("discontinue")} + + { + setShowDetails(false); + setShowEdit(true); + }} + > + + {t("edit")} + + setShowAdminister(true)} + > + + {t("administer")} + +
    +
    +
    + )} + {showEdit && ( + setShowEdit(false)} + show={showEdit} + title={`${t("edit")} ${t( + prescription.is_prn ? "prn_prescription" : "prescription_medication" + )}: ${ + prescription.medicine_object?.name ?? prescription.medicine_old + }`} + description={ +
    + + {t("edit_caution_note")} +
    + } + className="w-full max-w-3xl lg:min-w-[600px]" + > + { + setShowEdit(false); + if (success) { + props.refetch(); + } + }} + /> +
    + )} + setShowDetails(true)} + > +
    +
    + + {prescription.medicine_object?.name ?? prescription.medicine_old} + + + {prescription.discontinued && ( + + {t("discontinued")} + + )} + + {prescription.route && ( + + {t(prescription.route)} + + )} +
    + +
    +

    {prescription.dosage}

    +

    + {!prescription.is_prn + ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) + : prescription.indicator} +

    +
    +
    + + + + + {/* Administration Cells */} + {props.intervals.map(({ start, end }, index) => ( + <> + + + + + + {!data?.results ? ( + + ) : ( + + )} + + + ))} + + + {/* Action Buttons */} + + setShowAdminister(true)} + > + {t("administer")} + + + + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx b/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx new file mode 100644 index 00000000000..abd609871c8 --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/BulkAdminister.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from "react-i18next"; +import { Prescription } from "../models"; +import { useState } from "react"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import SlideOver from "../../../CAREUI/interactive/SlideOver"; +import MedicineAdministration from "../MedicineAdministration"; + +interface Props { + prescriptions: Prescription[]; + onDone: () => void; +} + +export default function BulkAdminister({ prescriptions, onDone }: Props) { + const { t } = useTranslation(); + const [showBulkAdminister, setShowBulkAdminister] = useState(false); + + return ( + <> + setShowBulkAdminister(true)} + className="w-full" + disabled={prescriptions.length === 0} + > + + {t("administer_medicines")} + {t("administer")} + + + { + setShowBulkAdminister(false); + onDone(); + }} + /> + + + ); +} diff --git a/src/Components/Medicine/MedicineAdministrationSheet/index.tsx b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx new file mode 100644 index 00000000000..187d5447314 --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/index.tsx @@ -0,0 +1,158 @@ +import { useTranslation } from "react-i18next"; +import useSlug from "../../../Common/hooks/useSlug"; +import useQuery from "../../../Utils/request/useQuery"; +import MedicineRoutes from "../routes"; +import { useMemo, useState } from "react"; +import { computeActivityBounds } from "./utils"; +import useBreakpoints from "../../../Common/hooks/useBreakpoints"; +import SubHeading from "../../../CAREUI/display/SubHeading"; +import ButtonV2 from "../../Common/components/ButtonV2"; +import CareIcon from "../../../CAREUI/icons/CareIcon"; +import BulkAdminister from "./BulkAdminister"; +import useRangePagination from "../../../Common/hooks/useRangePagination"; +import MedicineAdministrationTable from "./AdministrationTable"; +import Loading from "../../Common/Loading"; +import ScrollOverlay from "../../../CAREUI/interactive/ScrollOverlay"; + +interface Props { + readonly?: boolean; + is_prn: boolean; +} + +const DEFAULT_BOUNDS = { start: new Date(), end: new Date() }; + +const MedicineAdministrationSheet = ({ readonly, is_prn }: Props) => { + const { t } = useTranslation(); + const consultation = useSlug("consultation"); + + const [showDiscontinued, setShowDiscontinued] = useState(false); + + const filters = { is_prn, prescription_type: "REGULAR", limit: 100 }; + + const { data, loading, refetch } = useQuery( + MedicineRoutes.listPrescriptions, + { + pathParams: { consultation }, + query: { ...filters, discontinued: showDiscontinued ? undefined : false }, + } + ); + + const discontinuedPrescriptions = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { ...filters, discontinued: true }, + prefetch: !showDiscontinued, + }); + + const discontinuedCount = discontinuedPrescriptions.data?.count; + + const { activityTimelineBounds, prescriptions } = useMemo( + () => ({ + prescriptions: data?.results?.sort( + (a, b) => +a.discontinued - +b.discontinued + ), + activityTimelineBounds: data + ? computeActivityBounds(data.results) + : undefined, + }), + [data] + ); + + const daysPerPage = useBreakpoints({ default: 1, "2xl": 2 }); + const pagination = useRangePagination({ + bounds: activityTimelineBounds ?? DEFAULT_BOUNDS, + perPage: daysPerPage * 24 * 60 * 60 * 1000, + slots: (daysPerPage * 24) / 4, // Grouped by 4 hours + defaultEnd: true, + }); + + return ( +
    + + + + + {t("edit_prescriptions")} + + {t("edit")} + + refetch()} + /> + + ) + } + /> + + + Scroll to view more prescriptions + +
    + } + disableOverlay={loading || !prescriptions?.length} + > + {loading && } + {prescriptions?.length === 0 && } + + {!!prescriptions?.length && ( + { + refetch(); + discontinuedPrescriptions.refetch(); + }} + /> + )} + + {!showDiscontinued && !!discontinuedCount && ( + setShowDiscontinued(true)} + > + + + + Show {discontinuedCount} discontinued + prescription(s) + + + + )} + +
    + ); +}; + +export default MedicineAdministrationSheet; + +const NoPrescriptions = ({ prn }: { prn: boolean }) => { + return ( +
    + +

    + {prn + ? "No PRN Prescriptions Prescribed" + : "No Prescriptions Prescribed"} +

    +
    + ); +}; diff --git a/src/Components/Medicine/MedicineAdministrationSheet/utils.ts b/src/Components/Medicine/MedicineAdministrationSheet/utils.ts new file mode 100644 index 00000000000..93ee5fb4b08 --- /dev/null +++ b/src/Components/Medicine/MedicineAdministrationSheet/utils.ts @@ -0,0 +1,34 @@ +import { Prescription } from "../models"; + +export function computeActivityBounds(prescriptions: Prescription[]) { + // get start by finding earliest of all presciption's created_date + const start = new Date( + prescriptions.reduce( + (earliest, curr) => + earliest < curr.created_date ? earliest : curr.created_date, + prescriptions[0]?.created_date ?? new Date() + ) + ); + + // get end by finding latest of all presciption's last_administered_on + const end = new Date( + prescriptions + .filter((prescription) => prescription.last_administered_on) + .reduce( + (latest, curr) => + curr.last_administered_on && curr.last_administered_on > latest + ? curr.last_administered_on + : latest, + prescriptions[0]?.created_date ?? new Date() + ) + ); + + // floor start to 00:00 of the day + start.setHours(0, 0, 0, 0); + + // ceil end to 00:00 of the next day + end.setDate(end.getDate() + 1); + end.setHours(0, 0, 0, 0); + + return { start, end }; +} diff --git a/src/Components/Medicine/MedicineAdministrationsTable.tsx b/src/Components/Medicine/MedicineAdministrationsTable.tsx deleted file mode 100644 index 4ffc25e43f0..00000000000 --- a/src/Components/Medicine/MedicineAdministrationsTable.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import ResponsiveMedicineTable from "../Common/components/ResponsiveMedicineTables"; -import { formatDateTime } from "../../Utils/utils"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; -import { MedicineAdministrationRecord } from "./models"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import RecordMeta from "../../CAREUI/display/RecordMeta"; -import { useTranslation } from "react-i18next"; - -interface Props { - consultation_id: string; -} - -export default function MedicineAdministrationsTable({ - consultation_id, -}: Props) { - const { t } = useTranslation(); - const dispatch = useDispatch(); - const [items, setItems] = useState(); - - const { listAdministrations } = useMemo( - () => PrescriptionActions(consultation_id), - [consultation_id] - ); - - const fetchItems = useCallback(() => { - dispatch(listAdministrations()).then((res: any) => - setItems(res.data.results) - ); - }, [consultation_id]); - - useEffect(() => { - fetchItems(); - }, [consultation_id]); - - const lastModified = items?.[0]?.modified_date; - - return ( -
    -
    -
    - - {t("medicine_administration_history")} - -
    - - - {lastModified && formatDateTime(lastModified)} - -
    -
    -
    -
    -
    -
    - t(_))} - list={ - items?.map((obj) => ({ - ...obj, - medicine: - obj.prescription?.medicine_object?.name ?? - obj.prescription?.medicine_old, - created_date__pretty: ( - - by{" "} - {obj.administered_by?.first_name}{" "} - {obj.administered_by?.last_name} - - ), - ...obj, - })) || [] - } - objectKeys={["medicine", "notes", "created_date__pretty"]} - fieldsToDisplay={[2, 3]} - /> - {items?.length === 0 && ( -
    - {t("no_data_found")} -
    - )} -
    -
    -
    -
    - ); -} diff --git a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx b/src/Components/Medicine/PrescriptionAdministrationsTable.tsx deleted file mode 100644 index c80b66f44c6..00000000000 --- a/src/Components/Medicine/PrescriptionAdministrationsTable.tsx +++ /dev/null @@ -1,652 +0,0 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; -import { MedicineAdministrationRecord, Prescription } from "./models"; -import CareIcon from "../../CAREUI/icons/CareIcon"; -import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; -import SlideOver from "../../CAREUI/interactive/SlideOver"; -import MedicineAdministration from "./MedicineAdministration"; -import DiscontinuePrescription from "./DiscontinuePrescription"; -import AdministerMedicine from "./AdministerMedicine"; -import DialogModal from "../Common/Dialog"; -import PrescriptionDetailCard from "./PrescriptionDetailCard"; -import { useTranslation } from "react-i18next"; -import SubHeading from "../../CAREUI/display/SubHeading"; -import dayjs from "../../Utils/dayjs"; -import { - classNames, - formatDate, - formatDateTime, - formatTime, -} from "../../Utils/utils"; -import useRangePagination from "../../Common/hooks/useRangePagination"; -import EditPrescriptionForm from "./EditPrescriptionForm"; - -interface DateRange { - start: Date; - end: Date; -} - -interface Props { - prn: boolean; - prescription_type?: Prescription["prescription_type"]; - consultation_id: string; - readonly?: boolean; -} - -interface State { - prescriptions: Prescription[]; - administrationsTimeBounds: DateRange; -} - -export default function PrescriptionAdministrationsTable({ - prn, - consultation_id, - readonly, -}: Props) { - const dispatch = useDispatch(); - const { t } = useTranslation(); - - const [state, setState] = useState(); - const [showDiscontinued, setShowDiscontinued] = useState(false); - const [discontinuedCount, setDiscontinuedCount] = useState(); - const pagination = useRangePagination({ - bounds: state?.administrationsTimeBounds ?? { - start: new Date(), - end: new Date(), - }, - perPage: 24 * 60 * 60 * 1000, - slots: 24, - defaultEnd: true, - }); - const [showBulkAdminister, setShowBulkAdminister] = useState(false); - - const { list, prescription } = useMemo( - () => PrescriptionActions(consultation_id), - [consultation_id] - ); - - const refetch = useCallback(async () => { - const filters = { - is_prn: prn, - prescription_type: "REGULAR", - }; - - const res = await dispatch( - list(showDiscontinued ? filters : { ...filters, discontinued: false }) - ); - - setState({ - prescriptions: (res.data.results as Prescription[]).sort( - (a, b) => (a.discontinued ? 1 : 0) - (b.discontinued ? 1 : 0) - ), - administrationsTimeBounds: getAdministrationBounds(res.data.results), - }); - - if (showDiscontinued === false) { - const discontinuedRes = await dispatch( - list({ ...filters, discontinued: true, limit: 0 }) - ); - setDiscontinuedCount(discontinuedRes.data.count); - } - }, [consultation_id, showDiscontinued, dispatch]); - - useEffect(() => { - refetch(); - }, [refetch]); - - return ( -
    - {state?.prescriptions && ( - - { - setShowBulkAdminister(false); - refetch(); - }} - /> - - )} - - - - - - {t("edit_prescriptions")} - - {t("edit")} - - setShowBulkAdminister(true)} - className="w-full" - disabled={ - state === undefined || state.prescriptions.length === 0 - } - > - - - {t("administer_medicines")} - - {t("administer")} - - - ) - } - /> - -
    - - - - - - - {state === undefined - ? Array.from({ length: 24 }, (_, i) => i).map((i) => ( - - )) - : pagination.slots?.map(({ start, end }, index) => ( - - ))} - - - - - - - - {state?.prescriptions?.map((item) => ( - - ))} - -
    -
    - {t("medicine")} - -

    Dosage &

    -

    - {!state?.prescriptions[0]?.is_prn - ? "Frequency" - : "Indicator"} -

    -
    -
    -
    - - - - -

    -

    -

    {formatDateTime(start, "DD/MM")}

    -

    {formatDateTime(start, "HH:mm")}

    - - - Administration(s) between -
    - {formatTime(start)} and{" "} - {formatTime(end)} -
    - on {formatDate(start)} -
    -
    - - - -
    - - {showDiscontinued === false && !!discontinuedCount && ( - setShowDiscontinued(true)} - > - - - - Show {discontinuedCount} other discontinued - prescription(s) - - - - )} - - {state?.prescriptions.length === 0 && ( -
    - -

    - {prn - ? "No PRN Prescriptions Prescribed" - : "No Prescriptions Prescribed"} -

    -
    - )} -
    -
    - ); -} - -interface PrescriptionRowProps { - prescription: Prescription; - intervals: DateRange[]; - actions: ReturnType["prescription"]>; - refetch: () => void; -} - -const PrescriptionRow = ({ prescription, ...props }: PrescriptionRowProps) => { - const dispatch = useDispatch(); - const { t } = useTranslation(); - // const [showActions, setShowActions] = useState(false); - const [showDetails, setShowDetails] = useState(false); - const [showEdit, setShowEdit] = useState(false); - const [showAdminister, setShowAdminister] = useState(false); - const [showDiscontinue, setShowDiscontinue] = useState(false); - const [administrations, setAdministrations] = - useState(); - - useEffect(() => { - setAdministrations(undefined); - - const getAdministrations = async () => { - const res = await dispatch( - props.actions.listAdministrations({ - administered_date_after: formatDateTime( - props.intervals[0].start, - "YYYY-MM-DD" - ), - administered_date_before: formatDateTime( - props.intervals[props.intervals.length - 1].end, - "YYYY-MM-DD" - ), - }) - ); - - setAdministrations(res.data.results); - }; - - getAdministrations(); - }, [prescription.id, dispatch, props.intervals]); - - return ( - - {showDiscontinue && ( - { - setShowDiscontinue(false); - if (success) { - props.refetch(); - } - }} - /> - )} - {showAdminister && ( - { - setShowAdminister(false); - if (success) { - props.refetch(); - } - }} - /> - )} - {showDetails && ( - setShowDetails(false)} - className="w-full md:max-w-4xl" - show - > -
    - -
    - setShowDetails(false)} - label={t("close")} - /> - setShowDiscontinue(true)} - > - - {t("discontinue")} - - { - setShowDetails(false); - setShowEdit(true); - }} - > - - {t("edit")} - - setShowAdminister(true)} - > - - {t("administer")} - -
    -
    -
    - )} - {showEdit && ( - setShowEdit(false)} - show={showEdit} - title={`${t("edit")} ${t( - prescription.is_prn ? "prn_prescription" : "prescription_medication" - )}: ${ - prescription.medicine_object?.name ?? prescription.medicine_old - }`} - description={ -
    - - {t("edit_caution_note")} -
    - } - className="w-full max-w-3xl lg:min-w-[600px]" - > - { - setShowEdit(false); - if (success) { - props.refetch(); - } - }} - /> -
    - )} - setShowDetails(true)} - > -
    -
    - - {prescription.medicine_object?.name ?? prescription.medicine_old} - - - {prescription.discontinued && ( - - {t("discontinued")} - - )} - - {prescription.route && ( - - {t(prescription.route)} - - )} -
    - -
    -

    {prescription.dosage}

    -

    - {!prescription.is_prn - ? t("PRESCRIPTION_FREQUENCY_" + prescription.frequency) - : prescription.indicator} -

    -
    -
    - - - - {/* Administration Cells */} - {props.intervals.map(({ start, end }, index) => ( - - {administrations === undefined ? ( - - ) : ( - - )} - - ))} - - - {/* Action Buttons */} - - setShowAdminister(true)} - > - {t("administer")} - - - - ); -}; - -interface AdministrationCellProps { - administrations: MedicineAdministrationRecord[]; - interval: DateRange; - prescription: Prescription; -} - -const AdministrationCell = ({ - administrations, - interval: { start, end }, - prescription, -}: AdministrationCellProps) => { - // Check if cell belongs to an administered prescription - const administered = administrations.filter((administration) => - dayjs(administration.administered_date).isBetween(start, end) - ); - - if (administered.length) { - return ( -
    -
    - - {administered.length > 1 && ( - - {administered.length} - - )} -
    - -

    - Administered on{" "} - {formatDateTime(administered[0].administered_date)} -

    -

    - {administered.length > 1 - ? `Administered ${administered.length} times` - : `Administered ${formatTime(administered[0].administered_date)}`} -

    -
    -
    - ); - } - - // Check if cell belongs to a discontinued prescription - if ( - prescription.discontinued && - dayjs(end).isAfter(prescription.discontinued_date) - ) { - if (!dayjs(prescription.discontinued_date).isBetween(start, end)) return; - - return ( -
    - - -

    - Discontinued on{" "} - {formatDateTime(prescription.discontinued_date)} -

    -

    - Reason:{" "} - {prescription.discontinued_reason ? ( - {prescription.discontinued_reason} - ) : ( - Not specified - )} -

    -
    -
    - ); - } - - // Check if cell belongs to after prescription.created_date - if (dayjs(start).isAfter(prescription.created_date)) { - return ; - } - - // Check if prescription.created_date is between start and end - // if (dayjs(prescription.created_date).isBetween(start, end)) { - // return ( - //
    - // - // - //

    - // Prescribed on{" "} - // {formatDateTime(prescription.created_date)} - //

    - //
    - //
    - // ); - // } -}; - -function getAdministrationBounds(prescriptions: Prescription[]) { - // get start by finding earliest of all presciption's created_date - const start = new Date( - prescriptions.reduce( - (earliest, curr) => - earliest < curr.created_date ? earliest : curr.created_date, - prescriptions[0]?.created_date ?? new Date() - ) - ); - - // get end by finding latest of all presciption's last_administered_on - const end = new Date( - prescriptions - .filter((prescription) => prescription.last_administered_on) - .reduce( - (latest, curr) => - curr.last_administered_on && curr.last_administered_on > latest - ? curr.last_administered_on - : latest, - prescriptions[0]?.created_date ?? new Date() - ) - ); - - // floor start to previous hour - start.setMinutes(0, 0, 0); - - // ceil end to next hour - end.setMinutes(0, 0, 0); - end.setHours(end.getHours() + 1); - - return { start, end }; -} diff --git a/src/Components/Medicine/PrescriptionBuilder.tsx b/src/Components/Medicine/PrescriptionBuilder.tsx index f7a4dd49c93..39bf9b2f506 100644 --- a/src/Components/Medicine/PrescriptionBuilder.tsx +++ b/src/Components/Medicine/PrescriptionBuilder.tsx @@ -1,56 +1,49 @@ -import { useCallback, useEffect, useState } from "react"; +import { useState } from "react"; import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2 from "../Common/components/ButtonV2"; import { NormalPrescription, Prescription, PRNPrescription } from "./models"; import DialogModal from "../Common/Dialog"; import CreatePrescriptionForm from "./CreatePrescriptionForm"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import DiscontinuePrescription from "./DiscontinuePrescription"; import AdministerMedicine from "./AdministerMedicine"; import { useTranslation } from "react-i18next"; +import useQuery from "../../Utils/request/useQuery"; +import MedicineRoutes from "./routes"; +import useSlug from "../../Common/hooks/useSlug"; interface Props { prescription_type?: Prescription["prescription_type"]; - actions: ReturnType; is_prn?: boolean; disabled?: boolean; } export default function PrescriptionBuilder({ prescription_type, - actions, is_prn = false, disabled, }: Props) { const { t } = useTranslation(); - const dispatch = useDispatch(); - - const [prescriptions, setPrescriptions] = useState(); + const consultation = useSlug("consultation"); const [showCreate, setShowCreate] = useState(false); const [showDiscontinueFor, setShowDiscontinueFor] = useState(); const [showAdministerFor, setShowAdministerFor] = useState(); - const fetchPrescriptions = useCallback(() => { - dispatch(actions.list({ is_prn, prescription_type })).then((res: any) => - setPrescriptions(res.data.results) - ); - }, [dispatch, is_prn]); - - useEffect(() => { - fetchPrescriptions(); - }, []); + const { data, refetch } = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { is_prn, prescription_type, limit: 100 }, + }); return (
    {showDiscontinueFor && ( { setShowDiscontinueFor(undefined); - if (success) fetchPrescriptions(); + if (success) { + refetch(); + } }} key={showDiscontinueFor.id} /> @@ -58,20 +51,20 @@ export default function PrescriptionBuilder({ {showAdministerFor && ( { setShowAdministerFor(undefined); - if (success) fetchPrescriptions(); + if (success) { + refetch(); + } }} key={showAdministerFor.id} /> )}
    - {prescriptions?.map((obj, index) => ( + {data?.results.map((obj, index) => ( setShowDiscontinueFor(obj)} onAdministerClick={() => setShowAdministerFor(obj)} readonly={disabled} @@ -114,10 +107,9 @@ export default function PrescriptionBuilder({ prescription_type, } as Prescription } - create={actions.create} onDone={() => { setShowCreate(false); - fetchPrescriptions(); + refetch(); }} /> diff --git a/src/Components/Medicine/PrescriptionDetailCard.tsx b/src/Components/Medicine/PrescriptionDetailCard.tsx index bf27aa34068..4333eeb1a88 100644 --- a/src/Components/Medicine/PrescriptionDetailCard.tsx +++ b/src/Components/Medicine/PrescriptionDetailCard.tsx @@ -3,7 +3,6 @@ import CareIcon from "../../CAREUI/icons/CareIcon"; import { classNames } from "../../Utils/utils"; import ReadMore from "../Common/components/Readmore"; import ButtonV2 from "../Common/components/ButtonV2"; -import { PrescriptionActions } from "../../Redux/actions"; import { useTranslation } from "react-i18next"; import RecordMeta from "../../CAREUI/display/RecordMeta"; @@ -14,7 +13,6 @@ export default function PrescriptionDetailCard({ prescription: Prescription; readonly?: boolean; children?: React.ReactNode; - actions: ReturnType["prescription"]>; onDiscontinueClick?: () => void; onAdministerClick?: () => void; selected?: boolean; @@ -105,16 +103,17 @@ export default function PrescriptionDetailCard({ {prescription.indicator} {prescription.max_dosage} - {prescription.max_dosage} + {prescription.min_hours_between_doses && + prescription.min_hours_between_doses + " hrs."} ) : ( @@ -149,7 +148,7 @@ export default function PrescriptionDetailCard({
    - + Prescribed {props.children} ) : ( - {t("not_specified")} + + {t("not_specified")} + )}
    diff --git a/src/Components/Medicine/PrescriptionsTable.tsx b/src/Components/Medicine/PrescriptionsTable.tsx index 903f01b32a7..a1b039e71dd 100644 --- a/src/Components/Medicine/PrescriptionsTable.tsx +++ b/src/Components/Medicine/PrescriptionsTable.tsx @@ -1,8 +1,6 @@ -import { useCallback, useEffect, useMemo, useState } from "react"; -import ResponsiveMedicineTable from "../Common/components/ResponsiveMedicineTables"; +import { useState } from "react"; +import ResponsiveMedicineTable from "./ResponsiveMedicineTables"; import { formatDateTime } from "../../Utils/utils"; -import { PrescriptionActions } from "../../Redux/actions"; -import { useDispatch } from "react-redux"; import { Prescription } from "./models"; import CareIcon from "../../CAREUI/icons/CareIcon"; import ButtonV2, { Cancel, Submit } from "../Common/components/ButtonV2"; @@ -14,11 +12,13 @@ import AdministerMedicine from "./AdministerMedicine"; import DialogModal from "../Common/Dialog"; import PrescriptionDetailCard from "./PrescriptionDetailCard"; import { useTranslation } from "react-i18next"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import MedicineRoutes from "./routes"; interface Props { is_prn?: boolean; prescription_type?: Prescription["prescription_type"]; - consultation_id: string; onChange?: () => void; readonly?: boolean; } @@ -26,35 +26,22 @@ interface Props { export default function PrescriptionsTable({ is_prn = false, prescription_type = "REGULAR", - consultation_id, onChange, readonly, }: Props) { - const dispatch = useDispatch(); + const consultation = useSlug("consultation"); const { t } = useTranslation(); - - const [prescriptions, setPrescriptions] = useState(); const [showBulkAdminister, setShowBulkAdminister] = useState(false); const [showDiscontinueFor, setShowDiscontinueFor] = useState(); const [showAdministerFor, setShowAdministerFor] = useState(); const [detailedViewFor, setDetailedViewFor] = useState(); - const { list, prescription } = useMemo( - () => PrescriptionActions(consultation_id), - [consultation_id] - ); - - const fetchPrescriptions = useCallback(() => { - dispatch(list({ is_prn, prescription_type })).then((res: any) => - setPrescriptions(res.data.results) - ); - }, [consultation_id]); - - useEffect(() => { - fetchPrescriptions(); - }, [consultation_id]); + const { data } = useQuery(MedicineRoutes.listPrescriptions, { + pathParams: { consultation }, + query: { is_prn, prescription_type, limit: 100 }, + }); - const lastModified = prescriptions?.[0]?.modified_date; + const lastModified = data?.results[0]?.modified_date; const tkeys = prescription_type === "REGULAR" ? is_prn @@ -66,7 +53,7 @@ export default function PrescriptionsTable({ return (
    - {prescriptions && ( + {data?.results && ( { setShowBulkAdminister(false); onChange?.(); @@ -86,7 +72,6 @@ export default function PrescriptionsTable({ {showDiscontinueFor && ( { setShowDiscontinueFor(undefined); if (success) onChange?.(); @@ -97,7 +82,6 @@ export default function PrescriptionsTable({ {showAdministerFor && ( { setShowAdministerFor(undefined); if (success) onChange?.(); @@ -115,7 +99,6 @@ export default function PrescriptionsTable({
    @@ -198,7 +181,7 @@ export default function PrescriptionsTable({ maxWidthColumn={0} theads={Object.keys(tkeys).map((_) => t(_))} list={ - prescriptions?.map((obj) => ({ + data?.results.map((obj) => ({ ...obj, medicine: obj.medicine_object?.name ?? obj.medicine_old, route__pretty: @@ -277,7 +260,7 @@ export default function PrescriptionsTable({ : undefined } /> - {prescriptions?.length === 0 && ( + {data?.results.length === 0 && (
    {t("no_data_found")}
    diff --git a/src/Components/Medicine/PrescrpitionTimeline.tsx b/src/Components/Medicine/PrescrpitionTimeline.tsx new file mode 100644 index 00000000000..7c6d4479e3b --- /dev/null +++ b/src/Components/Medicine/PrescrpitionTimeline.tsx @@ -0,0 +1,227 @@ +import dayjs from "../../Utils/dayjs"; +import useSlug from "../../Common/hooks/useSlug"; +import useQuery from "../../Utils/request/useQuery"; +import { classNames, formatDateTime } from "../../Utils/utils"; +import { MedicineAdministrationRecord, Prescription } from "./models"; +import MedicineRoutes from "./routes"; +import Timeline, { + TimelineEvent, + TimelineNode, + TimelineNodeNotes, +} from "../../CAREUI/display/Timeline"; +import ButtonV2 from "../Common/components/ButtonV2"; +import { useState } from "react"; +import ConfirmDialog from "../Common/ConfirmDialog"; +import request from "../../Utils/request/request"; +import RecordMeta from "../../CAREUI/display/RecordMeta"; +import CareIcon from "../../CAREUI/icons/CareIcon"; + +interface MedicineAdministeredEvent extends TimelineEvent<"administered"> { + administration: MedicineAdministrationRecord; +} + +type PrescriptionTimelineEvents = + | TimelineEvent<"created" | "discontinued"> + | MedicineAdministeredEvent; + +interface Props { + interval: { start: Date; end: Date }; + prescription: Prescription; + showPrescriptionDetails?: boolean; +} + +export default function PrescrpitionTimeline({ + prescription, + interval, +}: Props) { + const consultation = useSlug("consultation"); + const { data, refetch, loading } = useQuery( + MedicineRoutes.listAdministrations, + { + pathParams: { consultation }, + query: { + prescription: prescription.id, + administered_date_after: formatDateTime(interval.start, "YYYY-MM-DD"), + administered_date_before: formatDateTime(interval.end, "YYYY-MM-DD"), + }, + } + ); + + const events = data && compileEvents(prescription, data.results, interval); + + if (loading && !data) { + return ( +
    + +
    + ); + } + + return ( + + {events?.map((event, index) => { + switch (event.type) { + case "created": + case "discontinued": + return ( + + ); + + case "administered": + return ( + + ); + } + })} + + ); +} + +const MedicineAdministeredNode = ({ + event, + onArchived, + isLastNode, + hideArchive, +}: { + event: MedicineAdministeredEvent; + onArchived: () => void; + isLastNode: boolean; + hideArchive?: boolean; +}) => { + const consultation = useSlug("consultation"); + const [showArchiveConfirmation, setShowArchiveConfirmation] = useState(false); + const [isArchiving, setIsArchiving] = useState(false); + + return ( + <> + setShowArchiveConfirmation(true)} + > + Archive + + ) + } + isLast={isLastNode} + > + {event.cancelled && ( + + + Prescription was archived{" "} + + + + )} + + { + setIsArchiving(true); + + const { res } = await request(MedicineRoutes.archiveAdministration, { + pathParams: { consultation, external_id: event.administration.id }, + }); + + if (res?.status === 200) { + setIsArchiving(false); + setShowArchiveConfirmation(false); + onArchived(); + } + }} + onClose={() => setShowArchiveConfirmation(false)} + /> + + ); +}; + +const compileEvents = ( + prescription: Prescription, + administrations: MedicineAdministrationRecord[], + interval: { start: Date; end: Date } +): PrescriptionTimelineEvents[] => { + const events: PrescriptionTimelineEvents[] = []; + + if ( + dayjs(prescription.created_date).isBetween(interval.start, interval.end) + ) { + events.push({ + type: "created", + icon: "l-plus-circle", + timestamp: prescription.created_date, + by: prescription.prescribed_by, + notes: prescription.notes, + }); + } + + administrations + .sort( + (a, b) => + new Date(a.created_date).getTime() - new Date(b.created_date).getTime() + ) + .forEach((administration) => { + events.push({ + type: "administered", + icon: "l-syringe", + timestamp: administration.created_date, + by: administration.administered_by, + cancelled: !!administration.archived_on, + administration, + notes: administration.notes, + }); + }); + + if ( + prescription?.discontinued && + dayjs(prescription.discontinued_date).isBetween( + interval.start, + interval.end + ) + ) { + events.push({ + type: "discontinued", + icon: "l-times-circle", + timestamp: prescription.discontinued_date, + by: undefined, + notes: prescription.discontinued_reason, + }); + } + + return events; +}; diff --git a/src/Components/Common/components/ResponsiveMedicineTables.tsx b/src/Components/Medicine/ResponsiveMedicineTables.tsx similarity index 97% rename from src/Components/Common/components/ResponsiveMedicineTables.tsx rename to src/Components/Medicine/ResponsiveMedicineTables.tsx index bbed23e7c07..ccec9a7c69d 100644 --- a/src/Components/Common/components/ResponsiveMedicineTables.tsx +++ b/src/Components/Medicine/ResponsiveMedicineTables.tsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import AccordionV2 from "./AccordionV2"; -import { classNames } from "../../../Utils/utils"; +import AccordionV2 from "../Common/components/AccordionV2"; +import { classNames } from "../../Utils/utils"; function getWindowSize() { const { innerWidth, innerHeight } = window; diff --git a/src/Components/Medicine/models.ts b/src/Components/Medicine/models.ts index 0c49d199b21..ee42b03eca2 100644 --- a/src/Components/Medicine/models.ts +++ b/src/Components/Medicine/models.ts @@ -10,7 +10,7 @@ interface BasePrescription { notes?: string; meta?: object; readonly prescription_type?: "DISCHARGE" | "REGULAR"; - readonly discontinued?: boolean; + readonly discontinued: boolean; discontinued_reason?: string; readonly prescribed_by: PerformedByModel; readonly discontinued_date: string; @@ -50,13 +50,15 @@ export interface PRNPrescription extends BasePrescription { export type Prescription = NormalPrescription | PRNPrescription; export type MedicineAdministrationRecord = { - readonly id?: string; - readonly prescription?: Prescription; + readonly id: string; + readonly prescription: Prescription; notes: string; administered_date?: string; - readonly administered_by?: PerformedByModel; - readonly created_date?: string; - readonly modified_date?: string; + readonly administered_by: PerformedByModel; + readonly archived_by: PerformedByModel | undefined; + readonly archived_on: string | undefined; + readonly created_date: string; + readonly modified_date: string; }; export type MedibaseMedicine = { diff --git a/src/Components/Medicine/routes.ts b/src/Components/Medicine/routes.ts new file mode 100644 index 00000000000..c0c4a602227 --- /dev/null +++ b/src/Components/Medicine/routes.ts @@ -0,0 +1,59 @@ +import { Type } from "../../Redux/api"; +import { PaginatedResponse } from "../../Utils/request/types"; +import { MedicineAdministrationRecord, Prescription } from "./models"; + +const MedicineRoutes = { + listPrescriptions: { + path: "/api/v1/consultation/{consultation}/prescriptions/", + method: "GET", + TRes: Type>(), + }, + + createPrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/", + method: "POST", + TBody: Type(), + TRes: Type(), + }, + + listAdministrations: { + path: "/api/v1/consultation/{consultation}/prescription_administration/", + method: "GET", + TRes: Type>(), + }, + + getAdministration: { + path: "/api/v1/consultation/{consultation}/prescription_administration/{external_id}/", + method: "GET", + TRes: Type(), + }, + + getPrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/", + method: "GET", + TRes: Type(), + }, + + administerPrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/administer/", + method: "POST", + TBody: Type>(), + TRes: Type(), + }, + + discontinuePrescription: { + path: "/api/v1/consultation/{consultation}/prescriptions/{external_id}/discontinue/", + method: "POST", + TBody: Type<{ discontinued_reason: string }>(), + TRes: Type>(), + }, + + archiveAdministration: { + path: "/api/v1/consultation/{consultation}/prescription_administration/{external_id}/archive/", + method: "POST", + TBody: Type>(), + TRes: Type>(), + }, +} as const; + +export default MedicineRoutes; diff --git a/src/Redux/actions.tsx b/src/Redux/actions.tsx index 15934d0957d..b066b98eec5 100644 --- a/src/Redux/actions.tsx +++ b/src/Redux/actions.tsx @@ -1,9 +1,5 @@ import { HCXClaimModel, HCXPolicyModel } from "../Components/HCX/models"; -import { - MedibaseMedicine, - MedicineAdministrationRecord, - Prescription, -} from "../Components/Medicine/models"; +import { MedibaseMedicine } from "../Components/Medicine/models"; import { fireRequest, fireRequestForFiles } from "./fireRequest"; export const getConfig = () => { @@ -858,74 +854,6 @@ export const getAssetAvailability = (id: string) => export const listPMJYPackages = (query?: string) => fireRequest("listPMJYPackages", [], { query }); -/** Prescription related actions */ -export const PrescriptionActions = (consultation_external_id: string) => { - const pathParams = { consultation_external_id }; - - return { - list: (query?: Record) => { - let altKey; - if (query?.is_prn !== undefined) { - altKey = query?.is_prn - ? "listPRNPrescriptions" - : "listNormalPrescriptions"; - } - return fireRequest("listPrescriptions", [], query, pathParams, altKey); - }, - - create: (obj: Prescription) => - fireRequest("createPrescription", [], obj, pathParams), - - listAdministrations: (query?: object) => - fireRequest("listAdministrations", [], query, pathParams), - - getAdministration: (external_id: string) => - fireRequest("getAdministration", [], {}, { ...pathParams, external_id }), - - /** Returns actions specific to a prescription */ - prescription(external_id: string) { - const pathParams = { consultation_external_id, external_id }; - - return { - /** Read a specific prescription of a consultation */ - get: () => fireRequest("getPrescription", [], {}, pathParams), - - /** Administer a prescription */ - administer: (obj: MedicineAdministrationRecord) => - fireRequest( - "administerPrescription", - [], - obj, - pathParams, - `administer-medicine-${external_id}` - ), - - listAdministrations: (query?: { - administered_date_after?: string; - administered_date_before?: string; - }) => - fireRequest( - "listAdministrations", - [], - { prescription: external_id, ...query }, - pathParams, - `list-administrations-${external_id}` - ), - - /** Discontinue a prescription */ - discontinue: (discontinued_reason: string | undefined) => - fireRequest( - "discontinuePrescription", - [], - { discontinued_reason }, - pathParams, - `discontinue-medicine-${external_id}` - ), - }; - }, - }; -}; - // HCX Actions export const HCXActions = { checkEligibility: (policy: string) => { diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 01c356f6603..9ec5caff4a6 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -41,9 +41,6 @@ import { ILocalBodyByDistrict, IPartialUpdateExternalResult, } from "../Components/ExternalResult/models"; - -import { Prescription } from "../Components/Medicine/models"; - import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; import { PatientModel } from "../Components/Patient/models"; @@ -53,7 +50,7 @@ import { IComment, IResource } from "../Components/Resource/models"; * A fake function that returns an empty object casted to type T * @returns Empty object as type T */ -function Type(): T { +export function Type(): T { return {} as T; } @@ -1115,47 +1112,6 @@ const routes = { method: "GET", }, - // Prescription endpoints - - listPrescriptions: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/", - method: "GET", - }, - - createPrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/", - method: "POST", - TBody: Type(), - TRes: Type(), - }, - - listAdministrations: { - path: "/api/v1/consultation/{consultation_external_id}/prescription_administration/", - method: "GET", - }, - - getAdministration: { - path: "/api/v1/consultation/{consultation_external_id}/prescription_administration/{external_id}/", - method: "GET", - }, - - getPrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/", - method: "GET", - }, - - administerPrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/administer/", - method: "POST", - }, - - discontinuePrescription: { - path: "/api/v1/consultation/{consultation_external_id}/prescriptions/{external_id}/discontinue/", - method: "POST", - TBody: Type<{ discontinued_reason: string }>(), - TRes: Type>(), - }, - // HCX Endpoints listPMJYPackages: { diff --git a/src/Utils/request/useQuery.ts b/src/Utils/request/useQuery.ts index 2a8cb2e2ad4..2dab2910278 100644 --- a/src/Utils/request/useQuery.ts +++ b/src/Utils/request/useQuery.ts @@ -6,6 +6,7 @@ import { mergeRequestOptions } from "./utils"; export interface QueryOptions extends RequestOptions { prefetch?: boolean; refetchOnWindowFocus?: boolean; + key?: string; } export default function useQuery( diff --git a/src/Utils/request/utils.ts b/src/Utils/request/utils.ts index f159df1b7a7..ec919c79490 100644 --- a/src/Utils/request/utils.ts +++ b/src/Utils/request/utils.ts @@ -1,4 +1,5 @@ import { LocalStorageKeys } from "../../Common/constants"; +import * as Notification from "../Notifications"; import { QueryParams, RequestOptions } from "./types"; export function makeUrl( @@ -38,6 +39,9 @@ const ensurePathNotMissingReplacements = (path: string) => { const missingParams = path.match(/\{.*\}/g); if (missingParams) { + Notification.Error({ + msg: `Missing path params: ${missingParams.join(", ")}`, + }); throw new Error(`Missing path params: ${missingParams.join(", ")}`); } }; @@ -78,7 +82,7 @@ export function mergeRequestOptions( ...overrides, query: { ...options.query, ...overrides.query }, - body: { ...options.body, ...overrides.body }, + body: { ...(options.body ?? {}), ...(overrides.body ?? {}) }, pathParams: { ...options.pathParams, ...overrides.pathParams }, onResponse: (res) => { diff --git a/src/Utils/utils.ts b/src/Utils/utils.ts index a96a4c65146..34d4070dbe9 100644 --- a/src/Utils/utils.ts +++ b/src/Utils/utils.ts @@ -92,6 +92,10 @@ export const relativeDate = (date: DateLike) => { return `${obj.fromNow()} at ${obj.format(TIME_FORMAT)}`; }; +export const formatName = (user: { first_name: string; last_name: string }) => { + return `${user.first_name} ${user.last_name}`; +}; + export const relativeTime = (time?: DateLike) => { return `${dayjs(time).fromNow()}`; }; From e7fbde8f54095f317ae3fd886134e698d3623bf6 Mon Sep 17 00:00:00 2001 From: Shyam Prakash <106866225+shyamprakash123@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:27:11 +0530 Subject: [PATCH 11/58] =?UTF-8?q?=F0=9F=94=A7=20Hide=20Camera=20Feed=20Tab?= =?UTF-8?q?=20for=20bed=20with=20no=20camera=20attached.=20(#6536)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Hide Camera Feed Tab for bed with no camera attached * Resolved requested changes. --- .../Facility/ConsultationDetails/index.tsx | 15 +++++++++++++++ src/Components/Facility/Consultations/Feed.tsx | 4 +--- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/Components/Facility/ConsultationDetails/index.tsx b/src/Components/Facility/ConsultationDetails/index.tsx index 6fda874e3b3..f2ef13fa833 100644 --- a/src/Components/Facility/ConsultationDetails/index.tsx +++ b/src/Components/Facility/ConsultationDetails/index.tsx @@ -8,6 +8,7 @@ import { ConsultationModel, ICD11DiagnosisModel } from "../models"; import { getConsultation, getPatient, + listAssetBeds, listShiftRequests, } from "../../../Redux/actions"; import { statusType, useAbortableEffect } from "../../../Common/utils"; @@ -88,6 +89,7 @@ export const ConsultationDetails = (props: any) => { const [openDischargeSummaryDialog, setOpenDischargeSummaryDialog] = useState(false); const [openDischargeDialog, setOpenDischargeDialog] = useState(false); + const [isCameraAttached, setIsCameraAttached] = useState(false); const getPatientGender = (patientData: any) => GENDER_TYPES.find((i) => i.id === patientData.gender)?.text; @@ -126,6 +128,17 @@ export const ConsultationDetails = (props: any) => { data.symptoms_text = symptoms.join(", "); } setConsultationData(data); + const assetRes = await dispatch( + listAssetBeds({ + bed: data?.current_bed?.bed_object?.id, + }) + ); + const isCameraAttachedRes = assetRes.data.results.some( + (asset: { asset_object: { asset_class: string } }) => { + return asset?.asset_object?.asset_class === "ONVIF"; + } + ); + setIsCameraAttached(isCameraAttachedRes); const id = res.data.patient; const patientRes = await dispatch(getPatient({ id })); if (patientRes?.data) { @@ -336,6 +349,7 @@ export const ConsultationDetails = (props: any) => { Doctor Connect {patientData.last_consultation?.id && + isCameraAttached && ["DistrictAdmin", "StateAdmin", "Doctor"].includes( authUser.user_type ) && ( @@ -507,6 +521,7 @@ export const ConsultationDetails = (props: any) => { {CONSULTATION_TABS.map((p: OptionsType) => { if (p.text === "FEED") { if ( + isCameraAttached === false || // No camera attached consultationData?.discharge_date || // Discharged !consultationData?.current_bed?.bed_object?.id || // Not admitted to bed !["DistrictAdmin", "StateAdmin", "Doctor"].includes( diff --git a/src/Components/Facility/Consultations/Feed.tsx b/src/Components/Facility/Consultations/Feed.tsx index 3448d85a6a2..ac39be325e9 100644 --- a/src/Components/Facility/Consultations/Feed.tsx +++ b/src/Components/Facility/Consultations/Feed.tsx @@ -124,9 +124,7 @@ export const Feed: React.FC = ({ consultationId, facilityId }) => { ...bedAssets.data, results: bedAssets.data.results.filter( (asset: { asset_object: { meta: { asset_type: string } } }) => { - return asset?.asset_object?.meta?.asset_type === "CAMERA" - ? true - : false; + return asset?.asset_object?.meta?.asset_type === "CAMERA"; } ), }, From 46c19a70a785d6ee31f7048dddd82e77e82c15e9 Mon Sep 17 00:00:00 2001 From: Gampa Sri Harsh <114745442+sriharsh05@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:28:01 +0530 Subject: [PATCH 12/58] Remove redundant facility selection pop-up for single facility users (#6535) * Remove redundant facility selection pop-up for single facility users * change permittedData variable name to permittedFacilities --- src/Components/Patient/ManagePatients.tsx | 17 ++++++++++++++--- src/Redux/api.tsx | 1 + 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Components/Patient/ManagePatients.tsx b/src/Components/Patient/ManagePatients.tsx index 7a78d94582e..24762d10279 100644 --- a/src/Components/Patient/ManagePatients.tsx +++ b/src/Components/Patient/ManagePatients.tsx @@ -49,6 +49,8 @@ import Page from "../Common/components/Page.js"; import dayjs from "dayjs"; import { triggerGoal } from "../../Integrations/Plausible.js"; import useAuthUser from "../../Common/hooks/useAuthUser.js"; +import useQuery from "../../Utils/request/useQuery.js"; +import routes from "../../Redux/api.js"; const Loading = lazy(() => import("../Common/Loading")); @@ -477,6 +479,11 @@ export const PatientManager = () => { [fetchFacilityBadgeName, fetchLocationBadgeName] ); + const { data: permittedFacilities } = useQuery( + routes.getPermittedFacilities, + {} + ); + const LastAdmittedToTypeBadges = () => { const badge = (key: string, value: any, id: string) => { return ( @@ -781,9 +788,13 @@ export const PatientManager = () => { { - qParams.facility - ? navigate(`/facility/${qParams.facility}/patient`) - : setShowDialog(true); + if (qParams.facility) + navigate(`/facility/${qParams.facility}/patient`); + else if (permittedFacilities?.results.length === 1) + navigate( + `/facility/${permittedFacilities?.results[0].id}/patient` + ); + else setShowDialog(true); }} className="w-full lg:w-fit" > diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 9ec5caff4a6..7194aba5278 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -219,6 +219,7 @@ const routes = { getPermittedFacilities: { path: "/api/v1/facility/", + TRes: Type>(), }, getAllFacilities: { From 585cb5a53dc26488e7527c455c94a7505c349155 Mon Sep 17 00:00:00 2001 From: Shyam Prakash <106866225+shyamprakash123@users.noreply.github.com> Date: Tue, 7 Nov 2023 09:28:16 +0530 Subject: [PATCH 13/58] Added Loading component while the assets are being fetched. (#6532) --- src/Components/Assets/AssetsList.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Components/Assets/AssetsList.tsx b/src/Components/Assets/AssetsList.tsx index 593afdb137c..8c19928fbee 100644 --- a/src/Components/Assets/AssetsList.tsx +++ b/src/Components/Assets/AssetsList.tsx @@ -65,7 +65,7 @@ const AssetsList = () => { status: qParams.status || "", }; - useQuery(routes.listAssets, { + const { loading } = useQuery(routes.listAssets, { query: params, onResponse: ({ res, data }) => { if (res?.status === 200 && data) { @@ -176,7 +176,13 @@ const AssetsList = () => { ); let manageAssets = null; - if (assetsExist) { + if (loading) { + manageAssets = ( +
    + +
    + ); + } else if (assetsExist) { manageAssets = (
    {assets.map((asset: AssetData) => ( @@ -309,7 +315,7 @@ const AssetsList = () => { From e8091bf0640220596a837c21af671fe1f78b422d Mon Sep 17 00:00:00 2001 From: Gampa Sri Harsh <114745442+sriharsh05@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:54:28 +0530 Subject: [PATCH 14/58] change the NIBP display from last 30 mins to current bed assignment date (#6479) * change the NIBP display from last 30 mins to currect bed assignment date * change model name to PascalCase and used PaginatedResponse to wrap the model * use onResponse for useQuery * resolved merge conflict * move bed assignment date fetch logic to parent component * replaced console log with notification for error * change logic by passing date to VitalsMonitor and change models * remove unused models and useState variables * update the CNS to pass the bedAssignmentStartDate prop to HL7PatientVitalsMonitor component * Reverted last commit * update the CNS to pass the correct bedAssignmentStartDate to the VitalsMonitor component * remove usage of useQuery and replaced the prop with existing data * fix NIBP visibility bug in Vitals Monitor * delete unused function --------- Co-authored-by: Mohammed Nihal <57055998+nihal467@users.noreply.github.com> --- src/Components/Facility/CentralNursingStation.tsx | 4 ++++ .../ConsultationDetails/ConsultationUpdatesTab.tsx | 8 ++++++++ src/Components/VitalsMonitor/HL7PatientVitalsMonitor.tsx | 8 ++------ src/Components/VitalsMonitor/types.ts | 1 + src/Redux/api.tsx | 3 +++ 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/src/Components/Facility/CentralNursingStation.tsx b/src/Components/Facility/CentralNursingStation.tsx index 0d510d8459e..26bed83635b 100644 --- a/src/Components/Facility/CentralNursingStation.tsx +++ b/src/Components/Facility/CentralNursingStation.tsx @@ -273,6 +273,10 @@ export default function CentralNursingStation({ facilityId }: Props) { {data.map((props) => (
    {
    {
    { return `${dayjs().diff(dayjs(timestamp), "minute")}m ago`; }; -const isWithinMinutes = (timestamp: string, minutes: number) => { - return dayjs().diff(dayjs(timestamp), "minute") < minutes; -}; - export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { const { connect, waveformCanvas, data, isOnline } = useHL7VitalsMonitor( props.config @@ -39,8 +35,8 @@ export default function HL7PatientVitalsMonitor(props: IVitalsComponentProps) { connect(props.socketUrl); }, [props.socketUrl]); - const bpWithinMaxPersistence = !!( - (data.bp?.["date-time"] && isWithinMinutes(data.bp?.["date-time"], 30)) // Max blood pressure persistence is 30 minutes + const bpWithinMaxPersistence = dayjs(data.bp?.["date-time"]).isAfter( + props.patientCurrentBedAssignmentDate ); return ( diff --git a/src/Components/VitalsMonitor/types.ts b/src/Components/VitalsMonitor/types.ts index 60979a6f9b0..c73d7399b59 100644 --- a/src/Components/VitalsMonitor/types.ts +++ b/src/Components/VitalsMonitor/types.ts @@ -46,6 +46,7 @@ export interface ChannelOptions { } export interface IVitalsComponentProps { + patientCurrentBedAssignmentDate?: string; patientAssetBed?: PatientAssetBed; socketUrl: string; config?: ReturnType; diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 7194aba5278..0e3fabf0bff 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -1,4 +1,5 @@ import { IConfig } from "../Common/hooks/useConfig"; + import { IAadhaarOtp, IAadhaarOtpTBody, @@ -29,6 +30,7 @@ import { } from "../Components/Assets/AssetTypes"; import { ConsultationModel, + CurrentBed, FacilityModel, LocationModel, WardModel, @@ -355,6 +357,7 @@ const routes = { listConsultationBeds: { path: "/api/v1/consultationbed/", method: "GET", + TRes: Type>(), }, createConsultationBed: { path: "/api/v1/consultationbed/", From bf932d59a6964daa2cc5aeec5b14be6406b65fee Mon Sep 17 00:00:00 2001 From: adriansliva <95087987+adriansliva@users.noreply.github.com> Date: Tue, 7 Nov 2023 12:27:36 +0100 Subject: [PATCH 15/58] Replace useDispatch w. useQuery/request: Notifications (src/Components/Notifications/**) #6392 (#6543) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add only imports. other functionality and changes in code to be complited later * ShowPushNotification - change useDispatch with useQuery, old code is on the comments, add model.tsx in to folder notifications and add references into api.tsx * Complete ShowPushNotification.tsx and NoticeBoard.tsx - change useDispatch with useQuery * Complete ShowPushNotification.tsx and NoticeBoard.tsx - change useDispatch with useQuery, delete all commnets of old code * Complete NotificationsList.tsx - change useDispatch with useQuery, delete all commnets of old code * deleting all comments * code modification according to requests * back the original content of package-lock.json from origin branch --------- Co-authored-by: Adrián Sliva --- package-lock.json | 2 +- src/Components/Notifications/NoticeBoard.tsx | 36 +++-------- .../Notifications/NotificationsList.tsx | 63 ++++++++++--------- .../Notifications/ShowPushNotification.tsx | 43 ++++++------- src/Components/Notifications/models.tsx | 29 +++++++++ src/Redux/api.tsx | 15 +++++ 6 files changed, 106 insertions(+), 82 deletions(-) create mode 100644 src/Components/Notifications/models.tsx diff --git a/package-lock.json b/package-lock.json index baa3205a234..0da53d597ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23708,4 +23708,4 @@ } } } -} \ No newline at end of file +} diff --git a/src/Components/Notifications/NoticeBoard.tsx b/src/Components/Notifications/NoticeBoard.tsx index 7fd16efb3cf..8ad3afba0f8 100644 --- a/src/Components/Notifications/NoticeBoard.tsx +++ b/src/Components/Notifications/NoticeBoard.tsx @@ -1,43 +1,23 @@ -import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { getNotifications } from "../../Redux/actions"; import Page from "../Common/components/Page"; import Loading from "../Common/Loading"; import { formatDateTime } from "../../Utils/utils"; import { useTranslation } from "react-i18next"; import CareIcon from "../../CAREUI/icons/CareIcon"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; export const NoticeBoard = () => { - const dispatch: any = useDispatch(); - const [isLoading, setIsLoading] = useState(true); - const [data, setData] = useState([]); const { t } = useTranslation(); - - useEffect(() => { - setIsLoading(true); - dispatch( - getNotifications( - { offset: 0, event: "MESSAGE", medium_sent: "SYSTEM" }, - new Date().getTime().toString() - ) - ) - .then((res: any) => { - if (res && res.data) { - setData(res.data.results); - } - setIsLoading(false); - }) - .catch(() => { - setIsLoading(false); - }); - }, [dispatch]); + const { data, loading } = useQuery(routes.getNotifications, { + query: { offset: 0, event: "MESSAGE", medium_sent: "SYSTEM" }, + }); let notices; - if (data?.length) { + if (data?.results.length) { notices = (
    - {data.map((item) => ( + {data.results.map((item) => (
    { ); } - if (isLoading) return ; + if (loading) return ; return (
    {notices}
    diff --git a/src/Components/Notifications/NotificationsList.tsx b/src/Components/Notifications/NotificationsList.tsx index 5f124516a14..864208a432f 100644 --- a/src/Components/Notifications/NotificationsList.tsx +++ b/src/Components/Notifications/NotificationsList.tsx @@ -1,13 +1,5 @@ import { navigate } from "raviger"; import { useEffect, useState } from "react"; -import { useDispatch } from "react-redux"; -import { - getNotifications, - markNotificationAsRead, - getUserPnconfig, - updateUserPnconfig, - getPublicKey, -} from "../../Redux/actions"; import Spinner from "../Common/Spinner"; import { NOTIFICATION_EVENTS } from "../../Common/constants"; import { Error } from "../../Utils/Notifications.js"; @@ -24,6 +16,8 @@ import SelectMenuV2 from "../Form/SelectMenuV2"; import { useTranslation } from "react-i18next"; import CircularProgress from "../Common/components/CircularProgress"; import useAuthUser from "../../Common/hooks/useAuthUser"; +import request from "../../Utils/request/request"; +import routes from "../../Redux/api"; const RESULT_LIMIT = 14; @@ -38,14 +32,16 @@ const NotificationTile = ({ onClickCB, setShowNotifications, }: NotificationTileProps) => { - const dispatch: any = useDispatch(); const [result, setResult] = useState(notification); const [isMarkingAsRead, setIsMarkingAsRead] = useState(false); const { t } = useTranslation(); const handleMarkAsRead = async () => { setIsMarkingAsRead(true); - await dispatch(markNotificationAsRead(result.id)); + await request(routes.markNotificationAsRead, { + pathParams: { id: result.id }, + body: { read_at: new Date() }, + }); setResult({ ...result, read_at: new Date() }); setIsMarkingAsRead(false); }; @@ -153,7 +149,6 @@ export default function NotificationsList({ handleOverflow, }: NotificationsListProps) { const { username } = useAuthUser(); - const dispatch: any = useDispatch(); const [data, setData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [offset, setOffset] = useState(0); @@ -180,12 +175,14 @@ export default function NotificationsList({ const intialSubscriptionState = async () => { try { - const res = await dispatch(getUserPnconfig({ username: username })); + const res = await request(routes.getUserPnconfig, { + pathParams: { username: username }, + }); const reg = await navigator.serviceWorker.ready; const subscription = await reg.pushManager.getSubscription(); - if (!subscription && !res?.data?.pf_endpoint) { + if (!subscription && !res.data?.pf_endpoint) { setIsSubscribed("NotSubscribed"); - } else if (subscription?.endpoint === res?.data?.pf_endpoint) { + } else if (subscription?.endpoint === res.data?.pf_endpoint) { setIsSubscribed("SubscribedOnThisDevice"); } else { setIsSubscribed("SubscribedOnAnotherDevice"); @@ -247,9 +244,11 @@ export default function NotificationsList({ pf_p256dh: "", pf_auth: "", }; - await dispatch( - updateUserPnconfig(data, { username: username }) - ); + + await request(routes.updateUserPnconfig, { + pathParams: { username: username }, + body: data, + }); setIsSubscribed("NotSubscribed"); setIsSubscribing(false); @@ -271,8 +270,8 @@ export default function NotificationsList({ async function subscribe() { setIsSubscribing(true); - const response = await dispatch(getPublicKey()); - const public_key = response.data.public_key; + const response = await request(routes.getPublicKey); + const public_key = response.data?.public_key; const sw = await navigator.serviceWorker.ready; const push = await sw.pushManager.subscribe({ userVisibleOnly: true, @@ -297,11 +296,12 @@ export default function NotificationsList({ pf_auth: auth, }; - const res = await dispatch( - updateUserPnconfig(data, { username: username }) - ); + const { res } = await request(routes.updateUserPnconfig, { + pathParams: { username: username }, + body: data, + }); - if (res.status >= 200 && res.status <= 300) { + if (res?.ok) { setIsSubscribed("SubscribedOnThisDevice"); } setIsSubscribing(false); @@ -310,8 +310,11 @@ export default function NotificationsList({ const handleMarkAllAsRead = async () => { setIsMarkingAllAsRead(true); await Promise.all( - data.map(async (notification) => { - return await dispatch(markNotificationAsRead(notification.id)); + data.map((notification) => { + return request(routes.markNotificationAsRead, { + pathParams: { id: notification.id }, + body: { read_at: new Date() }, + }); }) ); setReload(!reload); @@ -320,10 +323,10 @@ export default function NotificationsList({ useEffect(() => { setIsLoading(true); - dispatch( - getNotifications({ offset, event: eventFilter, medium_sent: "SYSTEM" }) - ) - .then((res: any) => { + request(routes.getNotifications, { + query: { offset, event: eventFilter, medium_set: "SYSTEM" }, + }) + .then((res) => { if (res && res.data) { setData(res.data.results); setUnreadCount( @@ -341,7 +344,7 @@ export default function NotificationsList({ setOffset((prev) => prev - RESULT_LIMIT); }); intialSubscriptionState(); - }, [dispatch, reload, open, offset, eventFilter, isSubscribed]); + }, [reload, open, offset, eventFilter, isSubscribed]); if (!offset && isLoading) { manageResults = ( diff --git a/src/Components/Notifications/ShowPushNotification.tsx b/src/Components/Notifications/ShowPushNotification.tsx index 09b62bb9333..35e9ecd7e70 100644 --- a/src/Components/Notifications/ShowPushNotification.tsx +++ b/src/Components/Notifications/ShowPushNotification.tsx @@ -1,29 +1,34 @@ -import { useDispatch } from "react-redux"; -import { getNotificationData } from "../../Redux/actions"; -import { useEffect } from "react"; import { DetailRoute } from "../../Routers/types"; +import useQuery from "../../Utils/request/useQuery"; +import routes from "../../Redux/api"; +import { NotificationData } from "./models"; export default function ShowPushNotification({ id }: DetailRoute) { - const dispatch: any = useDispatch(); + useQuery(routes.getNotificationData, { + pathParams: { id }, + onResponse(res) { + if (res.data) { + window.location.href = resultUrl(res.data); + } + }, + }); - const resultUrl = async () => { - const res = await dispatch(getNotificationData({ id })); - const data = res.data.caused_objects; - switch (res.data.event) { + const resultUrl = ({ caused_objects, event }: NotificationData) => { + switch (event) { case "PATIENT_CREATED": - return `/facility/${data.facility}/patient/${data.patient}`; + return `/facility/${caused_objects?.facility}/patient/${caused_objects?.patient}`; case "PATIENT_UPDATED": - return `/facility/${data.facility}/patient/${data.patient}`; + return `/facility/${caused_objects?.facility}/patient/${caused_objects?.patient}`; case "PATIENT_CONSULTATION_CREATED": - return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}`; + return `/facility/${caused_objects?.facility}/patient/${caused_objects?.patient}/consultation/${caused_objects?.consultation}`; case "PATIENT_CONSULTATION_UPDATED": - return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}`; + return `/facility/${caused_objects?.facility}/patient/${caused_objects?.patient}/consultation/${caused_objects?.consultation}`; case "PATIENT_CONSULTATION_UPDATE_CREATED": - return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}/daily-rounds/${data.daily_round}`; + return `/facility/${caused_objects?.facility}/patient/${caused_objects?.patient}/consultation/${caused_objects?.consultation}/daily-rounds/${caused_objects?.daily_round}`; case "PATIENT_CONSULTATION_UPDATE_UPDATED": - return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}/daily-rounds/${data.daily_round}`; + return `/facility/${caused_objects?.facility}/patient/${caused_objects?.patient}/consultation/${caused_objects?.consultation}/daily-rounds/${caused_objects?.daily_round}`; case "INVESTIGATION_SESSION_CREATED": - return `/facility/${data.facility}/patient/${data.patient}/consultation/${data.consultation}/investigation/${data.session}`; + return `/facility/${caused_objects?.facility}/patient/${caused_objects?.patient}/consultation/${caused_objects?.consultation}/investigation/${caused_objects?.session}`; case "MESSAGE": return "/notice_board/"; default: @@ -31,13 +36,5 @@ export default function ShowPushNotification({ id }: DetailRoute) { } }; - useEffect(() => { - resultUrl() - .then((url) => { - window.location.href = url; - }) - .catch((err) => console.log(err)); - }, []); - return <>; } diff --git a/src/Components/Notifications/models.tsx b/src/Components/Notifications/models.tsx new file mode 100644 index 00000000000..edb29178cb5 --- /dev/null +++ b/src/Components/Notifications/models.tsx @@ -0,0 +1,29 @@ +export interface NotificationData { + id: string; + title: string; + caused_objects: cause_object; + caused_by: any; + content: string; + offset: number; + event: string; + event_type: string; + medium_sent: string; + created_date: string; + read_at: string; + message: string; + public_key: string; +} + +export interface cause_object { + facility: string; + patient: string; + consultation: string; + daily_round: string; + session: string; +} + +export interface PNconfigData { + pf_auth: string; + pf_endpoint: string; + pf_p256dh: string; +} diff --git a/src/Redux/api.tsx b/src/Redux/api.tsx index 0e3fabf0bff..7c78322da94 100644 --- a/src/Redux/api.tsx +++ b/src/Redux/api.tsx @@ -45,6 +45,10 @@ import { } from "../Components/ExternalResult/models"; import { UserModel } from "../Components/Users/models"; import { PaginatedResponse } from "../Utils/request/types"; +import { + NotificationData, + PNconfigData, +} from "../Components/Notifications/models"; import { PatientModel } from "../Components/Patient/models"; import { IComment, IResource } from "../Components/Resource/models"; @@ -204,11 +208,14 @@ const routes = { getUserPnconfig: { path: "/api/v1/users/{username}/pnconfig/", + method: "GET", + TRes: Type(), }, updateUserPnconfig: { path: "/api/v1/users/{username}/pnconfig/", method: "PATCH", + TRes: Type(), }, // Skill Endpoints @@ -770,19 +777,27 @@ const routes = { path: "/api/v1/shift/{id}/comment/", method: "POST", }, + // Notifications getNotifications: { path: "/api/v1/notification/", + method: "GET", + TRes: Type>(), }, getNotificationData: { path: "/api/v1/notification/{id}/", + method: "GET", + TRes: Type(), }, markNotificationAsRead: { path: "/api/v1/notification/{id}/", method: "PATCH", + TRes: Type(), }, getPublicKey: { path: "/api/v1/notification/public_key/", + method: "GET", + TRes: Type(), }, sendNotificationMessages: { path: "/api/v1/notification/notify/", From 01d5b16b1f5d7868ec65f1f81d21045cad719520 Mon Sep 17 00:00:00 2001 From: konavivekramakrishna <101407963+konavivekramakrishna@users.noreply.github.com> Date: Tue, 7 Nov 2023 16:58:03 +0530 Subject: [PATCH 16/58] Replaced useDispatch w. useQuery/request: Shifting (src/Components/Shifting/**) (#6533) * replaced useDispatch in shifting * changed prefetch condition * removed refetch and fixed loading effect * fixed commentSection * removed paginatedList.whenEmpty --- src/CAREUI/misc/PaginatedList.tsx | 7 +- src/Components/Common/FacilitySelect.tsx | 2 +- src/Components/Facility/models.tsx | 8 +- src/Components/Resource/BadgesList.tsx | 6 +- src/Components/Resource/CommentSection.tsx | 1 - src/Components/Resource/ResourceBoard.tsx | 77 +++++---- src/Components/Shifting/BadgesList.tsx | 105 ++++-------- src/Components/Shifting/CommentsSection.tsx | 162 +++++++++--------- src/Components/Shifting/ListFilter.tsx | 122 ++++++------- src/Components/Shifting/ListView.tsx | 96 ++++------- src/Components/Shifting/ShiftDetails.tsx | 131 +++++++------- .../Shifting/ShiftDetailsUpdate.tsx | 106 +++++------- src/Components/Shifting/ShiftingBoard.tsx | 100 ++++++----- src/Components/Shifting/models.ts | 44 +++++ src/Redux/actions.tsx | 18 +- src/Redux/api.tsx | 16 +- 16 files changed, 472 insertions(+), 529 deletions(-) create mode 100644 src/Components/Shifting/models.ts diff --git a/src/CAREUI/misc/PaginatedList.tsx b/src/CAREUI/misc/PaginatedList.tsx index 3521807154e..0d2445ffbc9 100644 --- a/src/CAREUI/misc/PaginatedList.tsx +++ b/src/CAREUI/misc/PaginatedList.tsx @@ -33,7 +33,10 @@ function useContextualized() { interface Props extends QueryOptions> { route: QueryRoute>; perPage?: number; - children: (ctx: PaginatedListContext) => JSX.Element | JSX.Element[]; + children: ( + ctx: PaginatedListContext, + query: ReturnType>> + ) => JSX.Element | JSX.Element[]; } export default function PaginatedList({ @@ -59,7 +62,7 @@ export default function PaginatedList({ value={{ ...query, items, perPage, currentPage, setPage }} > - {(ctx) => children(ctx as PaginatedListContext)} + {(ctx) => children(ctx as PaginatedListContext, query)} ); diff --git a/src/Components/Common/FacilitySelect.tsx b/src/Components/Common/FacilitySelect.tsx index 19494081aa3..1aabc36013b 100644 --- a/src/Components/Common/FacilitySelect.tsx +++ b/src/Components/Common/FacilitySelect.tsx @@ -6,7 +6,7 @@ import { FacilityModel } from "../Facility/models"; interface FacilitySelectProps { name: string; - exclude_user: string; + exclude_user?: string; errors?: string | undefined; className?: string; searchAll?: boolean; diff --git a/src/Components/Facility/models.tsx b/src/Components/Facility/models.tsx index f5ddde19d2d..7bf6069a61a 100644 --- a/src/Components/Facility/models.tsx +++ b/src/Components/Facility/models.tsx @@ -52,10 +52,10 @@ export interface FacilityModel { ward_object?: WardModel; modified_date?: string; created_date?: string; - state: number; - district: number; - local_body: number; - ward: number; + state?: number; + district?: number; + local_body?: number; + ward?: number; } export interface CapacityModal { diff --git a/src/Components/Resource/BadgesList.tsx b/src/Components/Resource/BadgesList.tsx index 6977861d596..4ab4f3cc8e4 100644 --- a/src/Components/Resource/BadgesList.tsx +++ b/src/Components/Resource/BadgesList.tsx @@ -2,10 +2,10 @@ import { SHIFTING_FILTER_ORDER } from "../../Common/constants"; import routes from "../../Redux/api"; import useQuery from "../../Utils/request/useQuery"; -function useFacilityQuery(facilityId: string | undefined) { +export function useFacilityQuery(facilityId: string | undefined) { return useQuery(routes.getAnyFacility, { - pathParams: { id: String(facilityId) }, - prefetch: facilityId !== undefined, + pathParams: { id: facilityId as string }, + prefetch: !!facilityId, }); } diff --git a/src/Components/Resource/CommentSection.tsx b/src/Components/Resource/CommentSection.tsx index 25d8142dae7..33ed68da45d 100644 --- a/src/Components/Resource/CommentSection.tsx +++ b/src/Components/Resource/CommentSection.tsx @@ -16,7 +16,6 @@ const CommentSection = (props: { id: string }) => { routes.getResourceComments, { pathParams: { id: props.id }, - query: { limit: 8, offset: 0 }, } ); diff --git a/src/Components/Resource/ResourceBoard.tsx b/src/Components/Resource/ResourceBoard.tsx index 217f2941e10..070244cde3e 100644 --- a/src/Components/Resource/ResourceBoard.tsx +++ b/src/Components/Resource/ResourceBoard.tsx @@ -8,8 +8,9 @@ import { ExportButton } from "../Common/Export"; import dayjs from "../../Utils/dayjs"; import useQuery from "../../Utils/request/useQuery"; import routes from "../../Redux/api"; - -const limit = 14; +import { PaginatedResponse } from "../../Utils/request/types"; +import { IResource } from "./models"; +import request from "../../Utils/request/request"; interface boardProps { board: string; @@ -153,8 +154,7 @@ export default function ResourceBoard({ filterProp, formatFilter, }: boardProps) { - const [currentPage, setCurrentPage] = useState(1); - const [isLoading, setIsLoading] = useState({ board: false, more: false }); + const [isLoading, setIsLoading] = useState({ board: "BOARD", more: false }); const [{ isOver }, drop] = useDrop(() => ({ accept: "resource-card", drop: (item: any) => { @@ -165,24 +165,10 @@ export default function ResourceBoard({ collect: (monitor) => ({ isOver: !!monitor.isOver() }), })); const [offset, setOffSet] = useState(0); - - const { data, refetch } = useQuery(routes.listResourceRequests, { - query: formatFilter({ - ...filterProp, - status: board, - offset: offset, - }), - onResponse: ({ res, data }) => { - if (res && data) { - setCurrentPage(1); - } - setIsLoading((loading) => reduceLoading("COMPLETE", loading)); - }, - }); + const [data, setData] = useState>(); useEffect(() => { setIsLoading((loading) => reduceLoading("BOARD", loading)); - refetch(); }, [ board, filterProp.facility, @@ -197,25 +183,50 @@ export default function ResourceBoard({ filterProp.ordering, ]); - const handlePagination = (page: number, limit: number) => { - const offset = (page - 1) * limit; - setOffSet(offset); - setCurrentPage(page); + useQuery(routes.listResourceRequests, { + query: formatFilter({ + ...filterProp, + status: board, + }), + onResponse: ({ res, data: listResourceData }) => { + if (res?.ok && listResourceData) { + setData(listResourceData); + } + setIsLoading((loading) => reduceLoading("COMPLETE", loading)); + }, + }); + + const handlePagination = async () => { setIsLoading((loading) => reduceLoading("MORE", loading)); - refetch(); + setOffSet(offset + 14); + const { res, data: newPageData } = await request( + routes.listResourceRequests, + { + query: formatFilter({ + ...filterProp, + status: board, + offset: offset, + }), + } + ); + if (res?.ok && newPageData) { + setData((prev) => + prev + ? { ...prev, results: [...prev.results, ...newPageData.results] } + : newPageData + ); + } setIsLoading((loading) => reduceLoading("COMPLETE", loading)); }; const boardFilter = (filter: string) => { - return ( - data && - data?.results - .filter(({ status }) => status === filter) - .map((resource: any) => ( - - )) - ); + return data?.results + .filter(({ status }) => status === filter) + .map((resource: any) => ( + + )); }; + return (
    ) : (