diff --git a/packages/esm-active-visits-app/src/active-visits-widget/active-visits.resource.tsx b/packages/esm-active-visits-app/src/active-visits-widget/active-visits.resource.tsx index 975bfb653..cd667de77 100644 --- a/packages/esm-active-visits-app/src/active-visits-widget/active-visits.resource.tsx +++ b/packages/esm-active-visits-app/src/active-visits-widget/active-visits.resource.tsx @@ -11,6 +11,7 @@ import { parseDate, restBaseUrl, useConfig, + useOpenmrsFetchAll, useSession, type Visit, } from '@openmrs/esm-framework'; @@ -31,39 +32,18 @@ export function useActiveVisits() { 'visitType:(uuid,name,display),location:(uuid,name,display),startDatetime,stopDatetime,' + 'encounters:(encounterDatetime,obs:(uuid,concept:(uuid,display),value)))'; - const getUrl = (pageIndex, previousPageData: FetchResponse) => { - if (pageIndex && !previousPageData?.data?.links?.some((link) => link.rel === 'next')) { - return null; - } - + const getUrl = () => { let url = `${restBaseUrl}/visit?v=${customRepresentation}&`; let urlSearchParams = new URLSearchParams(); - urlSearchParams.append('includeInactive', 'false'); urlSearchParams.append('totalCount', 'true'); urlSearchParams.append('location', `${sessionLocation}`); - - if (pageIndex) { - urlSearchParams.append('startIndex', `${pageIndex * 50}`); - } - return url + urlSearchParams.toString(); }; - const { - data, - error, - isLoading, - isValidating, - size: pageNumber, - setSize, - } = useSWRInfinite, Error>(sessionLocation ? getUrl : null, openmrsFetch); - - useEffect(() => { - if (data && data?.[pageNumber - 1]?.data?.links?.some((link) => link.rel === 'next')) { - setSize((currentSize) => currentSize + 1); - } - }, [data, pageNumber, setSize]); + const { data, error, isLoading, isValidating, totalCount } = useOpenmrsFetchAll( + sessionLocation ? getUrl() : null, + ); const mapVisitProperties = (visit: Visit): ActiveVisit => { // create base object @@ -133,16 +113,14 @@ export function useActiveVisits() { return activeVisits; }; - const formattedActiveVisits: Array = data - ? [].concat(...data?.map((res) => res?.data?.results?.map(mapVisitProperties))) - : []; + const formattedActiveVisits: Array = data ? [].concat(...data?.map(mapVisitProperties)) : []; return { activeVisits: formattedActiveVisits, error, isLoading, isValidating, - totalResults: data?.[0]?.data?.totalCount ?? 0, + totalResults: !isNaN(totalCount) ? totalCount : 0, }; } diff --git a/packages/esm-service-queues-app/src/hooks/useQueueEntries.ts b/packages/esm-service-queues-app/src/hooks/useQueueEntries.ts index 6b163a63c..e29dd531c 100644 --- a/packages/esm-service-queues-app/src/hooks/useQueueEntries.ts +++ b/packages/esm-service-queues-app/src/hooks/useQueueEntries.ts @@ -1,11 +1,11 @@ -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import isEqual from 'lodash-es/isEqual'; import useSWR from 'swr'; import { useSWRConfig } from 'swr/_internal'; -import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import { type FetchResponse, openmrsFetch, restBaseUrl, useOpenmrsFetchAll } from '@openmrs/esm-framework'; import { type QueueEntry, type QueueEntrySearchCriteria } from '../types'; -type QueueEntryResponse = FetchResponse<{ +export type QueueEntryResponse = FetchResponse<{ results: Array; links: Array<{ rel: 'prev' | 'next'; @@ -16,10 +16,10 @@ type QueueEntryResponse = FetchResponse<{ const queueEntryBaseUrl = `${restBaseUrl}/queue-entry`; -const repString = +export const repString = 'custom:(uuid,display,queue,status,patient:(uuid,display,person,identifiers:(uuid,display,identifier,identifierType)),visit:(uuid,display,startDatetime,encounters:(uuid,display,diagnoses,encounterDatetime,encounterType,obs,encounterProviders,voided),attributes:(uuid,display,value,attributeType)),priority,priorityComment,sortWeight,startedAt,endedAt,locationWaitingFor,queueComingFrom,providerWaitingFor,previousQueueEntry)'; -function getInitialUrl(rep: string, searchCriteria?: QueueEntrySearchCriteria) { +export function getInitialUrl(rep: string, searchCriteria?: QueueEntrySearchCriteria) { const searchParam = new URLSearchParams(); searchParam.append('v', rep); searchParam.append('totalCount', 'true'); @@ -35,21 +35,7 @@ function getInitialUrl(rep: string, searchCriteria?: QueueEntrySearchCriteria) { return `${queueEntryBaseUrl}?${searchParam.toString()}`; } -function getNextUrlFromResponse(data: QueueEntryResponse) { - const next = data?.data?.links?.find((link) => link.rel === 'next'); - if (next) { - const nextUrl = new URL(next.uri); - // default for production - if (nextUrl.origin === window.location.origin) { - return nextUrl.toString(); - } - - // in development, the request should be routed through the local proxy - return new URL(`${nextUrl.pathname}${nextUrl.search ? nextUrl.search : ''}`, window.location.origin).toString(); - } - // There's no next URL - return null; -} +let queueEntryMutates: ReturnType['mutate'][] = []; export function useMutateQueueEntries() { const { mutate } = useSWRConfig(); @@ -57,133 +43,39 @@ export function useMutateQueueEntries() { return { mutateQueueEntries: () => { return mutate((key) => { - return ( - typeof key === 'string' && - (key.includes(`${restBaseUrl}/queue-entry`) || key.includes(`${restBaseUrl}/visit-queue-entry`)) - ); + return typeof key === 'string' && key.includes(`${restBaseUrl}/visit-queue-entry`); }).then(() => { - window.dispatchEvent(new CustomEvent('queue-entry-updated')); + for (const mutateQueueEntry of queueEntryMutates) { + mutateQueueEntry(); + } }); }, }; } export function useQueueEntries(searchCriteria?: QueueEntrySearchCriteria, rep: string = repString) { - // This manually implements a kind of pagination using the useSWR hook. It does not use useSWRInfinite - // because useSWRInfinite does not support with `mutate`. The hook starts by fetching the first page, - // page zero, waits until data is fetched, then fetches the next page, and so on. - // - // Fine so far. Where things get complicated is in supporting mutation. When a mutation is made, the - // SWR hook first returns stale data with `isValidating` set to false. At this point we say we are - // "waiting for mutate," because we have called mutate, but the useSWR hook hasn't updated properly - // for it yet. Next it returns stale data again, this time with `isValidating` set to true. At this - // point we say we are no longer waiting for mutate. Finally, it returns fresh data with `isValidating` - // again set to false. We may then update the data array and move on to the next page. - const { mutateQueueEntries } = useMutateQueueEntries(); - - const [currentPage, setCurrentPage] = useState(0); - const [currentRep, setCurrentRep] = useState(rep); - const [currentSearchCriteria, setCurrentSearchCriteria] = useState(searchCriteria); - const [data, setData] = useState>>([]); - const [error, setError] = useState(undefined); - const [pageUrl, setPageUrl] = useState(getInitialUrl(currentRep, currentSearchCriteria)); - const [totalCount, setTotalCount] = useState(0); - const [waitingForMutate, setWaitingForMutate] = useState(false); - - const refetchAllData = useCallback( - (newRep: string = currentRep, newSearchCriteria: QueueEntrySearchCriteria = currentSearchCriteria) => { - setWaitingForMutate(true); - setCurrentPage(0); - setPageUrl(getInitialUrl(newRep, newSearchCriteria)); + const [pageUrl, setPageUrl] = useState(getInitialUrl(rep, searchCriteria)); + const { data, mutate, ...rest } = useOpenmrsFetchAll(pageUrl, { + swrInfiniteConfig: { + revalidateFirstPage: false, }, - [currentRep, currentSearchCriteria], - ); - - // This hook listens to the searchCriteria and rep values and refetches the data when they change. - useEffect(() => { - const isSearchCriteriaUpdated = !isEqual(currentSearchCriteria, searchCriteria); - const isRepUpdated = currentRep !== rep; - if (isSearchCriteriaUpdated || isRepUpdated) { - if (isSearchCriteriaUpdated) { - setCurrentSearchCriteria(searchCriteria); - } - if (isRepUpdated) { - setCurrentRep(rep); - } - refetchAllData(rep, searchCriteria); - } - }, [currentRep, currentSearchCriteria, refetchAllData, rep, searchCriteria, setCurrentSearchCriteria]); - - const { data: pageData, isValidating, error: pageError } = useSWR(pageUrl, openmrsFetch); - - useEffect(() => { - const nextUrl = getNextUrlFromResponse(pageData); - const stillWaitingForMutate = waitingForMutate && !isValidating; - if (waitingForMutate && isValidating) { - setWaitingForMutate(false); - } - if (pageData && !isValidating && !stillWaitingForMutate) { - // We've got results! Time to update the data array and move on to the next page. - if (pageData?.data?.totalCount > -1 && pageData?.data?.totalCount !== totalCount) { - setTotalCount(pageData?.data?.totalCount); - } - if (pageData?.data?.results) { - const newData = [...data]; - newData[currentPage] = pageData?.data?.results; - setData(newData); - } - setCurrentPage(currentPage + 1); - setPageUrl(nextUrl); - // If we're mutating existing data, then we again need to wait for the mutate to work, - // since useSWR will (again) first return stale data with isValidating set to false. - const inMutateMode = data.length > currentPage; - if (inMutateMode && nextUrl) { - setWaitingForMutate(true); - } - } - // It may happen that there are fewer pages in the new data than in the old data. In this - // case, we need to remove the extra pages, which are stored on the `data` array. - // Note that since we mutated the `data` state earlier in this function, it is important to - // use the functional form of `setData` so as not to use the stale `data` state. - if (!nextUrl) { - // I will not be very suprised if there is an off-by-one error here. - if (data.length > currentPage + 1) { - setData((prevData) => { - const newData = [...prevData]; - newData.splice(currentPage + 1); - return newData; - }); - } - } - }, [pageData, data, currentPage, totalCount, waitingForMutate, isValidating]); + }); useEffect(() => { - // An error to one is an error to all - if (pageError) { - setError(pageError); - } - }, [pageError]); - - const queueUpdateListener = useCallback(() => { - refetchAllData(); - }, [refetchAllData]); + setPageUrl(getInitialUrl(rep, searchCriteria)); + }, [searchCriteria, rep]); useEffect(() => { - window.addEventListener('queue-entry-updated', queueUpdateListener); + queueEntryMutates.push(mutate); return () => { - window.removeEventListener('queue-entry-updated', queueUpdateListener); + queueEntryMutates = queueEntryMutates.filter((mutateQueueEntry) => mutate != mutateQueueEntry); }; - }, [queueUpdateListener]); - - const queueEntries = useMemo(() => data.flat(), [data]); + }, [mutate]); return { - queueEntries, - totalCount, - isLoading: totalCount === undefined || (totalCount && queueEntries.length < totalCount), - isValidating: isValidating || currentPage < data.length, - error, - mutate: mutateQueueEntries, + queueEntries: data, + mutate, + ...rest, }; } diff --git a/packages/esm-service-queues-app/src/patient-queue-metrics/clinic-metrics.component.tsx b/packages/esm-service-queues-app/src/patient-queue-metrics/clinic-metrics.component.tsx index ec1599e92..3f1d642c3 100644 --- a/packages/esm-service-queues-app/src/patient-queue-metrics/clinic-metrics.component.tsx +++ b/packages/esm-service-queues-app/src/patient-queue-metrics/clinic-metrics.component.tsx @@ -5,11 +5,12 @@ import { isDesktop, useLayoutType } from '@openmrs/esm-framework'; import { updateSelectedService, useSelectedService, useSelectedQueueLocationUuid } from '../helpers/helpers'; import { useActiveVisits, useAverageWaitTime } from './clinic-metrics.resource'; import { useServiceMetricsCount } from './queue-metrics.resource'; -import { useQueueEntries } from '../hooks/useQueueEntries'; import MetricsCard from './metrics-card.component'; import MetricsHeader from './metrics-header.component'; import useQueueServices from '../hooks/useQueueService'; import styles from './clinic-metrics.scss'; +import { getInitialUrl, type QueueEntryResponse, repString } from '../hooks/useQueueEntries'; +import useSWR from 'swr'; export interface Service { uuid: string; @@ -27,11 +28,17 @@ function ClinicMetrics() { const [initialSelectedItem, setInitialSelectItem] = useState(() => { return !currentService?.serviceDisplay || !currentService?.serviceUuid; }); - const { totalCount } = useQueueEntries({ - service: currentService?.serviceUuid, - location: currentQueueLocation, - isEnded: false, - }); + + const { data } = useSWR( + getInitialUrl(repString, { + service: currentService?.serviceUuid, + location: currentQueueLocation, + isEnded: false, + }), + ); + + const totalCount = data?.data?.totalCount; + const { activeVisitsCount, isLoading: loading } = useActiveVisits(); const { waitTime } = useAverageWaitTime(currentService?.serviceUuid, '');