From aa528d2504ce932528fb47d2d3413fb1631de1b9 Mon Sep 17 00:00:00 2001 From: Donald Kibet Date: Wed, 2 Mar 2022 14:33:18 +0300 Subject: [PATCH 1/5] Add recommended visit work-flow --- .../src/config-schema.ts | 5 ++ .../hooks/usePatientProgramEnrollment.tsx | 31 ++++++++++ .../visit/hooks/useRecommendedVisitTypes.tsx | 23 ++++++++ ...nent.tsx => base-visit-type.component.tsx} | 9 +-- .../recommended-visit-type.component.tsx | 57 +++++++++++++++++++ .../visit/visit-form/visit-form.component.tsx | 44 ++++++++++---- 6 files changed, 154 insertions(+), 15 deletions(-) create mode 100644 packages/esm-patient-chart-app/src/visit/hooks/usePatientProgramEnrollment.tsx create mode 100644 packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx rename packages/esm-patient-chart-app/src/visit/visit-form/{visit-type-overview.component.tsx => base-visit-type.component.tsx} (91%) create mode 100644 packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx diff --git a/packages/esm-patient-chart-app/src/config-schema.ts b/packages/esm-patient-chart-app/src/config-schema.ts index cb39b7e67a..223043f08b 100644 --- a/packages/esm-patient-chart-app/src/config-schema.ts +++ b/packages/esm-patient-chart-app/src/config-schema.ts @@ -22,6 +22,11 @@ export const esmPatientChartSchema = { _description: 'The UUID of the visit type to be used for the automatically created offline visits.', _default: 'a22733fa-3501-4020-a520-da024eeff088', }, + displayRecommendedVisitType: { + _type: Type.Boolean, + _description: 'Whether start visit form should display recommended visit type tab', + _default: false, + }, }; export interface ChartConfig { diff --git a/packages/esm-patient-chart-app/src/visit/hooks/usePatientProgramEnrollment.tsx b/packages/esm-patient-chart-app/src/visit/hooks/usePatientProgramEnrollment.tsx new file mode 100644 index 0000000000..27018ccb96 --- /dev/null +++ b/packages/esm-patient-chart-app/src/visit/hooks/usePatientProgramEnrollment.tsx @@ -0,0 +1,31 @@ +import useSWR from 'swr'; +import { openmrsFetch, OpenmrsResource } from '@openmrs/esm-framework'; +import { useMemo } from 'react'; +const customRepresentation = `custom:(uuid,display,program,dateEnrolled,dateCompleted,location:(uuid,display))`; + +export interface PatientProgram { + uuid: string; + display: string; + patient: OpenmrsResource; + program: OpenmrsResource; + dateEnrolled: string; + dateCompleted: string; + location: OpenmrsResource; +} + +export const useActivePatientEnrollment = (patientUuid: string) => { + const { data, error } = useSWR<{ data: { results: Array } }>( + `/ws/rest/v1/programenrollment?patient=${patientUuid}&v=${customRepresentation}`, + openmrsFetch, + ); + + const activePatientEnrollment = useMemo( + () => + data?.data.results + .sort((a, b) => (b.dateEnrolled > a.dateEnrolled ? 1 : -1)) + .filter((enrollment) => enrollment.dateCompleted === null) ?? [], + [data?.data.results], + ); + + return { activePatientEnrollment, error, isLoading: !data && !error }; +}; diff --git a/packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx b/packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx new file mode 100644 index 0000000000..d5acd0eb11 --- /dev/null +++ b/packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx @@ -0,0 +1,23 @@ +import useSWR from 'swr'; +import { openmrsFetch } from '@openmrs/esm-framework'; + +export const useRecommendedVisitTypes = ( + patientUuid: string, + enrollmentUuid: string, + programUuid: string, + locationUuid: string, +) => { + const { data, error } = useSWR<{ data: any }>( + patientUuid && + enrollmentUuid && + programUuid && + `/etl-latest/etl/patient/${patientUuid}/program/${programUuid}/enrollment/${enrollmentUuid}?intendedLocationUuid=${locationUuid}`, + openmrsFetch, + ); + const recommendedVisitTypes = data?.data?.visitTypes?.allowed.map(mapToVisitType) ?? []; + return { recommendedVisitTypes, error, isLoading: !recommendedVisitTypes && !error }; +}; + +const mapToVisitType = (visitType) => { + return { ...visitType, display: visitType.name }; +}; diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-type-overview.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.tsx similarity index 91% rename from packages/esm-patient-chart-app/src/visit/visit-form/visit-type-overview.component.tsx rename to packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.tsx index 55764df31d..c955da2fdc 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-type-overview.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.tsx @@ -7,14 +7,15 @@ import { PatientChartPagination } from '@openmrs/esm-patient-common-lib'; import { useTranslation } from 'react-i18next'; import { useLayoutType, usePagination, useVisitTypes } from '@openmrs/esm-framework'; -interface VisitTypeOverviewProps { +interface BaseVisitTypeProps { onChange: (event) => void; + patientUuid: string; + visitTypes; } -const VisitTypeOverview: React.FC = ({ onChange }) => { +const BaseVisitType: React.FC = ({ onChange, patientUuid, visitTypes }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; - const visitTypes = useVisitTypes(); const [searchTerm, setSearchTerm] = useState(''); const searchResults = useMemo(() => { @@ -63,4 +64,4 @@ const VisitTypeOverview: React.FC = ({ onChange }) => { ); }; -export default VisitTypeOverview; +export default BaseVisitType; diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx new file mode 100644 index 0000000000..3818884d66 --- /dev/null +++ b/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx @@ -0,0 +1,57 @@ +import React, { useState } from 'react'; +import { Tag } from 'carbon-components-react'; +import { useRecommendedVisitTypes } from '../hooks/useRecommendedVisitTypes'; +import { PatientProgram } from '../hooks/usePatientProgramEnrollment'; +import BaseVisitType from './base-visit-type.component'; +import { EmptyState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; + +interface RecommendedVisitTypeProp { + patientUuid: string; + activePatientEnrollment: Array; + onChange: (visitTypeUuid) => void; + locationUuid: string; +} + +const RecommendedVisitType: React.FC = ({ + patientUuid, + activePatientEnrollment, + onChange, + locationUuid, +}) => { + const [selectedProgram, setSelectedProgram] = useState(activePatientEnrollment[0]); + const { recommendedVisitTypes, error } = useRecommendedVisitTypes( + patientUuid, + selectedProgram?.uuid, + selectedProgram?.program?.uuid, + locationUuid, + ); + + return ( +
+ {activePatientEnrollment.length === 0 && ( + launchPatientWorkspace('programs-form-workspace')} + /> + )} + {activePatientEnrollment.map((enrollment) => ( + { + setSelectedProgram(enrollment); + e.preventDefault(); + }} + type={selectedProgram?.uuid === enrollment.uuid ? 'blue' : 'cool-gray'} + key={enrollment.uuid} + > + {enrollment.program['name']} + + ))} + {recommendedVisitTypes?.length > 0 && ( + + )} +
+ ); +}; + +export default RecommendedVisitType; diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx index 944b04d057..a74afc297b 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx @@ -33,17 +33,22 @@ import { toOmrsIsoString, toDateObjectStrict, useLayoutType, + useVisitTypes, + useConfig, } from '@openmrs/esm-framework'; import { amPm, convertTime12to24, DefaultWorkspaceProps } from '@openmrs/esm-patient-common-lib'; -import VisitTypeOverview from './visit-type-overview.component'; -import styles from './visit-form.scss'; +import BaseVisitType from './base-visit-type.component'; +import styles from './visit-form.component.scss'; +import RecommendedVisitType from './recommended-visit-type.component'; +import { useActivePatientEnrollment } from '../hooks/usePatientProgramEnrollment'; const StartVisitForm: React.FC = ({ patientUuid, closeWorkspace }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const locations = useLocations(); const sessionUser = useSessionUser(); - const [contentSwitcherIndex, setContentSwitcherIndex] = useState(1); + const config = useConfig(); + const [contentSwitcherIndex, setContentSwitcherIndex] = useState(config.displayRecommendedVisitType ? 0 : 1); const [isMissingVisitType, setIsMissingVisitType] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [selectedLocation, setSelectedLocation] = useState(''); @@ -52,6 +57,8 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor const [visitTime, setVisitTime] = useState(dayjs(new Date()).format('hh:mm')); const [visitType, setVisitType] = useState(null); const state = useMemo(() => ({ patientUuid }), [patientUuid]); + const allVisitTypes = useVisitTypes(); + const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid); useEffect(() => { if (locations && sessionUser?.sessionLocation?.uuid) { @@ -117,7 +124,7 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor ); return ( -
+ {isTablet && ( @@ -204,12 +211,27 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor - { - setVisitType(visitType); - setIsMissingVisitType(false); - }} - /> + {contentSwitcherIndex === 0 && !isLoading && ( + { + setVisitType(visitType); + setIsMissingVisitType(false); + }} + patientUuid={patientUuid} + activePatientEnrollment={activePatientEnrollment} + locationUuid={selectedLocation} + /> + )} + {contentSwitcherIndex === 1 && ( + { + setVisitType(visitType); + setIsMissingVisitType(false); + }} + visitTypes={allVisitTypes} + patientUuid={patientUuid} + /> + )} {isMissingVisitType && ( @@ -231,7 +253,7 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor - From 6390e776771b7fc37a5325dc386f2fc2a9ac8ed5 Mon Sep 17 00:00:00 2001 From: Donald Kibet Date: Thu, 3 Mar 2022 10:14:14 +0300 Subject: [PATCH 2/5] Add ability to recommended clinical forms --- .../src/config-schema.ts | 2 +- .../base-visit-type.component.test.tsx | 47 ++++++++++++++ .../recommended-visit-type.component.tsx | 18 ++++-- .../visit/visit-form/visit-form.component.tsx | 12 +++- .../src/visit/visit-form/visit-type.test.tsx | 4 +- packages/esm-patient-common-lib/src/index.ts | 1 + .../programs}/usePatientProgramEnrollment.tsx | 20 +++--- .../esm-patient-common-lib/src/types/index.ts | 12 ++++ .../src/config-schema.ts | 6 ++ .../forms-detailed-overview.component.tsx | 26 +++++--- .../src/forms/forms-overview.component.tsx | 26 +++++--- .../src/forms/forms.component.tsx | 64 ++++++++++++++++--- .../src/hooks/use-forms.ts | 1 + .../src/hooks/use-program-config.ts | 21 ++++++ .../translations/en.json | 1 + 15 files changed, 213 insertions(+), 48 deletions(-) create mode 100644 packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.test.tsx rename packages/{esm-patient-chart-app/src/visit/hooks => esm-patient-common-lib/src/programs}/usePatientProgramEnrollment.tsx (66%) create mode 100644 packages/esm-patient-forms-app/src/hooks/use-program-config.ts diff --git a/packages/esm-patient-chart-app/src/config-schema.ts b/packages/esm-patient-chart-app/src/config-schema.ts index 223043f08b..fd17ffb2b9 100644 --- a/packages/esm-patient-chart-app/src/config-schema.ts +++ b/packages/esm-patient-chart-app/src/config-schema.ts @@ -22,7 +22,7 @@ export const esmPatientChartSchema = { _description: 'The UUID of the visit type to be used for the automatically created offline visits.', _default: 'a22733fa-3501-4020-a520-da024eeff088', }, - displayRecommendedVisitType: { + showRecommendedVisitType: { _type: Type.Boolean, _description: 'Whether start visit form should display recommended visit type tab', _default: false, diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.test.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.test.tsx new file mode 100644 index 0000000000..11f2c2637c --- /dev/null +++ b/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.test.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import { usePagination, useVisitTypes } from '@openmrs/esm-framework'; +import { screen, render } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { mockVisitTypes } from '../../../../../__mocks__/visits.mock'; +import BaseVisitType from './base-visit-type.component'; + +jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn)); + +const mockUsePagination = usePagination as jest.Mock; +const mockUseVisitTypes = useVisitTypes as jest.Mock; +const mockHandleChange = jest.fn(); +const mockGoToPage = jest.fn(); + +jest.mock('@openmrs/esm-framework', () => ({ + ...(jest.requireActual('@openmrs/esm-framework') as any), + usePagination: jest.fn(), + useVisitTypes: jest.fn(), +})); + +describe('VisitTypeOverview', () => { + const renderVisitTypeOverview = () => { + mockUsePagination.mockReturnValue({ + results: mockVisitTypes.slice(0, 2), + goTo: mockGoToPage, + currentPage: 1, + }); + mockUseVisitTypes.mockReturnValue(mockVisitTypes); + render(); + }; + + it('should be able to search for a visit type', () => { + renderVisitTypeOverview(); + + const hivVisit = screen.getByRole('radio', { name: /HIV Return Visit/i }); + const outpatientVisit = screen.getByRole('radio', { name: /Outpatient Visit/i }); + + expect(outpatientVisit).toBeInTheDocument(); + expect(hivVisit).toBeInTheDocument(); + + const searchInput = screen.getByRole('searchbox'); + userEvent.type(searchInput, 'HIV'); + + expect(outpatientVisit).toBeEmptyDOMElement(); + expect(hivVisit).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx index 3818884d66..fda296aedb 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx @@ -1,15 +1,16 @@ -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { Tag } from 'carbon-components-react'; import { useRecommendedVisitTypes } from '../hooks/useRecommendedVisitTypes'; -import { PatientProgram } from '../hooks/usePatientProgramEnrollment'; import BaseVisitType from './base-visit-type.component'; -import { EmptyState, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; +import { EmptyState, launchPatientWorkspace, PatientProgram } from '@openmrs/esm-patient-common-lib'; +import { useTranslation } from 'react-i18next'; interface RecommendedVisitTypeProp { patientUuid: string; activePatientEnrollment: Array; onChange: (visitTypeUuid) => void; locationUuid: string; + onProgramUuidChange: (uuid) => void; } const RecommendedVisitType: React.FC = ({ @@ -17,7 +18,9 @@ const RecommendedVisitType: React.FC = ({ activePatientEnrollment, onChange, locationUuid, + onProgramUuidChange, }) => { + const { t } = useTranslation(); const [selectedProgram, setSelectedProgram] = useState(activePatientEnrollment[0]); const { recommendedVisitTypes, error } = useRecommendedVisitTypes( patientUuid, @@ -26,18 +29,23 @@ const RecommendedVisitType: React.FC = ({ locationUuid, ); + useEffect(() => { + onProgramUuidChange(selectedProgram.program.uuid); + }, []); + return (
{activePatientEnrollment.length === 0 && ( launchPatientWorkspace('programs-form-workspace')} /> )} {activePatientEnrollment.map((enrollment) => ( { + onProgramUuidChange(enrollment.program.uuid); setSelectedProgram(enrollment); e.preventDefault(); }} diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx index a74afc297b..6f93c77be0 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx @@ -36,11 +36,15 @@ import { useVisitTypes, useConfig, } from '@openmrs/esm-framework'; -import { amPm, convertTime12to24, DefaultWorkspaceProps } from '@openmrs/esm-patient-common-lib'; +import { + amPm, + convertTime12to24, + DefaultWorkspaceProps, + useActivePatientEnrollment, +} from '@openmrs/esm-patient-common-lib'; import BaseVisitType from './base-visit-type.component'; import styles from './visit-form.component.scss'; import RecommendedVisitType from './recommended-visit-type.component'; -import { useActivePatientEnrollment } from '../hooks/usePatientProgramEnrollment'; const StartVisitForm: React.FC = ({ patientUuid, closeWorkspace }) => { const { t } = useTranslation(); @@ -48,7 +52,7 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor const locations = useLocations(); const sessionUser = useSessionUser(); const config = useConfig(); - const [contentSwitcherIndex, setContentSwitcherIndex] = useState(config.displayRecommendedVisitType ? 0 : 1); + const [contentSwitcherIndex, setContentSwitcherIndex] = useState(config.showRecommendedVisitType ? 0 : 1); const [isMissingVisitType, setIsMissingVisitType] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [selectedLocation, setSelectedLocation] = useState(''); @@ -56,6 +60,7 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor const [visitDate, setVisitDate] = useState(new Date()); const [visitTime, setVisitTime] = useState(dayjs(new Date()).format('hh:mm')); const [visitType, setVisitType] = useState(null); + const [programUuid, setProgramUuid] = useState(); const state = useMemo(() => ({ patientUuid }), [patientUuid]); const allVisitTypes = useVisitTypes(); const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid); @@ -220,6 +225,7 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor patientUuid={patientUuid} activePatientEnrollment={activePatientEnrollment} locationUuid={selectedLocation} + onProgramUuidChange={setProgramUuid} /> )} {contentSwitcherIndex === 1 && ( diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-type.test.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-type.test.tsx index df97aa3d99..11f2c2637c 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-type.test.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-type.test.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import VisitTypeOverview from './visit-type-overview.component'; import { usePagination, useVisitTypes } from '@openmrs/esm-framework'; import { screen, render } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { mockVisitTypes } from '../../../../../__mocks__/visits.mock'; +import BaseVisitType from './base-visit-type.component'; jest.mock('lodash-es/debounce', () => jest.fn((fn) => fn)); @@ -26,7 +26,7 @@ describe('VisitTypeOverview', () => { currentPage: 1, }); mockUseVisitTypes.mockReturnValue(mockVisitTypes); - render(); + render(); }; it('should be able to search for a visit type', () => { diff --git a/packages/esm-patient-common-lib/src/index.ts b/packages/esm-patient-common-lib/src/index.ts index 89d0b81c26..51ba5356cd 100644 --- a/packages/esm-patient-common-lib/src/index.ts +++ b/packages/esm-patient-common-lib/src/index.ts @@ -9,3 +9,4 @@ export * from './time-helper'; export * from './useVitalsConceptMetadata'; export * from './form-entry/form-entry'; export * from './workspaces'; +export * from './programs/usePatientProgramEnrollment'; diff --git a/packages/esm-patient-chart-app/src/visit/hooks/usePatientProgramEnrollment.tsx b/packages/esm-patient-common-lib/src/programs/usePatientProgramEnrollment.tsx similarity index 66% rename from packages/esm-patient-chart-app/src/visit/hooks/usePatientProgramEnrollment.tsx rename to packages/esm-patient-common-lib/src/programs/usePatientProgramEnrollment.tsx index 27018ccb96..ae5307e321 100644 --- a/packages/esm-patient-chart-app/src/visit/hooks/usePatientProgramEnrollment.tsx +++ b/packages/esm-patient-common-lib/src/programs/usePatientProgramEnrollment.tsx @@ -1,18 +1,10 @@ import useSWR from 'swr'; -import { openmrsFetch, OpenmrsResource } from '@openmrs/esm-framework'; +import { openmrsFetch } from '@openmrs/esm-framework'; import { useMemo } from 'react'; +import { PatientProgram } from '../types'; +import uniqBy from 'lodash-es/uniqBy'; const customRepresentation = `custom:(uuid,display,program,dateEnrolled,dateCompleted,location:(uuid,display))`; -export interface PatientProgram { - uuid: string; - display: string; - patient: OpenmrsResource; - program: OpenmrsResource; - dateEnrolled: string; - dateCompleted: string; - location: OpenmrsResource; -} - export const useActivePatientEnrollment = (patientUuid: string) => { const { data, error } = useSWR<{ data: { results: Array } }>( `/ws/rest/v1/programenrollment?patient=${patientUuid}&v=${customRepresentation}`, @@ -27,5 +19,9 @@ export const useActivePatientEnrollment = (patientUuid: string) => { [data?.data.results], ); - return { activePatientEnrollment, error, isLoading: !data && !error }; + return { + activePatientEnrollment: uniqBy(activePatientEnrollment, (program) => program?.program?.uuid), + error, + isLoading: !data && !error, + }; }; diff --git a/packages/esm-patient-common-lib/src/types/index.ts b/packages/esm-patient-common-lib/src/types/index.ts index e82814aef9..8b33462bb0 100644 --- a/packages/esm-patient-common-lib/src/types/index.ts +++ b/packages/esm-patient-common-lib/src/types/index.ts @@ -1,3 +1,5 @@ +import { OpenmrsResource } from '@openmrs/esm-framework'; + export * from './test-results'; export * from './workspace'; @@ -14,3 +16,13 @@ export interface DashboardConfig extends DashboardLinkConfig { slot: string; config: DashbardConfig; } + +export interface PatientProgram { + uuid: string; + display: string; + patient: OpenmrsResource; + program: OpenmrsResource; + dateEnrolled: string; + dateCompleted: string; + location: OpenmrsResource; +} diff --git a/packages/esm-patient-forms-app/src/config-schema.ts b/packages/esm-patient-forms-app/src/config-schema.ts index caf50dfb4f..1c76cfab08 100644 --- a/packages/esm-patient-forms-app/src/config-schema.ts +++ b/packages/esm-patient-forms-app/src/config-schema.ts @@ -67,6 +67,11 @@ export const configSchema = { _default: true, _description: 'Whether HTML Form Entry forms should be included in lists of forms', }, + showRecommendedFormsTab: { + _type: Type.Boolean, + _description: 'Whether to display recommended forms tab', + _default: false, + }, }; export interface HtmlFormEntryForm { @@ -78,4 +83,5 @@ export interface HtmlFormEntryForm { export interface ConfigObject { htmlFormEntryForms: Array; + showRecommendedFormsTab: boolean; } diff --git a/packages/esm-patient-forms-app/src/forms/forms-detailed-overview.component.tsx b/packages/esm-patient-forms-app/src/forms/forms-detailed-overview.component.tsx index 72d8e4f38c..92ce06928d 100644 --- a/packages/esm-patient-forms-app/src/forms/forms-detailed-overview.component.tsx +++ b/packages/esm-patient-forms-app/src/forms/forms-detailed-overview.component.tsx @@ -1,6 +1,8 @@ import React from 'react'; import Forms from './forms.component'; import { useTranslation } from 'react-i18next'; +import { useActivePatientEnrollment } from '@openmrs/esm-patient-common-lib'; +import { InlineLoading } from 'carbon-components-react'; interface FormsProps { patientUuid: string; @@ -13,16 +15,24 @@ const FormsDetailedOverView: React.FC = ({ patientUuid, patient, isO const { t } = useTranslation(); const urlLabel: string = t('goToSummary', 'Go to Summary'); const pageUrl: string = `$\{openmrsSpaBase}/patient/${patientUuid}/chart/summary`; + const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid); return ( - + <> + {isLoading ? ( + + ) : ( + + )} + ); }; diff --git a/packages/esm-patient-forms-app/src/forms/forms-overview.component.tsx b/packages/esm-patient-forms-app/src/forms/forms-overview.component.tsx index 399d883adb..2ce42d468c 100644 --- a/packages/esm-patient-forms-app/src/forms/forms-overview.component.tsx +++ b/packages/esm-patient-forms-app/src/forms/forms-overview.component.tsx @@ -1,6 +1,8 @@ import React, { FunctionComponent } from 'react'; import Forms from './forms.component'; import { useTranslation } from 'react-i18next'; +import { InlineLoading } from 'carbon-components-react'; +import { useActivePatientEnrollment } from '@openmrs/esm-patient-common-lib'; interface FormsProps { patientUuid: string; @@ -11,18 +13,26 @@ interface FormsProps { const FormsSummaryDashboard: FunctionComponent = ({ patientUuid, patient, isOffline }) => { const pageSize: number = 5; const { t } = useTranslation(); + const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid); const urlLabel: string = t('seeAll', 'See all'); const pageUrl: string = `$\{openmrsSpaBase}/patient/${patientUuid}/chart/forms`; return ( - + <> + {isLoading ? ( + + ) : ( + + )} + ); }; diff --git a/packages/esm-patient-forms-app/src/forms/forms.component.tsx b/packages/esm-patient-forms-app/src/forms/forms.component.tsx index b476b759ef..d38d04a195 100644 --- a/packages/esm-patient-forms-app/src/forms/forms.component.tsx +++ b/packages/esm-patient-forms-app/src/forms/forms.component.tsx @@ -1,14 +1,15 @@ -import React, { useState } from 'react'; +import React, { useMemo, useState } from 'react'; import FormView from './form-view.component'; import styles from './forms.component.scss'; import EmptyFormView from './empty-form.component'; -import { ContentSwitcher, Switch, DataTableSkeleton, InlineLoading } from 'carbon-components-react'; -import { CardHeader, ErrorState } from '@openmrs/esm-patient-common-lib'; +import { ContentSwitcher, Switch, DataTableSkeleton, InlineLoading, Tag } from 'carbon-components-react'; +import { CardHeader, ErrorState, PatientProgram } from '@openmrs/esm-patient-common-lib'; import { useTranslation } from 'react-i18next'; import { useForms } from '../hooks/use-forms'; -import { useConfig, useLayoutType } from '@openmrs/esm-framework'; +import { useConfig, useLayoutType, useVisit } from '@openmrs/esm-framework'; import { isValidOfflineFormEncounter } from '../offline-forms/offline-form-helpers'; import { ConfigObject } from '../config-schema'; +import { useProgramConfig } from '../hooks/use-program-config'; const enum FormsCategory { Recommended, @@ -23,18 +24,42 @@ interface FormsProps { pageUrl: string; urlLabel: string; isOffline: boolean; + activePatientEnrollment?: Array; } -const Forms: React.FC = ({ patientUuid, patient, pageSize, pageUrl, urlLabel, isOffline }) => { +const Forms: React.FC = ({ + patientUuid, + patient, + pageSize, + pageUrl, + urlLabel, + isOffline, + activePatientEnrollment, +}) => { const { t } = useTranslation(); - const config = useConfig() as ConfigObject; + const { htmlFormEntryForms, showRecommendedFormsTab } = useConfig() as ConfigObject; const headerTitle = t('forms', 'Forms'); const isTablet = useLayoutType() === 'tablet'; - const [formsCategory, setFormsCategory] = useState(FormsCategory.All); + const [formsCategory, setFormsCategory] = useState( + showRecommendedFormsTab ? FormsCategory.Recommended : FormsCategory.All, + ); const { isValidating, data, error } = useForms(patientUuid, undefined, undefined, isOffline); const formsToDisplay = isOffline - ? data?.filter((formInfo) => isValidOfflineFormEncounter(formInfo.form, config.htmlFormEntryForms)) + ? data?.filter((formInfo) => isValidOfflineFormEncounter(formInfo.form, htmlFormEntryForms)) : data; + const { currentVisit } = useVisit(patientUuid); + const { programConfigs } = useProgramConfig(patientUuid); + const [selectedProgram, setSelectedProgram] = useState(activePatientEnrollment[0]); + + const recommendedForms = useMemo( + () => + formsToDisplay?.filter(({ form }) => + programConfigs[selectedProgram.program.uuid]?.visitTypes + ?.find((visitType) => visitType.uuid === currentVisit?.visitType.uuid) + ?.encounterTypes.some(({ uuid }) => uuid === form.encounterType.uuid), + ), + [currentVisit?.visitType.uuid, formsToDisplay, programConfigs, selectedProgram.program.uuid], + ); if (!formsToDisplay && !error) { return ; @@ -90,7 +115,28 @@ const Forms: React.FC = ({ patientUuid, patient, pageSize, pageUrl, /> )} {formsCategory === FormsCategory.Recommended && ( - + <> + {activePatientEnrollment.map((enrollment) => ( + { + setSelectedProgram(enrollment); + e.preventDefault(); + }} + type={selectedProgram?.uuid === enrollment.uuid ? 'blue' : 'cool-gray'} + key={enrollment.uuid} + > + {enrollment.program['name']} + + ))} + + )}
diff --git a/packages/esm-patient-forms-app/src/hooks/use-forms.ts b/packages/esm-patient-forms-app/src/hooks/use-forms.ts index 267d362543..2a17e4bf4e 100644 --- a/packages/esm-patient-forms-app/src/hooks/use-forms.ts +++ b/packages/esm-patient-forms-app/src/hooks/use-forms.ts @@ -48,6 +48,7 @@ export function useForms(patientUuid: string, startDate?: Date, endDate?: Date, data, error: allFormsRes.error, isValidating: allFormsRes.isValidating || encountersRes.isValidating, + allForms: allFormsRes.data, }; } diff --git a/packages/esm-patient-forms-app/src/hooks/use-program-config.ts b/packages/esm-patient-forms-app/src/hooks/use-program-config.ts new file mode 100644 index 0000000000..9ba8ecbabb --- /dev/null +++ b/packages/esm-patient-forms-app/src/hooks/use-program-config.ts @@ -0,0 +1,21 @@ +import { useMemo } from 'react'; +import useSWR from 'swr'; +import { openmrsFetch, OpenmrsResource, Visit } from '@openmrs/esm-framework'; + +interface ProgramConfig { + programUuid: Record< + string, + { name: string; dataDependencies: Array; enrollmentAllowed: boolean; visitTypes: Array } + >; +} + +export const useProgramConfig = (patientUuid: string) => { + const { data, error } = useSWR<{ data: ProgramConfig }>( + `/etl-latest/etl/patient-program-config?patientUuid=${patientUuid}`, + openmrsFetch, + ); + + const programConfigs = useMemo(() => data?.data ?? [], [data]); + + return { programConfigs, error }; +}; diff --git a/packages/esm-patient-forms-app/translations/en.json b/packages/esm-patient-forms-app/translations/en.json index 8c8dbad91c..27e0eceb0c 100644 --- a/packages/esm-patient-forms-app/translations/en.json +++ b/packages/esm-patient-forms-app/translations/en.json @@ -5,6 +5,7 @@ "goToSummary": "Go to Summary", "homeOverviewCardView": "View", "lastCompleted": "Last Completed", + "loading": "Loading...", "matchesFound": "match(es) found", "never": "Never", "noFormsAvailable": "There are no Forms to display for this patient", From 42fd9a4911c09106026099deec159ef5d2f14074 Mon Sep 17 00:00:00 2001 From: Donald Kibet Date: Mon, 7 Mar 2022 14:19:20 +0300 Subject: [PATCH 3/5] Adapt visit and forms recommended tab with design --- .../src/config-schema.ts | 8 +- .../visit/hooks/useRecommendedVisitTypes.tsx | 6 +- .../visit-form/base-visit-type.component.tsx | 4 +- ...nent.test.tsx => base-visit-type.test.tsx} | 0 .../recommended-visit-type.component.tsx | 48 +---- .../visit/visit-form/visit-form.component.tsx | 181 ++++++++++-------- .../src/visit/visit-form/visit-form.scss | 28 ++- .../translations/en.json | 2 + .../src/forms/forms.component.tsx | 36 ++-- yarn.lock | 2 +- 10 files changed, 143 insertions(+), 172 deletions(-) rename packages/esm-patient-chart-app/src/visit/visit-form/{base-visit-type.component.test.tsx => base-visit-type.test.tsx} (100%) diff --git a/packages/esm-patient-chart-app/src/config-schema.ts b/packages/esm-patient-chart-app/src/config-schema.ts index fd17ffb2b9..ce80552338 100644 --- a/packages/esm-patient-chart-app/src/config-schema.ts +++ b/packages/esm-patient-chart-app/src/config-schema.ts @@ -24,11 +24,17 @@ export const esmPatientChartSchema = { }, showRecommendedVisitType: { _type: Type.Boolean, - _description: 'Whether start visit form should display recommended visit type tab', + _description: 'Whether start visit form should display recommended visit type tab. Requires `visitTypeResourceUrl`', _default: false, }, + visitTypeResourceUrl: { + _type: Type.String, + _default: '/etl-latest/etl/patient/', + _description: 'Custom URL to load resources required for showing recommended visit types', + }, }; export interface ChartConfig { offlineVisitTypeUuid: string; + visitTypeResourceUrl: string; } diff --git a/packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx b/packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx index d5acd0eb11..cfc681d901 100644 --- a/packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx +++ b/packages/esm-patient-chart-app/src/visit/hooks/useRecommendedVisitTypes.tsx @@ -1,5 +1,6 @@ import useSWR from 'swr'; -import { openmrsFetch } from '@openmrs/esm-framework'; +import { openmrsFetch, useConfig } from '@openmrs/esm-framework'; +import { ChartConfig } from '../../config-schema'; export const useRecommendedVisitTypes = ( patientUuid: string, @@ -7,11 +8,12 @@ export const useRecommendedVisitTypes = ( programUuid: string, locationUuid: string, ) => { + const config = useConfig() as ChartConfig; const { data, error } = useSWR<{ data: any }>( patientUuid && enrollmentUuid && programUuid && - `/etl-latest/etl/patient/${patientUuid}/program/${programUuid}/enrollment/${enrollmentUuid}?intendedLocationUuid=${locationUuid}`, + `${config.visitTypeResourceUrl}${patientUuid}/program/${programUuid}/enrollment/${enrollmentUuid}?intendedLocationUuid=${locationUuid}`, openmrsFetch, ); const recommendedVisitTypes = data?.data?.visitTypes?.allowed.map(mapToVisitType) ?? []; diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.tsx index c955da2fdc..388e0433f1 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.tsx @@ -5,7 +5,7 @@ import isEmpty from 'lodash-es/isEmpty'; import { Search, RadioButtonGroup, RadioButton } from 'carbon-components-react'; import { PatientChartPagination } from '@openmrs/esm-patient-common-lib'; import { useTranslation } from 'react-i18next'; -import { useLayoutType, usePagination, useVisitTypes } from '@openmrs/esm-framework'; +import { useLayoutType, usePagination } from '@openmrs/esm-framework'; interface BaseVisitTypeProps { onChange: (event) => void; @@ -13,7 +13,7 @@ interface BaseVisitTypeProps { visitTypes; } -const BaseVisitType: React.FC = ({ onChange, patientUuid, visitTypes }) => { +const BaseVisitType: React.FC = ({ onChange, visitTypes }) => { const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const [searchTerm, setSearchTerm] = useState(''); diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.test.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.test.tsx similarity index 100% rename from packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.component.test.tsx rename to packages/esm-patient-chart-app/src/visit/visit-form/base-visit-type.test.tsx diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx index fda296aedb..f9f341389e 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/recommended-visit-type.component.tsx @@ -1,65 +1,33 @@ -import React, { useEffect, useState } from 'react'; -import { Tag } from 'carbon-components-react'; +import React from 'react'; import { useRecommendedVisitTypes } from '../hooks/useRecommendedVisitTypes'; import BaseVisitType from './base-visit-type.component'; -import { EmptyState, launchPatientWorkspace, PatientProgram } from '@openmrs/esm-patient-common-lib'; -import { useTranslation } from 'react-i18next'; +import { PatientProgram } from '@openmrs/esm-patient-common-lib'; interface RecommendedVisitTypeProp { patientUuid: string; - activePatientEnrollment: Array; + patientProgramEnrollment: PatientProgram; onChange: (visitTypeUuid) => void; locationUuid: string; - onProgramUuidChange: (uuid) => void; } const RecommendedVisitType: React.FC = ({ patientUuid, - activePatientEnrollment, + patientProgramEnrollment, onChange, locationUuid, - onProgramUuidChange, }) => { - const { t } = useTranslation(); - const [selectedProgram, setSelectedProgram] = useState(activePatientEnrollment[0]); const { recommendedVisitTypes, error } = useRecommendedVisitTypes( patientUuid, - selectedProgram?.uuid, - selectedProgram?.program?.uuid, + patientProgramEnrollment?.uuid, + patientProgramEnrollment?.program?.uuid, locationUuid, ); - useEffect(() => { - onProgramUuidChange(selectedProgram.program.uuid); - }, []); - return (
- {activePatientEnrollment.length === 0 && ( - launchPatientWorkspace('programs-form-workspace')} - /> - )} - {activePatientEnrollment.map((enrollment) => ( - { - onProgramUuidChange(enrollment.program.uuid); - setSelectedProgram(enrollment); - e.preventDefault(); - }} - type={selectedProgram?.uuid === enrollment.uuid ? 'blue' : 'cool-gray'} - key={enrollment.uuid} - > - {enrollment.program['name']} - - ))} - {recommendedVisitTypes?.length > 0 && ( - - )} +
); }; -export default RecommendedVisitType; +export const MemoizedRecommendedVisitType = React.memo(RecommendedVisitType); diff --git a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx index 6f93c77be0..e6ab894a19 100644 --- a/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx +++ b/packages/esm-patient-chart-app/src/visit/visit-form/visit-form.component.tsx @@ -3,13 +3,14 @@ import dayjs from 'dayjs'; import { Button, ButtonSet, - Column, ContentSwitcher, DatePicker, DatePickerInput, Form, - Grid, + FormGroup, InlineNotification, + RadioButton, + RadioButtonGroup, Row, Select, SelectItem, @@ -41,10 +42,11 @@ import { convertTime12to24, DefaultWorkspaceProps, useActivePatientEnrollment, + PatientProgram, } from '@openmrs/esm-patient-common-lib'; import BaseVisitType from './base-visit-type.component'; -import styles from './visit-form.component.scss'; -import RecommendedVisitType from './recommended-visit-type.component'; +import styles from './visit-form.scss'; +import { MemoizedRecommendedVisitType } from './recommended-visit-type.component'; const StartVisitForm: React.FC = ({ patientUuid, closeWorkspace }) => { const { t } = useTranslation(); @@ -60,10 +62,10 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor const [visitDate, setVisitDate] = useState(new Date()); const [visitTime, setVisitTime] = useState(dayjs(new Date()).format('hh:mm')); const [visitType, setVisitType] = useState(null); - const [programUuid, setProgramUuid] = useState(); const state = useMemo(() => ({ patientUuid }), [patientUuid]); const allVisitTypes = useVisitTypes(); const { activePatientEnrollment, isLoading } = useActivePatientEnrollment(patientUuid); + const [enrollment, setEnrollment] = useState(activePatientEnrollment[0]); useEffect(() => { if (locations && sessionUser?.sessionLocation?.uuid) { @@ -130,18 +132,16 @@ const StartVisitForm: React.FC = ({ patientUuid, closeWor return ( - +
{isTablet && ( )}
- - - {t('dateAndTimeOfVisit', 'Date and time of visit')} - - +
+
{t('dateAndTimeOfVisit', 'Date and time of visit')}
+
= ({ patientUuid, closeWor - - - - - {t('visitLocation', 'Visit Location')} - - - - - - - - {t('visitType', 'Visit Type')} - - - setContentSwitcherIndex(index)} +
+
+ +
+
{t('visitLocation', 'Visit Location')}
+ +
+ +
+
{t('program', 'Program')}
+ + setEnrollment(activePatientEnrollment.find(({ program }) => program.uuid === uuid))} + name="program-type-radio-group" + valueSelected="default-selected" > - - - - {contentSwitcherIndex === 0 && !isLoading && ( - { - setVisitType(visitType); - setIsMissingVisitType(false); - }} - patientUuid={patientUuid} - activePatientEnrollment={activePatientEnrollment} - locationUuid={selectedLocation} - onProgramUuidChange={setProgramUuid} - /> - )} - {contentSwitcherIndex === 1 && ( - { - setVisitType(visitType); - setIsMissingVisitType(false); - }} - visitTypes={allVisitTypes} - patientUuid={patientUuid} - /> - )} - - + {activePatientEnrollment.map(({ uuid, display, program }) => ( + + ))} + + +
+
+
{t('visitType', 'Visit Type')}
+ setContentSwitcherIndex(index)} + > + + + + {contentSwitcherIndex === 0 && !isLoading && ( + { + setVisitType(visitType); + setIsMissingVisitType(false); + }} + patientUuid={patientUuid} + patientProgramEnrollment={enrollment} + locationUuid={selectedLocation} + /> + )} + {contentSwitcherIndex === 1 && ( + { + setVisitType(visitType); + setIsMissingVisitType(false); + }} + visitTypes={allVisitTypes} + patientUuid={patientUuid} + /> + )} +
{isMissingVisitType && ( - - - - - +
+ +
)}
- +