From fbf58c3123c956750d4ac2ad0e2ee4f51caa8c6e Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Mon, 3 Jul 2023 12:22:33 +0530 Subject: [PATCH 1/6] (feat) Enable reverse order for name fields in registration form via configuration (#744) * Display family name before the first name as per configuration * Review changes --------- Co-authored-by: Dennis Kigen --- .../src/config-schema.ts | 6 ++ .../field/name/name-field.component.tsx | 73 ++++++++++++------- 2 files changed, 51 insertions(+), 28 deletions(-) diff --git a/packages/esm-patient-registration-app/src/config-schema.ts b/packages/esm-patient-registration-app/src/config-schema.ts index 6eab94c4f..7a6a77aad 100644 --- a/packages/esm-patient-registration-app/src/config-schema.ts +++ b/packages/esm-patient-registration-app/src/config-schema.ts @@ -41,6 +41,7 @@ export interface RegistrationConfig { defaultUnknownGivenName: string; defaultUnknownFamilyName: string; displayCapturePhoto: boolean; + displayReverseFieldOrder: boolean; }; gender: Array; address: { @@ -220,6 +221,11 @@ export const esmPatientRegistrationSchema = { _default: true, _description: 'Whether to display capture patient photo slot on name field', }, + displayReverseFieldOrder: { + _type: Type.Boolean, + _default: false, + _description: "Whether to display the name fields in the order 'Family name' -> 'Middle name' -> 'First name'", + }, }, gender: { _type: Type.Array, diff --git a/packages/esm-patient-registration-app/src/patient-registration/field/name/name-field.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/field/name/name-field.component.tsx index 3ef56ba7f..d25da7614 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/field/name/name-field.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/field/name/name-field.component.tsx @@ -21,7 +21,7 @@ function checkNumber(value: string) { export const NameField = () => { const { fieldConfigurations: { - name: { displayCapturePhoto }, + name: { displayCapturePhoto, displayReverseFieldOrder }, }, } = useConfig() as RegistrationConfig; const { t } = useTranslation(); @@ -55,6 +55,36 @@ export const NameField = () => { } }; + const firstNameField = ( + + ); + + const middleNameField = fieldConfigs.displayMiddleName && ( + + ); + + const familyNameField = ( + + ); + return (

{t('fullNameLabelText', 'Full Name')}

@@ -75,33 +105,20 @@ export const NameField = () => { - {nameKnown && ( - <> - - {fieldConfigs.displayMiddleName && ( - - )} - - - )} + {nameKnown && + (!displayReverseFieldOrder ? ( + <> + {firstNameField} + {middleNameField} + {familyNameField} + + ) : ( + <> + {familyNameField} + {middleNameField} + {firstNameField} + + ))}
From cc55924cba099c1168291548abb12cc12f87ec3f Mon Sep 17 00:00:00 2001 From: Vineet Sharma Date: Mon, 3 Jul 2023 12:54:28 +0530 Subject: [PATCH 2/6] (fix) Properly handle focus and blur states for address hierarchy fields (#745) * Input focus and blur should hide entries * Cleanup --------- Co-authored-by: Dennis Kigen --- .../combo-input/combo-input.component.tsx | 43 +++++++++++-------- .../src/patient-registration/input/input.scss | 5 +++ 2 files changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/esm-patient-registration-app/src/patient-registration/input/combo-input/combo-input.component.tsx b/packages/esm-patient-registration-app/src/patient-registration/input/combo-input/combo-input.component.tsx index 646ccbab4..78ce727ab 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/input/combo-input/combo-input.component.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/input/combo-input/combo-input.component.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { TextInput, Layer } from '@carbon/react'; import SelectionTick from './selection-tick.component'; import styles from '../input.scss'; @@ -26,18 +26,6 @@ const ComboInput: React.FC = ({ entries, fieldProps, handleInpu setHighlightedEntry(-1); }, [setShowEntries, setHighlightedEntry]); - const handleBlur = useCallback( - (e) => { - // This check is in place to not hide the entries when an entry is clicked - // Else the onClick of the entry will not be counted. - if (!comboInputRef?.current?.contains(e.target)) { - setShowEntries(false); - } - setHighlightedEntry(-1); - }, - [setShowEntries, setHighlightedEntry], - ); - const filteredEntries = useMemo(() => { if (!entries) { return []; @@ -60,6 +48,12 @@ const ComboInput: React.FC = ({ entries, fieldProps, handleInpu const handleKeyPress = useCallback( (e: KeyboardEvent) => { const totalResults = filteredEntries.length ?? 0; + + if (e.key === 'Tab') { + setShowEntries(false); + setHighlightedEntry(-1); + } + if (e.key === 'ArrowUp') { setHighlightedEntry((prev) => Math.max(-1, prev - 1)); } else if (e.key === 'ArrowDown') { @@ -68,17 +62,32 @@ const ComboInput: React.FC = ({ entries, fieldProps, handleInpu handleOptionClick(filteredEntries[highlightedEntry], e); } }, - [highlightedEntry, handleOptionClick, filteredEntries, setHighlightedEntry], + [highlightedEntry, handleOptionClick, filteredEntries, setHighlightedEntry, setShowEntries], ); + useEffect(() => { + const listener = (e) => { + if (!comboInputRef.current.contains(e.target as Node)) { + setShowEntries(false); + setHighlightedEntry(-1); + } + }; + window.addEventListener('click', listener); + return () => { + window.removeEventListener('click', listener); + }; + }); + return (
handleInputChange(e.target.value)} + onChange={(e) => { + setHighlightedEntry(-1); + handleInputChange(e.target.value); + }} onFocus={handleFocus} - onBlur={handleBlur} autoComplete={'off'} onKeyDown={handleKeyPress} /> @@ -98,7 +107,7 @@ const ComboInput: React.FC = ({ entries, fieldProps, handleInpu aria-selected="true" onClick={() => handleOptionClick(entry)}>
{entry} diff --git a/packages/esm-patient-registration-app/src/patient-registration/input/input.scss b/packages/esm-patient-registration-app/src/patient-registration/input/input.scss index 5ea727b9a..10a7e2e5f 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/input/input.scss +++ b/packages/esm-patient-registration-app/src/patient-registration/input/input.scss @@ -111,3 +111,8 @@ .comboInputEntries { position: relative; } + +.comboInputItemOption { + margin: 0; + padding-left: 1rem; +} From 72f4c3fdfa66548955a74628caab24a1250a8aa7 Mon Sep 17 00:00:00 2001 From: jnsereko <58003327+jnsereko@users.noreply.github.com> Date: Mon, 3 Jul 2023 01:00:13 -0700 Subject: [PATCH 3/6] (feat) Display recently viewed patients in the search menu (#665) Co-authored-by: Dennis Kigen --- .../index.tsx | 4 +- .../compact-patient-banner.component.tsx | 18 +- .../compact-patient-search.component.tsx | 122 ++++++++----- .../patient-search.component.tsx | 18 +- .../recent-patient-search.component.tsx | 116 +++++++++++++ .../src/hooks/useArrowNavigation.tsx | 3 +- .../advanced-patient-search.component.tsx | 4 +- .../src/patient-search.resource.tsx | 160 +++++++++++------- .../esm-patient-search-app/src/types/index.ts | 8 + .../translations/am.json | 2 + .../translations/en.json | 2 + .../translations/es.json | 2 + .../translations/fr.json | 2 + .../translations/he.json | 4 + .../translations/km.json | 1 + 15 files changed, 346 insertions(+), 120 deletions(-) create mode 100644 packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.component.tsx diff --git a/packages/esm-patient-search-app/src/compact-patient-search-extension/index.tsx b/packages/esm-patient-search-app/src/compact-patient-search-extension/index.tsx index 6b35cff8b..82389962e 100644 --- a/packages/esm-patient-search-app/src/compact-patient-search-extension/index.tsx +++ b/packages/esm-patient-search-app/src/compact-patient-search-extension/index.tsx @@ -4,7 +4,7 @@ import { SearchedPatient } from '../types'; import { Search, Button } from '@carbon/react'; import { useTranslation } from 'react-i18next'; import styles from './compact-patient-search.scss'; -import { usePatientSearchInfinite } from '../patient-search.resource'; +import { useInfinitePatientSearch } from '../patient-search.resource'; import { useConfig, navigate, interpolateString } from '@openmrs/esm-framework'; import useArrowNavigation from '../hooks/useArrowNavigation'; @@ -24,7 +24,7 @@ const CompactPatientSearchComponent: React.FC = ({ const handleChange = useCallback((val) => setSearchTerm(val), [setSearchTerm]); const showSearchResults = useMemo(() => !!searchTerm?.trim(), [searchTerm]); const config = useConfig(); - const patientSearchResponse = usePatientSearchInfinite(searchTerm, config.includeDead, showSearchResults); + const patientSearchResponse = useInfinitePatientSearch(searchTerm, config.includeDead, showSearchResults); const { data: patients } = patientSearchResponse; const handleSubmit = useCallback((evt) => { diff --git a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx index 5cbe6c7e8..96cda1a8e 100644 --- a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx +++ b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-banner.component.tsx @@ -2,12 +2,16 @@ import React, { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { SkeletonIcon, SkeletonText, Tag } from '@carbon/react'; import { ExtensionSlot, useConfig, interpolateString, ConfigurableLink, age } from '@openmrs/esm-framework'; -import { Identifier, SearchedPatient } from '../types/index'; +import type { Identifier, SearchedPatient } from '../types'; import styles from './compact-patient-banner.scss'; interface PatientSearchResultsProps { patients: Array; - selectPatientAction?: (evt: any, index: number) => void; + selectPatientAction?: ( + event: React.MouseEvent, + index: number, + patients: Array, + ) => void; } const PatientSearchResults = React.forwardRef( @@ -15,7 +19,7 @@ const PatientSearchResults = React.forwardRef { + const getGender = (gender: string) => { switch (gender) { case 'M': return t('male', 'Male'); @@ -68,14 +72,15 @@ const PatientSearchResults = React.forwardRef - {fhirPatients.map((patient, indx) => { + {fhirPatients.map((patient, index) => { const patientIdentifiers = patient.identifier.filter((identifier) => config.defaultIdentifierTypes.includes(identifier.identifierType.uuid), ); const isDeceased = Boolean(patient?.deceasedDateTime); + return ( selectPatientAction(evt, indx)} + onClick={(event) => selectPatientAction(event, index, patients)} to={`${interpolateString(config.search.patientResultUrl, { patientUuid: patient.id, })}`} @@ -110,7 +115,7 @@ const PatientSearchResults = React.forwardRef 1 ? ( ) : ( - + )} ) : ( @@ -172,6 +177,7 @@ const CustomIdentifier: React.FC<{ patient: SearchedPatient; identifierName: str identifierName, }) => { const identifier = patient.identifiers.find((identifier) => identifier.identifierType.display === identifierName); + return identifier ? ( <> diff --git a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx index c4ef74b6e..59848b32f 100644 --- a/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx +++ b/packages/esm-patient-search-app/src/compact-patient-search/compact-patient-search.component.tsx @@ -1,12 +1,18 @@ import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react'; -import { navigate, interpolateString, useConfig } from '@openmrs/esm-framework'; +import debounce from 'lodash-es/debounce'; +import { navigate, interpolateString, useConfig, setSessionLocation, useSession } from '@openmrs/esm-framework'; +import type { SearchedPatient } from '../types'; import PatientSearch from './patient-search.component'; import PatientSearchBar from '../patient-search-bar/patient-search-bar.component'; -import styles from './compact-patient-search.scss'; -import { SearchedPatient } from '../types'; -import debounce from 'lodash-es/debounce'; +import RecentPatientSearch from './recent-patient-search.component'; import useArrowNavigation from '../hooks/useArrowNavigation'; -import { usePatientSearchInfinite } from '../patient-search.resource'; +import { + updateRecentlyViewedPatients, + useInfinitePatientSearch, + useRESTPatients, + useRecentlyViewedPatients, +} from '../patient-search.resource'; +import styles from './compact-patient-search.scss'; interface CompactPatientSearchProps { isSearchPage: boolean; @@ -26,16 +32,23 @@ const CompactPatientSearchComponent: React.FC = ({ const [searchTerm, setSearchTerm] = useState(initialSearchTerm); const showSearchResults = useMemo(() => !!searchTerm.trim(), [searchTerm]); const bannerContainerRef = useRef(null); - const inputRef = useRef(null); + const searchInputRef = useRef(null); const config = useConfig(); - const patientSearchResponse = usePatientSearchInfinite(searchTerm, config.includeDead, showSearchResults); - const { data: patients } = patientSearchResponse; + const patientSearchResponse = useInfinitePatientSearch(searchTerm, config.includeDead, showSearchResults); + const { data: searchedPatients } = patientSearchResponse; + const { recentlyViewedPatients, mutateUserProperties } = useRecentlyViewedPatients(); + const recentPatientSearchResponse = useRESTPatients(recentlyViewedPatients, !showSearchResults); + const { data: recentPatients } = recentPatientSearchResponse; + const { + user, + sessionLocation: { uuid: currentLocation }, + } = useSession(); const handleFocusToInput = useCallback(() => { - var len = inputRef.current.value?.length ?? 0; - inputRef.current.setSelectionRange(len, len); - inputRef.current.focus(); - }, [inputRef]); + const len = searchInputRef.current.value?.length ?? 0; + searchInputRef.current.setSelectionRange(len, len); + searchInputRef.current.focus(); + }, [searchInputRef]); const handleCloseSearchResults = useCallback(() => { setSearchTerm(''); @@ -43,22 +56,40 @@ const CompactPatientSearchComponent: React.FC = ({ }, [onPatientSelect, setSearchTerm]); const handlePatientSelection = useCallback( - (evt, index: number) => { + (evt, index: number, patients: Array) => { evt.preventDefault(); - if (selectPatientAction) { - selectPatientAction(patients[index]); - } else { - navigate({ - to: `${interpolateString(config.search.patientResultUrl, { - patientUuid: patients[index].uuid, - })}`, - }); + if (patients) { + if (selectPatientAction) { + selectPatientAction(patients[index]); + } else { + navigate({ + to: `${interpolateString(config.search.patientResultUrl, { + patientUuid: patients[index].uuid, + })}`, + }); + updateRecentlyViewedPatients(patients[index].uuid, user).then(() => { + setSessionLocation(currentLocation, new AbortController()); + mutateUserProperties(); + }); + } + handleCloseSearchResults(); } - handleCloseSearchResults(); }, - [config.search, selectPatientAction, patients, handleCloseSearchResults], + [ + selectPatientAction, + handleCloseSearchResults, + config.search.patientResultUrl, + user, + currentLocation, + mutateUserProperties, + ], + ); + const focussedResult = useArrowNavigation( + !recentPatients ? searchedPatients?.length ?? 0 : recentPatients?.length ?? 0, + handlePatientSelection, + handleFocusToInput, + -1, ); - const focussedResult = useArrowNavigation(patients?.length ?? 0, handlePatientSelection, handleFocusToInput, -1); useEffect(() => { if (bannerContainerRef.current && focussedResult > -1) { @@ -68,12 +99,12 @@ const CompactPatientSearchComponent: React.FC = ({ block: 'end', inline: 'nearest', }); - } else if (bannerContainerRef.current && inputRef.current && focussedResult === -1) { + } else if (bannerContainerRef.current && searchInputRef.current && focussedResult === -1) { handleFocusToInput(); } }, [focussedResult, bannerContainerRef, handleFocusToInput]); - const onSubmit = useCallback( + const handleSubmit = useCallback( (searchTerm) => { if (shouldNavigateToPatientSearchPage && searchTerm.trim()) { if (!isSearchPage) { @@ -87,7 +118,7 @@ const CompactPatientSearchComponent: React.FC = ({ [isSearchPage, shouldNavigateToPatientSearchPage], ); - const onClear = useCallback(() => { + const handleClear = useCallback(() => { setSearchTerm(''); }, [setSearchTerm]); @@ -99,20 +130,31 @@ const CompactPatientSearchComponent: React.FC = ({ small initialSearchTerm={initialSearchTerm ?? ''} onChange={handleSearchQueryChange} - onSubmit={onSubmit} - onClear={onClear} - ref={inputRef} + onSubmit={handleSubmit} + onClear={handleClear} + ref={searchInputRef} /> - {!isSearchPage && showSearchResults && ( -
- -
- )} + {!isSearchPage && + (showSearchResults ? ( +
+ +
+ ) : ( + <> +
+ +
+ + ))}
); }; diff --git a/packages/esm-patient-search-app/src/compact-patient-search/patient-search.component.tsx b/packages/esm-patient-search-app/src/compact-patient-search/patient-search.component.tsx index f8918d443..cb84d327d 100644 --- a/packages/esm-patient-search-app/src/compact-patient-search/patient-search.component.tsx +++ b/packages/esm-patient-search-app/src/compact-patient-search/patient-search.component.tsx @@ -1,14 +1,18 @@ import React, { useRef, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { Layer, Loading, Tile } from '@carbon/react'; -import PatientSearchResults, { SearchResultSkeleton } from './compact-patient-banner.component'; import EmptyDataIllustration from '../ui-components/empty-data-illustration.component'; +import PatientSearchResults, { SearchResultSkeleton } from './compact-patient-banner.component'; +import type { PatientSearchResponse, SearchedPatient } from '../types'; import styles from './patient-search.scss'; -import { PatientSearchResponse } from '../types'; interface PatientSearchProps extends PatientSearchResponse { query: string; - selectPatientAction?: (evt: any, index: number) => void; + selectPatientAction?: ( + evt: React.MouseEvent, + index: number, + patients: Array, + ) => void; } const PatientSearch = React.forwardRef( @@ -46,11 +50,9 @@ const PatientSearch = React.forwardRef( if (isLoading) { return (
- - - - - + {[...Array(5)].map((_, index) => ( + + ))}
); } diff --git a/packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.component.tsx b/packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.component.tsx new file mode 100644 index 000000000..df55d8d3e --- /dev/null +++ b/packages/esm-patient-search-app/src/compact-patient-search/recent-patient-search.component.tsx @@ -0,0 +1,116 @@ +import React, { useRef, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Layer, Loading, Tile } from '@carbon/react'; +import type { PatientSearchResponse, SearchedPatient } from '../types'; +import EmptyDataIllustration from '../ui-components/empty-data-illustration.component'; +import PatientSearchResults, { SearchResultSkeleton } from './compact-patient-banner.component'; +import styles from './patient-search.scss'; + +interface RecentPatientSearchProps extends PatientSearchResponse { + selectPatientAction?: (evt: any, index: number, patients: Array) => void; +} + +const RecentPatientSearch = React.forwardRef( + ({ selectPatientAction, isLoading, data: searchResults, fetchError, loadingNewData, setPage, hasMore }, ref) => { + const { t } = useTranslation(); + const observer = useRef(null); + const loadingIconRef = useCallback( + (node) => { + if (loadingNewData) { + return; + } + if (observer.current) { + observer.current.disconnect(); + } + observer.current = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore) { + setPage((page) => page + 1); + } + }, + { + threshold: 0.75, + }, + ); + if (node) { + observer.current.observe(node); + } + }, + [loadingNewData, hasMore, setPage], + ); + + if (isLoading) { + return ( +
+ {[...Array(5)].map((_, index) => ( + + ))} +
+ ); + } + + if (fetchError) { + return ( +
+ + + +
+

{t('error', 'Error')}

+

+ {t( + 'errorCopy', + 'Sorry, there was an error. You can try to reload this page, or contact the site administrator and quote the error code above.', + )} +

+
+
+
+
+ ); + } + + if (searchResults?.length) { + return ( +
+
+

+ {t('recentSearchResultsCount', '{count} recent search result{plural}', { + count: searchResults.length, + plural: searchResults.length === 0 || searchResults.length > 1 ? 's' : '', + })} +

+ + {hasMore && ( +
+ +
+ )} +
+
+ ); + } + + return ( +
+
+ + + +

+ {t('noPatientChartsFoundMessage', 'Sorry, no patient charts were found')} +

+

+ + {t('trySearchWithPatientUniqueID', "Try to search again using the patient's unique ID number")} + +

+
+
+
+
+ ); + }, +); + +export default RecentPatientSearch; diff --git a/packages/esm-patient-search-app/src/hooks/useArrowNavigation.tsx b/packages/esm-patient-search-app/src/hooks/useArrowNavigation.tsx index 99d630157..06bd1e76d 100644 --- a/packages/esm-patient-search-app/src/hooks/useArrowNavigation.tsx +++ b/packages/esm-patient-search-app/src/hooks/useArrowNavigation.tsx @@ -1,8 +1,9 @@ import { useEffect, useState, useCallback } from 'react'; +import { SearchedPatient } from '../types'; const useArrowNavigation = ( totalResults: number, - enterCallback: (evt: any, index: number) => void, + enterCallback: (evt: React.MouseEvent, index: number, patients?: Array) => void, resetFocusCallback: () => void, initalFocussedResult: number = -1, ) => { diff --git a/packages/esm-patient-search-app/src/patient-search-page/advanced-patient-search.component.tsx b/packages/esm-patient-search-app/src/patient-search-page/advanced-patient-search.component.tsx index 5f4738801..74b5bf008 100644 --- a/packages/esm-patient-search-app/src/patient-search-page/advanced-patient-search.component.tsx +++ b/packages/esm-patient-search-app/src/patient-search-page/advanced-patient-search.component.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useMemo, useState } from 'react'; -import { useGetPatientAttributePhoneUuid, usePatientSearchInfinite } from '../patient-search.resource'; +import { useInfinitePatientSearch } from '../patient-search.resource'; import { AdvancedPatientSearchState } from '../types'; import styles from './advanced-patient-search.scss'; import { initialState } from './advanced-search-reducer'; @@ -39,7 +39,7 @@ const AdvancedPatientSearchComponent: React.FC = ({ hasMore, isLoading, fetchError, - } = usePatientSearchInfinite(query, false, !!query, 50); + } = useInfinitePatientSearch(query, false, !!query, 50); useEffect(() => { if (searchResults?.length === currentPage * 50 && hasMore) { diff --git a/packages/esm-patient-search-app/src/patient-search.resource.tsx b/packages/esm-patient-search-app/src/patient-search.resource.tsx index 4229ef939..4ba19ced5 100644 --- a/packages/esm-patient-search-app/src/patient-search.resource.tsx +++ b/packages/esm-patient-search-app/src/patient-search.resource.tsx @@ -1,10 +1,9 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; import useSWR from 'swr'; import useSWRInfinite from 'swr/infinite'; -import useSWRImmutable from 'swr/immutable'; -import { openmrsFetch, useConfig, FetchResponse, openmrsObservableFetch, showToast } from '@openmrs/esm-framework'; -import { PatientSearchResponse, SearchedPatient } from './types'; -import { useTranslation } from 'react-i18next'; +import { openmrsFetch, showNotification, useSession, FetchResponse, LoggedInUser } from '@openmrs/esm-framework'; +import type { PatientSearchResponse, SearchedPatient, User } from './types'; const v = 'custom:(patientId,uuid,identifiers,display,' + @@ -12,49 +11,7 @@ const v = 'person:(gender,age,birthdate,birthdateEstimated,personName,addresses,display,dead,deathDate),' + 'attributes:(value,attributeType:(uuid,display)))'; -export function usePatientSearchPaginated( - searchTerm: string, - searching: boolean = true, - resultsToFetch: number, - page: number, - customRepresentation: string = v, -) { - const config = useConfig(); - let url = `/ws/rest/v1/patient?q=${searchTerm}&v=${customRepresentation}&limit=${resultsToFetch}&totalCount=true`; - if (config.includeDead) { - url += `&includeDead=${config?.includeDead}`; - } - if (page > 1) { - url += `&startIndex=${(page - 1) * resultsToFetch}`; - } - - const { data, isValidating, error } = useSWR< - FetchResponse<{ results: Array; links: Array<{ rel: 'prev' | 'next' }>; totalCount: number }> - >(searching ? url : null, openmrsFetch); - - const results: { - data: Array; - isLoading: boolean; - fetchError: any; - hasMore: boolean; - loadingNewData: boolean; - totalResults: number; - } = useMemo( - () => ({ - data: data?.data?.results, - isLoading: !data?.data && !error, - fetchError: error, - hasMore: data?.data?.links?.some((link) => link.rel === 'next'), - loadingNewData: isValidating, - totalResults: data?.data?.totalCount, - }), - [data, isValidating, error], - ); - - return results; -} - -export function usePatientSearchInfinite( +export function useInfinitePatientSearch( searchTerm: string, includeDead: boolean, searching: boolean = true, @@ -100,20 +57,101 @@ export function usePatientSearchInfinite( return results; } -export function useGetPatientAttributePhoneUuid(): string { +export function useRecentlyViewedPatients() { const { t } = useTranslation(); - const { data, error, isLoading } = useSWRImmutable }>>( - '/ws/rest/v1/personattributetype?q=Telephone Number', + const { user } = useSession(); + const userUuid = user?.uuid; + const { data, error, mutate } = useSWR, Error>( + userUuid ? `/ws/rest/v1/user/${userUuid}` : null, openmrsFetch, ); - if (error) { - showToast({ - description: `${t( - 'fetchingPhoneNumberUuidFailed', - 'Fetching Phone number attribute type UUID failed with error', - )}: ${error?.message}`, - kind: 'error', - }); - } - return data?.data?.results?.[0]?.uuid; + + useEffect(() => { + if (error) { + showNotification({ + kind: 'error', + title: t('errorFetchingUserProperties', 'Error fetching user properties'), + description: error.message, + }); + } + }, [error, t]); + + const result = useMemo( + () => ({ + isLoadingPatients: !data && !error, + recentlyViewedPatients: data?.data?.userProperties?.patientsVisited?.split(',') ?? [], + mutateUserProperties: mutate, + }), + [data, error, mutate], + ); + + return result; +} + +export function useRESTPatients( + patientUuids: string[], + searching: boolean = true, + resultsToFetch: number = 10, + customRepresentation: string = v, +): PatientSearchResponse { + const { t } = useTranslation(); + + const getPatientUrl = (index) => { + if (index < patientUuids.length) { + return `/ws/rest/v1/patient/${patientUuids[index]}?v=${customRepresentation}`; + } else { + return null; + } + }; + + const { data, isLoading, isValidating, setSize, error, size } = useSWRInfinite< + FetchResponse<{ results: Array; links: Array<{ rel: 'prev' | 'next' }>; totalCount: number }>, + Error + >(searching ? getPatientUrl : null, openmrsFetch, { + initialSize: resultsToFetch < patientUuids.length ? resultsToFetch : patientUuids.length, + }); + + useEffect(() => { + if (error) { + showNotification({ + kind: 'error', + title: t('fetchingPatientFailed', 'Fetching patient details failed'), + description: error.message, + }); + } + }, [error, t]); + + const results = useMemo( + () => ({ + data: data ? [].concat(...data?.map((resp) => resp?.data)) : null, + isLoading: isLoading, + fetchError: error, + hasMore: data?.length ? !!data[data.length - 1].data?.links?.some((link) => link.rel === 'next') : false, + loadingNewData: isValidating, + setPage: setSize, + currentPage: size, + totalResults: data?.[0]?.data?.totalCount, + }), + [data, isLoading, isValidating, error, setSize, size], + ); + + return results; +} + +export function updateRecentlyViewedPatients(patientUuid: string, user: LoggedInUser) { + const recentlyViewedPatients: Array = user?.userProperties?.patientsVisited?.split(',') ?? []; + const restPatients = recentlyViewedPatients.filter((uuid) => uuid !== patientUuid); + const newPatientsVisited = [patientUuid, ...restPatients].join(','); + + return openmrsFetch(`/ws/rest/v1/user/${user?.uuid}`, { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: { + userProperties: { + patientsVisited: newPatientsVisited, + }, + }, + }); } diff --git a/packages/esm-patient-search-app/src/types/index.ts b/packages/esm-patient-search-app/src/types/index.ts index e4689f230..73b5ae233 100644 --- a/packages/esm-patient-search-app/src/types/index.ts +++ b/packages/esm-patient-search-app/src/types/index.ts @@ -117,3 +117,11 @@ export interface AdvancedPatientSearchAction { postcode?: string; age?: number; } + +export interface User { + uuid: string; + userProperties: { + [x: string]: string; + patientsVisited: string; + }; +} diff --git a/packages/esm-patient-search-app/translations/am.json b/packages/esm-patient-search-app/translations/am.json index 906843d27..c2b31f7ea 100644 --- a/packages/esm-patient-search-app/translations/am.json +++ b/packages/esm-patient-search-app/translations/am.json @@ -22,6 +22,8 @@ "phoneNumber": "Phone number", "postcode": "Postcode", "previousPage": "Previous page", + "recentSearchResultsCount": "{count} recent search result{plural}", + "recentSearchResultsCount_plural": "{count} recent search result{plural}", "refineSearch": "Refine search", "refineSearchHeaderText": "Add additional search criteria", "refineSearchTabletBannerText": "Can't find who you're looking for?", diff --git a/packages/esm-patient-search-app/translations/en.json b/packages/esm-patient-search-app/translations/en.json index 279f4527f..dea82a333 100644 --- a/packages/esm-patient-search-app/translations/en.json +++ b/packages/esm-patient-search-app/translations/en.json @@ -22,6 +22,8 @@ "phoneNumber": "Phone number", "postcode": "Postcode", "previousPage": "Previous page", + "recentSearchResultsCount": "{count} recent search result{plural}", + "recentSearchResultsCount_plural": "{count} recent search result{plural}", "refineSearch": "Refine search", "refineSearchHeaderText": "Add additional search criteria", "refineSearchTabletBannerText": "Can't find who you're looking for?", diff --git a/packages/esm-patient-search-app/translations/es.json b/packages/esm-patient-search-app/translations/es.json index 6cee7a796..2b54a1c10 100644 --- a/packages/esm-patient-search-app/translations/es.json +++ b/packages/esm-patient-search-app/translations/es.json @@ -22,6 +22,8 @@ "phoneNumber": "Phone number", "postcode": "Postcode", "previousPage": "Previous page", + "recentSearchResultsCount": "{count} recent search result{plural}", + "recentSearchResultsCount_plural": "{count} recent search result{plural}", "refineSearch": "Refine search", "refineSearchHeaderText": "Add additional search criteria", "refineSearchTabletBannerText": "Can't find who you're looking for?", diff --git a/packages/esm-patient-search-app/translations/fr.json b/packages/esm-patient-search-app/translations/fr.json index 9dd2de7d0..a72afb8ab 100644 --- a/packages/esm-patient-search-app/translations/fr.json +++ b/packages/esm-patient-search-app/translations/fr.json @@ -22,6 +22,8 @@ "phoneNumber": "Numéro de téléphone", "postcode": "Code postal", "previousPage": "Page précédente", + "recentSearchResultsCount": "{count} recent search result{plural}", + "recentSearchResultsCount_plural": "{count} recent search result{plural}", "refineSearch": "Affiner la recherche", "refineSearchHeaderText": "Ajouter un critère de recherche supplémentaire", "refineSearchTabletBannerText": "Vous ne trouvez pas la personne recherchée?", diff --git a/packages/esm-patient-search-app/translations/he.json b/packages/esm-patient-search-app/translations/he.json index 6b7618380..16d4a2623 100644 --- a/packages/esm-patient-search-app/translations/he.json +++ b/packages/esm-patient-search-app/translations/he.json @@ -22,6 +22,10 @@ "phoneNumber": "מספר טלפון", "postcode": "מיקוד", "previousPage": "הדף הקודם", + "recentSearchResultsCount_0": "{count} recent search result{plural}", + "recentSearchResultsCount_1": "{count} recent search result{plural}", + "recentSearchResultsCount_2": "{count} recent search result{plural}", + "recentSearchResultsCount_3": "{count} recent search result{plural}", "refineSearch": "צמצם חיפוש", "refineSearchHeaderText": "הוסף קריטריונים נוספים לחיפוש", "refineSearchTabletBannerText": "לא ניתן למצוא את האדם שאתה מחפש?", diff --git a/packages/esm-patient-search-app/translations/km.json b/packages/esm-patient-search-app/translations/km.json index 6762d4c75..eb167f74e 100644 --- a/packages/esm-patient-search-app/translations/km.json +++ b/packages/esm-patient-search-app/translations/km.json @@ -22,6 +22,7 @@ "phoneNumber": "លេខទូរស័ព្ទ", "postcode": "លេខកូដប្រៃសណីយ៍", "previousPage": "ទំព័រ​មុន", + "recentSearchResultsCount": "{count} recent search result{plural}", "refineSearch": "កែលម្អការស្វែងរក", "refineSearchHeaderText": "បន្ថែមលក្ខណៈវិនិច្ឆ័យស្វែងរកបន្ថែម", "refineSearchTabletBannerText": "រកមិនឃើញអ្នកណាដែលអ្នកកំពុងស្វែងរក?", From 0e3568772bfc9ca2d355ec519376bb657e3d3793 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Mon, 3 Jul 2023 23:53:53 +0530 Subject: [PATCH 4/6] (test) O3-2218 O3-2219: Add test for `add-patient-link` and `nav-link tests` components (#749) * add-patient-link and nav-link tests * fixed test suite name --- .../src/add-patient-link.test.tsx | 18 ++++++++++++++++++ .../src/nav-link.test.tsx | 13 +++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 packages/esm-patient-registration-app/src/add-patient-link.test.tsx create mode 100644 packages/esm-patient-registration-app/src/nav-link.test.tsx diff --git a/packages/esm-patient-registration-app/src/add-patient-link.test.tsx b/packages/esm-patient-registration-app/src/add-patient-link.test.tsx new file mode 100644 index 000000000..c8d96949f --- /dev/null +++ b/packages/esm-patient-registration-app/src/add-patient-link.test.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import * as esmFramework from '@openmrs/esm-framework'; +import Root from './add-patient-link'; + +describe('Add patient link component', () => { + it('renders an "Add Patient" button and triggers navigation on click', () => { + const navigateMock = jest.fn(); + jest.spyOn(esmFramework, 'navigate').mockImplementation(navigateMock); + + const { getByRole } = render(); + const addButton = getByRole('button', { name: /add patient/i }); + + fireEvent.click(addButton); + + expect(navigateMock).toHaveBeenCalledWith({ to: '${openmrsSpaBase}/patient-registration' }); + }); +}); diff --git a/packages/esm-patient-registration-app/src/nav-link.test.tsx b/packages/esm-patient-registration-app/src/nav-link.test.tsx new file mode 100644 index 000000000..36428973e --- /dev/null +++ b/packages/esm-patient-registration-app/src/nav-link.test.tsx @@ -0,0 +1,13 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import Root from './nav-link'; + +describe('Nav link component', () => { + it('renders a link to the patient registration page', () => { + const { getByText } = render(); + const linkElement = getByText('Patient Registration'); + + expect(linkElement).toBeInTheDocument(); + expect(linkElement).toHaveAttribute('href', '/openmrs/spa/patient-registration'); + }); +}); From 763c5b5ac318e64b9ff4145bd21584c1d007fef2 Mon Sep 17 00:00:00 2001 From: Ayush <54752747+ayush-AI@users.noreply.github.com> Date: Tue, 4 Jul 2023 10:39:56 +0530 Subject: [PATCH 5/6] fixed the failing skipped tests (#680) Co-authored-by: Dennis Kigen --- .../death-info/death-info-section.test.tsx | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx b/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx index 8e55bfe20..6ce31922b 100644 --- a/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx +++ b/packages/esm-patient-registration-app/src/patient-registration/section/death-info/death-info-section.test.tsx @@ -4,6 +4,7 @@ import { Formik, Form } from 'formik'; import { initialFormValues } from '../../patient-registration.component'; import { DeathInfoSection } from './death-info-section.component'; import { FormValues } from '../../patient-registration-types'; +import { PatientRegistrationContext } from '../../patient-registration-context'; jest.mock('@openmrs/esm-framework', () => { const originalModule = jest.requireActual('@openmrs/esm-framework'); @@ -15,16 +16,18 @@ jest.mock('@openmrs/esm-framework', () => { }); // TODO: Implement feature and get tests to pass -describe.skip('death info section', () => { +describe('death info section', () => { const formValues: FormValues = initialFormValues; const setupSection = async (isDead?: boolean) => { render( - -
- - -
, + + +
+ + +
+
, ); const allInputs = screen.queryAllByLabelText( (content, element) => element.tagName.toLowerCase() === 'input', From 2d15566d54962110c156cee0e67e270c0b8ae66b Mon Sep 17 00:00:00 2001 From: CynthiaKamau Date: Thu, 6 Jul 2023 12:25:39 +0300 Subject: [PATCH 6/6] (feat) Remove filtering todays queue entries on frontend (#752) --- .../src/active-visits/active-visits-table.resource.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts b/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts index 005c39533..3829b9a14 100644 --- a/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts +++ b/packages/esm-outpatient-app/src/active-visits/active-visits-table.resource.ts @@ -253,9 +253,7 @@ export function useVisitQueueEntries(currServiceName: string, locationUuid: stri ?.map(mapVisitQueueEntryProperties) .filter((data) => dayjs(data.visitStartDateTime).isToday()); } else { - mappedVisitQueueEntries = data?.data?.results - ?.map(mapVisitQueueEntryProperties) - .filter((data) => data.service === currServiceName && dayjs(data.visitStartDateTime).isToday()); + mappedVisitQueueEntries = data?.data?.results?.map(mapVisitQueueEntryProperties); } return { @@ -349,9 +347,7 @@ export function useServiceQueueEntries(service: string, locationUuid: string) { patientUuid: visitQueueEntry.queueEntry ? visitQueueEntry?.queueEntry.uuid : '--', }); - const mappedServiceQueueEntries = data?.data?.results - ?.map(mapServiceQueueEntryProperties) - .filter(({ returnDate }) => dayjs(returnDate).isToday()); + const mappedServiceQueueEntries = data?.data?.results?.map(mapServiceQueueEntryProperties); return { serviceQueueEntries: mappedServiceQueueEntries ? mappedServiceQueueEntries : [],