From d0e59889e4cf8cead15cfb3ca7c0ccfb243e0c84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Manuel=20R=C3=B6mer?= <30902964+manuelroemer@users.noreply.github.com> Date: Fri, 9 Sep 2022 16:23:25 +0200 Subject: [PATCH] (fix) Display offline patient updates in offline tools (#516) --- .../src/hooks/offline-patient-data-hooks.ts | 136 ++++++++++++++---- .../patient-name-table-cell.component.tsx | 4 +- .../patients-overview-card.component.tsx | 26 +--- 3 files changed, 116 insertions(+), 50 deletions(-) diff --git a/packages/apps/esm-offline-tools-app/src/hooks/offline-patient-data-hooks.ts b/packages/apps/esm-offline-tools-app/src/hooks/offline-patient-data-hooks.ts index bf230afcb..3e88dd575 100644 --- a/packages/apps/esm-offline-tools-app/src/hooks/offline-patient-data-hooks.ts +++ b/packages/apps/esm-offline-tools-app/src/hooks/offline-patient-data-hooks.ts @@ -3,39 +3,99 @@ import { getSynchronizationItems, getDynamicOfflineDataEntries, } from "@openmrs/esm-framework"; -import useSWR from "swr"; +import merge from "lodash-es/merge"; +import { useMemo } from "react"; +import useSWR, { SWRResponse } from "swr"; -export function useOfflineRegisteredPatients() { - return useSWR("offlineTools/offlineRegisteredPatients", async () => { - const offlinePatients = await getDynamicOfflineDataEntries("patient"); - const syncItems = await getSynchronizationItems<{ - fhirPatient?: fhir.Patient; - }>("patient-registration"); - return syncItems - .filter( - (item) => - item.fhirPatient && - !offlinePatients.find( - (entry) => entry.identifier === item.fhirPatient.id - ) +function useDynamicOfflineDataEntries(type: string) { + return useSWR(`dynamicOfflineData/entries/${type}`, () => + getDynamicOfflineDataEntries(type) + ); +} + +function useSynchronizationItems(type: string) { + return useSWR(`syncQueue/items/${type}`, () => + getSynchronizationItems(type) + ); +} + +function useFhirPatients(ids: Array) { + const stableIds = useMemo(() => [...ids].sort(), [ids]); + return useSWR(["fhirPatients", stableIds], () => + Promise.all( + stableIds.map((patientId) => + fetchCurrentPatient(patientId).then((res) => res.data) ) + ) + ); +} + +export function useOfflineRegisteredPatients() { + const offlinePatientsSwr = useDynamicOfflineDataEntries("patient"); + const patientSyncItemsSwr = useSynchronizationItems<{ + fhirPatient?: fhir.Patient; + }>("patient-registration"); + + return useMergedSwr(() => { + return patientSyncItemsSwr.data + .filter((patientRegistrationItem) => { + const isNewlyRegistered = + patientRegistrationItem.fhirPatient && + !offlinePatientsSwr.data.find( + (offlinePatientEntry) => + offlinePatientEntry.identifier === + patientRegistrationItem.fhirPatient.id + ); + return isNewlyRegistered; + }) .map((item) => item.fhirPatient); - }); + }, [offlinePatientsSwr, patientSyncItemsSwr]); } export function useOfflinePatientsWithEntries() { - return useSWR("offlineTools/offlinePatients", async () => { - const offlinePatientEntries = await getDynamicOfflineDataEntries("patient"); + const offlinePatientsSwr = useDynamicOfflineDataEntries("patient"); + const patientSyncItemsSwr = useSynchronizationItems<{ + fhirPatient?: fhir.Patient; + }>("patient-registration"); + const fhirPatientsSwr = useFhirPatients( + offlinePatientsSwr.data?.map((entry) => entry.identifier) ?? [] + ); + + return useMergedSwr(() => { + return offlinePatientsSwr.data.map((offlinePatientEntry) => { + const matchingFhirPatient = fhirPatientsSwr.data.find( + (patient) => patient.id === offlinePatientEntry.identifier + ); + const offlineUpdates = patientSyncItemsSwr.data + .filter( + (syncItem) => + syncItem.fhirPatient.id === offlinePatientEntry.identifier + ) + .map((item) => item.fhirPatient); + const finalPatient = merge( + matchingFhirPatient, + ...offlineUpdates + ) as fhir.Patient; + + return { + patient: finalPatient, + entry: offlinePatientEntry, + }; + }); + }, [offlinePatientsSwr, patientSyncItemsSwr, fhirPatientsSwr]); +} - const result = await Promise.all( - offlinePatientEntries.map(async (entry) => ({ - patient: (await fetchCurrentPatient(entry.identifier))?.data, - entry, - })) - ); +export function useOfflinePatientStats() { + const offlinePatientsSwr = useDynamicOfflineDataEntries("patient"); + const offlineRegisteredPatientsSwr = useOfflineRegisteredPatients(); - return result.filter((x) => x.patient); - }); + return useMergedSwr( + () => ({ + downloadedCount: offlinePatientsSwr.data.length, + registeredCount: offlineRegisteredPatientsSwr.data.length, + }), + [offlinePatientsSwr, offlineRegisteredPatientsSwr] + ); } export function useLastSyncStateOfPatient(patientUuid: string) { @@ -52,3 +112,29 @@ export function useLastSyncStateOfPatient(patientUuid: string) { } ); } + +function useMergedSwr( + merge: () => T, + swrResponses: Array +): SWRResponse { + return useMemo(() => { + const areAllLoaded = swrResponses.every((res) => !!res.data); + const data = areAllLoaded ? merge() : null; + const error = swrResponses.find((res) => res.error); + const mutate = () => + Promise.all(swrResponses.map((res) => res.mutate())).then(merge); + const isValidating = swrResponses.some((res) => res.isValidating); + + return { + data, + error, + mutate, + isValidating, + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + merge, + // eslint-disable-next-line react-hooks/exhaustive-deps + ...swrResponses.flatMap((res) => [res.data, res.error, res.isValidating]), + ]); +} diff --git a/packages/apps/esm-offline-tools-app/src/offline-patients/patient-name-table-cell.component.tsx b/packages/apps/esm-offline-tools-app/src/offline-patients/patient-name-table-cell.component.tsx index 9ffc49bf5..81843d13b 100644 --- a/packages/apps/esm-offline-tools-app/src/offline-patients/patient-name-table-cell.component.tsx +++ b/packages/apps/esm-offline-tools-app/src/offline-patients/patient-name-table-cell.component.tsx @@ -14,7 +14,9 @@ const PatientNameTableCell: React.FC = ({ isNewlyRegistered = false, }) => { const { t } = useTranslation(); - const name = `${patient.name[0].given.join(" ")} ${patient.name[0].family}`; + const name = `${[patient.name?.[0]?.given, patient.name?.[0]?.family] + .filter(Boolean) + .join(" ")}`; return (
diff --git a/packages/apps/esm-offline-tools-app/src/offline-patients/patients-overview-card.component.tsx b/packages/apps/esm-offline-tools-app/src/offline-patients/patients-overview-card.component.tsx index 72c7dd866..48520f954 100644 --- a/packages/apps/esm-offline-tools-app/src/offline-patients/patients-overview-card.component.tsx +++ b/packages/apps/esm-offline-tools-app/src/offline-patients/patients-overview-card.component.tsx @@ -3,15 +3,11 @@ import { useTranslation } from "react-i18next"; import HeaderedQuickInfo from "../components/headered-quick-info.component"; import OverviewCard from "../components/overview-card.component"; import { routes } from "../constants"; -import useSWR from "swr"; -import { - getDynamicOfflineDataEntries, - getSynchronizationItems, -} from "@openmrs/esm-framework"; +import { useOfflinePatientStats } from "../hooks/offline-patient-data-hooks"; const PatientsOverviewCard: React.FC = () => { const { t } = useTranslation(); - const { data } = useDownloadedOfflinePatients(); + const { data } = useOfflinePatientStats(); return ( { ); }; -function useDownloadedOfflinePatients() { - return useSWR(["offlineTools/offlinePatientsTotalCount"], async () => { - const patientDataEntries = await getDynamicOfflineDataEntries("patient"); - const patientRegistrationSyncItems = await getSynchronizationItems( - "patient-registration" - ); - const registeredCount = patientRegistrationSyncItems.length; - const downloadedCount = patientDataEntries.filter( - (entry) => entry.syncState && entry.syncState.erroredHandlers.length === 0 - ).length; - - return { - downloadedCount, - registeredCount, - }; - }); -} - export default PatientsOverviewCard;