From f33d0d71965c04a52ba7188806a0622927b785ad Mon Sep 17 00:00:00 2001 From: Rugute Date: Wed, 24 Jul 2024 18:42:33 +0300 Subject: [PATCH 1/2] Updated the Care Panel app --- .DS_Store | Bin 6148 -> 6148 bytes packages/esm-care-panel-app/README.md | 4 +- packages/esm-care-panel-app/package.json | 6 +- .../care-panel-dashboard.component.tsx | 10 +- .../esm-care-panel-app/src/config-schema.ts | 6 + .../src/hooks/usePatientHIVStatus.ts | 18 +++ .../src/hooks/usePatientIITScore.ts | 19 +++ .../iit-risk-score/iit-risk-score-plot.tsx | 68 ++++++++++ .../iit-risk-score.component.tsx | 73 +++++++++++ .../src/iit-risk-score/iit-risk-score.scss | 39 ++++++ .../src/iit-risk-score/risk-score.mock.ts | 56 ++++++++ .../machine-learning.component.tsx | 30 +++++ .../machine-learning/machine-learning.scss | 37 ++++++ .../patient-summary.component.test.tsx | 120 ++++++++++++++++++ .../program-enrollment.component.tsx | 20 ++- .../program-summary/program-summary.test.tsx | 101 +++++++++++++++ .../non-standard-regimen.component.tsx | 7 +- .../standard-regimen.component.tsx | 7 +- .../src/regimen-editor/utils.tsx | 33 +++++ .../regimen-history.component.test.tsx | 43 +++++++ .../esm-care-panel-app/src/types/index.ts | 26 ++++ .../esm-care-panel-app/translations/en.json | 1 + .../package.json | 6 +- .../src/amrs-link/amrs-link.component.tsx | 2 +- 24 files changed, 711 insertions(+), 21 deletions(-) create mode 100755 packages/esm-care-panel-app/src/hooks/usePatientHIVStatus.ts create mode 100755 packages/esm-care-panel-app/src/hooks/usePatientIITScore.ts create mode 100755 packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score-plot.tsx create mode 100755 packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.component.tsx create mode 100755 packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.scss create mode 100755 packages/esm-care-panel-app/src/iit-risk-score/risk-score.mock.ts create mode 100755 packages/esm-care-panel-app/src/machine-learning/machine-learning.component.tsx create mode 100755 packages/esm-care-panel-app/src/machine-learning/machine-learning.scss create mode 100755 packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx create mode 100755 packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx create mode 100755 packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx diff --git a/.DS_Store b/.DS_Store index 2fd35c1b30583e982379cac75123db02affc3a7e..eea5c580ef02f3d32951ca4cc22db31743152a39 100755 GIT binary patch delta 60 zcmZoMXfc@J&&aniU^g=(-(((^$?|y&`3xxxsSNQ9xnNc)Lk^HvoKl>ela!yIvw0?q LC-Y`@j=%f>_S6wH delta 31 ncmZoMXfc@J&&azmU^g=(?_?g9$(xU`I5SUdP~FVV@s}R}pkxXj diff --git a/packages/esm-care-panel-app/README.md b/packages/esm-care-panel-app/README.md index a2a214c0..5a8a71ad 100755 --- a/packages/esm-care-panel-app/README.md +++ b/packages/esm-care-panel-app/README.md @@ -1,5 +1,5 @@ -![Node.js CI](https://github.com/palladiumkenya/ampath-esm-3.x/workflows/Node.js%20CI/badge.svg) +![Node.js CI](https://github.com/AMPATH/ampath-esm-3.x/workflows/Node.js%20CI/badge.svg) # ESM Care Panel App -This repository provides a starting point for creating ampath patient care panels +This repository provides a starting point for creating AMPATH patient care panels diff --git a/packages/esm-care-panel-app/package.json b/packages/esm-care-panel-app/package.json index 430f089d..b3916011 100755 --- a/packages/esm-care-panel-app/package.json +++ b/packages/esm-care-panel-app/package.json @@ -6,7 +6,7 @@ "main": "src/index.ts", "source": true, "license": "MPL-2.0", - "homepage": "https://github.com/palladiumkenya/ampath-esm-core#readme", + "homepage": "https://github.com/AMPATH/ampath-esm-core#readme", "scripts": { "start": "openmrs develop", "serve": "webpack serve --mode=development", @@ -31,10 +31,10 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/palladiumkenya/ampath-esm-core#readme" + "url": "git+https://github.com/AMPATH/ampath-esm-core#readme" }, "bugs": { - "url": "https://github.com/palladiumkenya/ampath-esm-core/issues" + "url": "https://github.com/AMPATH/ampath-esm-core/issues" }, "dependencies": { "@carbon/react": "^1.42.1", diff --git a/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.component.tsx b/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.component.tsx index 044f8130..ec389f83 100755 --- a/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.component.tsx +++ b/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.component.tsx @@ -1,11 +1,11 @@ +import { Layer, Tab, TabList, TabPanel, TabPanels, Tabs, Tile } from '@carbon/react'; +import { Analytics, CloudMonitoring, Dashboard } from '@carbon/react/icons'; import React from 'react'; -import { Tabs, TabList, Tab, TabPanel, TabPanels, Layer, Tile } from '@carbon/react'; -import { Dashboard, CloudMonitoring, Printer } from '@carbon/react/icons'; import { useTranslation } from 'react-i18next'; import CarePanel from '../care-panel/care-panel.component'; import CarePrograms from '../care-programs/care-programs.component'; -import PatientSummary from '../patient-summary/patient-summary.component'; +import CarePanelMachineLearning from '../machine-learning/machine-learning.component'; import styles from './care-panel-dashboard.scss'; type CarePanelDashboardProps = { patientUuid: string; formEntrySub: any; launchPatientWorkspace: Function }; @@ -28,6 +28,7 @@ const CarePanelDashboard: React.FC = ({ {t('panelSummary', 'Panel summary')} {t('enrollments', 'Program enrollment')} + {t('machineLearning', 'Machine Learning')} @@ -40,6 +41,9 @@ const CarePanelDashboard: React.FC = ({ + + + diff --git a/packages/esm-care-panel-app/src/config-schema.ts b/packages/esm-care-panel-app/src/config-schema.ts index 093ca4f5..fd58c51f 100755 --- a/packages/esm-care-panel-app/src/config-schema.ts +++ b/packages/esm-care-panel-app/src/config-schema.ts @@ -4,6 +4,7 @@ export interface CarePanelConfig { regimenObs: { encounterProviderRoleUuid: string; }; + hivProgramUuid: string; } export const configSchema = { @@ -14,4 +15,9 @@ export const configSchema = { _description: "The provider role to use for the regimen encounter. Default is 'Unkown'.", }, }, + hivProgramUuid: { + _type: Type.String, + _description: 'HIV Program UUID', + _default: 'dfdc6d40-2f2f-463d-ba90-cc97350441a8', + }, }; diff --git a/packages/esm-care-panel-app/src/hooks/usePatientHIVStatus.ts b/packages/esm-care-panel-app/src/hooks/usePatientHIVStatus.ts new file mode 100755 index 00000000..b0df0f65 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/usePatientHIVStatus.ts @@ -0,0 +1,18 @@ +import { openmrsFetch, useConfig } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { type CarePanelConfig } from '../config-schema'; +import { type Enrollment } from '../types'; + +const usePatientHIVStatus = (patientUuid: string) => { + const customeRepresentation = 'custom:(uuid,program:(name,uuid))'; + const url = `/ws/rest/v1/programenrollment?v=${customeRepresentation}&patient=${patientUuid}`; + const config = useConfig(); + const { data, error, isLoading } = useSWR<{ data: { results: Enrollment[] } }>(url, openmrsFetch); + return { + error, + isLoading, + isPositive: (data?.data?.results ?? []).find((en) => en.program.uuid === config.hivProgramUuid) !== undefined, + }; +}; + +export default usePatientHIVStatus; diff --git a/packages/esm-care-panel-app/src/hooks/usePatientIITScore.ts b/packages/esm-care-panel-app/src/hooks/usePatientIITScore.ts new file mode 100755 index 00000000..39d555d5 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/usePatientIITScore.ts @@ -0,0 +1,19 @@ +import { type FetchResponse, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { type IITRiskScore } from '../types'; +import { patientRiskScore } from '../iit-risk-score/risk-score.mock'; +import { useMemo } from 'react'; + +const usePatientIITScore = (patientUuid: string) => { + const url = `${restBaseUrl}/keml/patientiitscore?patientUuid=${patientUuid}`; + const { data, error, isLoading } = useSWR>(url, openmrsFetch); + + const riskScore = useMemo(() => data?.data ?? patientRiskScore.at(-1), [patientUuid]); + return { + isLoading: false, + error, + riskScore, + }; +}; + +export default usePatientIITScore; diff --git a/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score-plot.tsx b/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score-plot.tsx new file mode 100755 index 00000000..59f70352 --- /dev/null +++ b/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score-plot.tsx @@ -0,0 +1,68 @@ +import { LineChart, type LineChartOptions, ScaleTypes } from '@carbon/charts-react'; +import '@carbon/charts/styles.css'; +import React from 'react'; +import styles from './iit-risk-score.scss'; +import usePatientIITScore from '../hooks/usePatientIITScore'; +import { CardHeader } from '@openmrs/esm-patient-common-lib'; +import { patientRiskScore } from './risk-score.mock'; +import { formatDate, parseDate } from '@openmrs/esm-framework'; + +interface CarePanelRiskScorePlotProps { + patientUuid: string; +} + +const CarePanelRiskScorePlot: React.FC = ({ patientUuid }) => { + const { isLoading, error, riskScore } = usePatientIITScore(patientUuid); + const options: LineChartOptions = { + title: 'KenyaHMIS ML Model', + legend: { enabled: false }, + axes: { + bottom: { + title: 'Evaluation Time', + mapsTo: 'evaluationDate', + scaleType: ScaleTypes.LABELS, + }, + left: { + mapsTo: 'riskScore', + title: 'Risk Score (%)', + percentage: true, + scaleType: ScaleTypes.LINEAR, + includeZero: true, + }, + }, + curve: 'curveMonotoneX', + height: '400px', + tooltip: { + // Tooltip configuration for displaying descriptions + enabled: true, + + valueFormatter(value, label) { + if (label === 'Risk Score (%)') { + return `${value} (${patientRiskScore.find((r) => `${r.riskScore}` === `${value}`)?.description ?? ''})`; + } + return `${value}`; + }, + }, + }; + + return ( +
+ IIT Risk Score Trend +
+ Latest risk score: + {`${riskScore.riskScore}%`} +
+
+ ({ + ...risk, + evaluationDate: formatDate(parseDate(risk.evaluationDate)), + }))} + options={options} + /> +
+
+ ); +}; + +export default CarePanelRiskScorePlot; diff --git a/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.component.tsx b/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.component.tsx new file mode 100755 index 00000000..9751873c --- /dev/null +++ b/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.component.tsx @@ -0,0 +1,73 @@ +import { Column, Row, SkeletonText } from '@carbon/react'; +import { ErrorState, formatDate, parseDate } from '@openmrs/esm-framework'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import usePatientIITScore from '../hooks/usePatientIITScore'; +import styles from './iit-risk-score.scss'; + +interface CarePanellIITRiskScoreProps { + patientUuid: string; +} + +const CarePanellIITRiskScore: React.FC = ({ patientUuid }) => { + const { riskScore, error, isLoading } = usePatientIITScore(patientUuid); + const { t } = useTranslation(); + + if (isLoading) { + return ( +
+ + + + + + + + + + + + + + + + + + + + +
+ ); + } + + // if (error) { + // return ; + // } + return ( +
+ IIT Risk Score + + + Risk Score: +

{`${riskScore?.riskScore ?? 0}%`}

+
+ + Evaluation Date: +

{formatDate(parseDate(riskScore?.evaluationDate))}

+
+
+ + + Description: +

{riskScore?.description}

+
+ + Risk Factors: +

{riskScore?.riskFactors}

+
+
+
+ ); +}; + +export default CarePanellIITRiskScore; diff --git a/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.scss b/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.scss new file mode 100755 index 00000000..3eaaab6c --- /dev/null +++ b/packages/esm-care-panel-app/src/iit-risk-score/iit-risk-score.scss @@ -0,0 +1,39 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.risk-score-card { + width: 100%; + border: 0.15rem solid $grey-2; + margin-bottom: spacing.$spacing-08; +} + +.risk-score-card__item { + flex: 1; + padding: spacing.$spacing-05; +} + +.sectionHeader { + @include type.type-style('heading-02'); + display: flex; + align-items: center; + justify-content: space-between; + margin: spacing.$spacing-05; + row-gap: 1.5rem; + position: relative; + + &::after { + content: ''; + display: block; + width: 2rem; + border-bottom: 0.375rem solid var(--brand-03); + position: absolute; + bottom: -0.75rem; + left: 0; + } + + & > span { + @include type.type-style('body-01'); + } +} diff --git a/packages/esm-care-panel-app/src/iit-risk-score/risk-score.mock.ts b/packages/esm-care-panel-app/src/iit-risk-score/risk-score.mock.ts new file mode 100755 index 00000000..87afd503 --- /dev/null +++ b/packages/esm-care-panel-app/src/iit-risk-score/risk-score.mock.ts @@ -0,0 +1,56 @@ +export const patientRiskScore = [ + { + evaluationDate: '2023-01-12T00:00:00.000Z', + riskScore: 0, + description: 'Low Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2023-04-10T00:00:00.000Z', + riskScore: 0, + description: 'Low Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2023-07-06T00:00:00.000Z', + riskScore: 3, + description: 'Medium Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2023-10-13T00:00:00.000Z', + riskScore: 3, + description: 'Medium Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2023-11-23T00:00:00.000Z', + riskScore: 3, + description: 'Medium Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2023-12-17T00:00:00.000Z', + riskScore: 3, + description: 'Medium Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2024-01-17T00:00:00.000Z', + riskScore: 3, + description: 'Medium Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2024-04-09T00:00:00.000Z', + riskScore: 11, + description: 'High Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, + { + evaluationDate: '2024-07-07T00:00:00.000Z', + riskScore: 11, + description: 'High Risk', + riskFactors: 'Poor adherance, Missed appointments, Late drug pickup', + }, +]; diff --git a/packages/esm-care-panel-app/src/machine-learning/machine-learning.component.tsx b/packages/esm-care-panel-app/src/machine-learning/machine-learning.component.tsx new file mode 100755 index 00000000..26f3cef7 --- /dev/null +++ b/packages/esm-care-panel-app/src/machine-learning/machine-learning.component.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import styles from './machine-learning.scss'; +import CarePanellIITRiskScore from '../iit-risk-score/iit-risk-score.component'; +import CarePanelRiskScorePlot from '../iit-risk-score/iit-risk-score-plot'; +import usePatientHIVStatus from '../hooks/usePatientHIVStatus'; +import { ErrorState } from '@openmrs/esm-framework'; +import { useTranslation } from 'react-i18next'; +import { EmptyState } from '@openmrs/esm-patient-common-lib'; + +interface CarePanelMachineLearningProps { + patientUuid: string; +} + +const CarePanelMachineLearning: React.FC = ({ patientUuid }) => { + const { error, isLoading, isPositive } = usePatientHIVStatus(patientUuid); + const { t } = useTranslation(); + const header = t('machineLearning', 'Machine Learning'); + if (error) { + return ; + } + return ( +
+ {!isPositive && } + {isPositive && } + {isPositive && } +
+ ); +}; + +export default CarePanelMachineLearning; diff --git a/packages/esm-care-panel-app/src/machine-learning/machine-learning.scss b/packages/esm-care-panel-app/src/machine-learning/machine-learning.scss new file mode 100755 index 00000000..d2337ef4 --- /dev/null +++ b/packages/esm-care-panel-app/src/machine-learning/machine-learning.scss @@ -0,0 +1,37 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@use '@carbon/layout'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.machine-learning { + padding: 8px; + gap: spacing.$spacing-08; +} + +.sectionHeader { + @include type.type-style('heading-02'); +} + +.title { + @include type.type-style('heading-02'); + display: flex; + align-items: center; + justify-content: space-between; + margin: spacing.$spacing-05; + row-gap: 1.5rem; + position: relative; + + &::after { + content: ''; + display: block; + width: 2rem; + border-bottom: 0.375rem solid var(--brand-03); + position: absolute; + bottom: -0.75rem; + left: 0; + } + + & > span { + @include type.type-style('body-01'); + } +} diff --git a/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx b/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx new file mode 100755 index 00000000..1a25fc4b --- /dev/null +++ b/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx @@ -0,0 +1,120 @@ +import React from 'react'; +import dayjs from 'dayjs'; +import userEvent from '@testing-library/user-event'; +import { render, screen } from '@testing-library/react'; +import { useReactToPrint } from 'react-to-print'; +import { useConfig } from '@openmrs/esm-framework'; +import { usePatientSummary } from '../hooks/usePatientSummary'; +import { mockPatient } from '../../../../__mocks__/patient-summary.mock'; +import PatientSummary from './patient-summary.component'; + +jest.mock('../hooks/usePatientSummary'); + +const mockedUseConfig = useConfig as jest.Mock; +const mockedUsePatientSummary = usePatientSummary as jest.Mock; +const mockedUseReactToPrint = jest.mocked(useReactToPrint); + +jest.mock('@openmrs/esm-framework', () => { + return { + ...jest.requireActual('@openmrs/esm-framework'), + formatDate: jest.fn().mockImplementation((mockDate) => `${dayjs(mockDate).format('DD-MMM-YYYY')}`), + }; +}); + +jest.mock('react-to-print', () => { + const originalModule = jest.requireActual('react-to-print'); + + return { + ...originalModule, + useReactToPrint: jest.fn(), + }; +}); + +describe('PatientSummary', () => { + beforeEach(() => { + mockedUsePatientSummary.mockReturnValue({ + data: null, + error: false, + isLoading: true, + }); + }); + + afterEach(() => jest.clearAllMocks()); + + it('renders a skeleton loader when loading', () => { + render(); + const skeletonLoader = screen.getByRole('progressbar'); + expect(skeletonLoader).toBeInTheDocument(); + }); + + it('renders an error message when data retrieval fails', () => { + const mockError = { + message: 'You are not logged in', + response: { + status: 401, + statusText: 'Unauthorized', + }, + }; + + mockedUsePatientSummary.mockReturnValue({ + data: null, + error: mockError, + isLoading: false, + }); + + render(); + const errorMessage = screen.getByText('Error loading patient summary'); + expect(errorMessage).toBeInTheDocument(); + }); + + it('renders patient summary data when loaded', () => { + mockedUsePatientSummary.mockReturnValue({ + data: mockPatient, + error: null, + isLoading: false, + }); + + render(); + expect(screen.getByText(mockPatient.patientName)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.birthDate)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.reportDate)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.age)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.maritalStatus)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.mflCode)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.clinicName)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.nationalUniquePatientIdentifier)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.weight)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.height)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.bmi)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.bloodPressure)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.oxygenSaturation)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.pulseRate)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.familyProtection)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.respiratoryRate)).toBeInTheDocument(); + expect(screen.getByText(mockPatient.tbScreeningOutcome)).toBeInTheDocument(); + // TODO: Extend the test to check all the values + }); + + it('triggers print when print button is clicked', async () => { + const user = userEvent.setup(); + + mockedUsePatientSummary.mockReturnValue({ + data: mockPatient, + error: null, + isLoading: false, + }); + + mockedUseConfig.mockReturnValue({ logo: {} }); + + render(); + + const printButton = screen.getByRole('button', { name: /print/i }); + expect(printButton).toBeInTheDocument(); + + await screen.findByText(/patient summary/i); + await user.click(printButton); + + // FIXME: Why does this happen twice? + expect(mockedUseReactToPrint).toHaveBeenCalled(); + }); +}); diff --git a/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.component.tsx b/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.component.tsx index 2b47bb36..99a86101 100755 --- a/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.component.tsx +++ b/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.component.tsx @@ -16,10 +16,10 @@ import { import styles from './program-enrollment.scss'; import isEmpty from 'lodash/isEmpty'; import dayjs from 'dayjs'; -import { formatDate } from '@openmrs/esm-framework'; +import { formatDate, useVisit } from '@openmrs/esm-framework'; import orderBy from 'lodash/orderBy'; import { mutate } from 'swr'; -import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; +import { getPatientUuidFromUrl, launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; export interface ProgramEnrollmentProps { patientUuid: string; @@ -33,8 +33,6 @@ const programDetailsMap = { dateEnrolled: 'Enrolled on', whoStage: 'WHO Stage', entryPoint: 'Entry Point', - regimenShortDisplay: 'Regimen', - changeReasons: 'Reason for regimen change', reason: 'Reason for discontinuation', }, TB: { @@ -66,6 +64,7 @@ const programDetailsMap = { const ProgramEnrollment: React.FC = ({ enrollments = [], programName }) => { const { t } = useTranslation(); + const { currentVisit } = useVisit(getPatientUuidFromUrl()); const orderedEnrollments = orderBy(enrollments, 'dateEnrolled', 'desc'); const headers = useMemo( () => @@ -101,6 +100,8 @@ const ProgramEnrollment: React.FC = ({ enrollments = [], }, formInfo: { encounterUuid: '', + visitTypeUuid: currentVisit?.visitType?.uuid ?? '', + visitUuid: currentVisit?.uuid ?? '', formUuid: enrollment?.discontinuationFormUuid, additionalProps: { enrollmentDetails: { dateEnrolled: new Date(enrollment.dateEnrolled), uuid: enrollment.enrollmentUuid } } ?? @@ -113,13 +114,18 @@ const ProgramEnrollment: React.FC = ({ enrollments = [], launchPatientWorkspace('patient-form-entry-workspace', { workspaceTitle: enrollment?.enrollmentFormName, mutateForm: () => { - mutate((key) => true, undefined, { - revalidate: true, - }); + mutate( + (key) => + typeof key === 'string' && key.startsWith('/ws/rest/v1/amrs/patientHistoricalEnrollment?patientUuid='), + undefined, + { revalidate: true }, + ); }, formInfo: { encounterUuid: enrollment?.enrollmentEncounterUuid, formUuid: enrollment?.enrollmentFormUuid, + visitTypeUuid: currentVisit?.visitType?.uuid ?? '', + visitUuid: currentVisit?.uuid ?? '', additionalProps: { enrollmentDetails: { dateEnrolled: new Date(enrollment.dateEnrolled), uuid: enrollment.enrollmentUuid } } ?? {}, diff --git a/packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx b/packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx new file mode 100755 index 00000000..40ed5605 --- /dev/null +++ b/packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx @@ -0,0 +1,101 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { mockProgram } from '../../../../__mocks__/program-summary.mock'; +import { formatDate } from '@openmrs/esm-framework'; +import ProgramSummary, { type ProgramSummaryProps } from './program-summary.component'; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), +})); + +jest.mock('../hooks/useProgramSummary', () => ({ + useProgramSummary: jest.fn(() => ({ + data: mockProgram, + isError: false, + isLoading: false, + })), +})); + +const mockFormatDate = (date) => formatDate(new Date(date)); + +describe('ProgramSummary Component', () => { + const mockProps: ProgramSummaryProps = { + patientUuid: 'patient-123', + programName: 'HIV', + }; + + it('displays HIV program details correctly', async () => { + render(); + + expect(screen.getByText('Current status')).toBeInTheDocument(); + expect(screen.getByText('Last viral load')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.HIV.ldlValue)).toBeInTheDocument(); + expect(screen.getByText(`(${mockFormatDate(mockProgram.HIV.ldlDate)})`)).toBeInTheDocument(); + expect(screen.getByText('Last CD4 count')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.HIV.cd4)).toBeInTheDocument(); + expect(screen.getByText(`(${mockFormatDate(mockProgram.HIV.cd4Date)})`)).toBeInTheDocument(); + expect(screen.getByText('CD4 percentage')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.HIV.cd4Percent)).toBeInTheDocument(); + expect(screen.getByText(`(${mockFormatDate(mockProgram.HIV.cd4PercentDate)})`)).toBeInTheDocument(); + expect(screen.getByText('Last WHO stage')).toBeInTheDocument(); + expect(screen.getByText('Regimen')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.HIV.lastEncDetails.regimenShortDisplay)).toBeInTheDocument(); + expect(screen.getByText('Date started regimen')).toBeInTheDocument(); + expect(screen.getByText(`01-Aug-2023`)).toBeInTheDocument(); + }); + + xit('displays TB program details correctly', async () => { + const tbProps: ProgramSummaryProps = { + ...mockProps, + programName: 'TB', + }; + + render(); + + expect(screen.getByText('Treatment number')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.TB.tbTreatmentNumber)).toBeInTheDocument(); + expect(screen.getByText('Disease classification')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.TB.tbDiseaseClassification)).toBeInTheDocument(); + expect(screen.getByText('Patient classification')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.TB.tbPatientClassification)).toBeInTheDocument(); + }); + + it('displays MCHMOTHER program details correctly', async () => { + const mchMotherProps: ProgramSummaryProps = { + ...mockProps, + programName: 'mchMother', + }; + + render(); + + expect(screen.getByText('HIV status')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.mchMother.hivStatus)).toBeInTheDocument(); + expect(screen.getByText(`(${mockFormatDate(mockProgram.mchMother.hivStatusDate)})`)).toBeInTheDocument(); + expect(screen.getByText('On ART')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.mchMother.onHaart)).toBeInTheDocument(); + expect(screen.getByText(`(14-Aug-2023)`)).toBeInTheDocument(); + }); + + it('displays MCHCHILD program details correctly', async () => { + const mchChildProps: ProgramSummaryProps = { + ...mockProps, + programName: 'mchChild', + }; + + render(); + + expect(screen.getByText('HIV Status')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.mchChild.hivStatus)).toBeInTheDocument(); + expect(screen.getByText('HEI Outcome')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.mchChild.heiOutcome)).toBeInTheDocument(); + expect(screen.getByText('Milestones Attained')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.mchChild.milestonesAttained)).toBeInTheDocument(); + expect(screen.getByText(mockFormatDate(mockProgram.mchChild.milestonesAttainedDate))).toBeInTheDocument(); + expect(screen.getByText('Current feeding option')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.mchChild.currentFeedingOption)).toBeInTheDocument(); + expect(screen.getByText(mockFormatDate(mockProgram.mchChild.currentFeedingOptionDate))).toBeInTheDocument(); + expect(screen.getByText('Current prophylaxis used')).toBeInTheDocument(); + expect(screen.getByText(mockProgram.mchChild.currentProphylaxisUsed)).toBeInTheDocument(); + expect(screen.getByText(mockFormatDate(mockProgram.mchChild.currentProphylaxisUsedDate))).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-care-panel-app/src/regimen-editor/non-standard-regimen.component.tsx b/packages/esm-care-panel-app/src/regimen-editor/non-standard-regimen.component.tsx index 64ad3812..8ad4b1fc 100755 --- a/packages/esm-care-panel-app/src/regimen-editor/non-standard-regimen.component.tsx +++ b/packages/esm-care-panel-app/src/regimen-editor/non-standard-regimen.component.tsx @@ -5,6 +5,8 @@ import { useStandardRegimen } from '../hooks/useStandardRegimen'; import styles from './standard-regimen.scss'; import { useNonStandardRegimen } from '../hooks/useNonStandardRegimen'; import { Regimen } from '../types'; +import { usePatient } from '@openmrs/esm-framework'; +import { filterRegimenData, calculateAge } from './utils'; interface NonStandardRegimenProps { category: string; @@ -26,6 +28,9 @@ const NonStandardRegimen: React.FC = ({ const matchingCategory = standardRegimen.find((item) => item.categoryCode === 'ARV'); // Non standard regimen will be exclusively for ARVs const [selectedRegimens, setSelectedRegimens] = useState(Array(5).fill('')); const [nonStandardRegimenObjects, setStandardRegimenObjects] = useState([]); + const { patient } = usePatient(); + const patientAge = calculateAge(patient?.birthDate); + const filteredRegimenLineByAge = filterRegimenData(matchingCategory?.category, patientAge); const handleRegimenLineChange = (e) => { setSelectedRegimenLine(e.target.value); @@ -77,7 +82,7 @@ const NonStandardRegimen: React.FC = ({ {!selectedRegimenLine || selectedRegimenLine == '--' ? ( ) : null} - {matchingCategory?.category.map((line) => ( + {filteredRegimenLineByAge.map((line) => ( {line.regimenline} diff --git a/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.component.tsx b/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.component.tsx index 79587b7c..15148bfa 100755 --- a/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.component.tsx +++ b/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.component.tsx @@ -3,6 +3,8 @@ import { useTranslation } from 'react-i18next'; import { Select, SelectItem } from '@carbon/react'; import { useStandardRegimen } from '../hooks/useStandardRegimen'; import styles from './standard-regimen.scss'; +import { usePatient } from '@openmrs/esm-framework'; +import { filterRegimenData, calculateAge } from './utils'; interface StandardRegimenProps { category: string; @@ -24,6 +26,9 @@ const StandardRegimen: React.FC = ({ const [selectedRegimen, setSelectedRegimen] = useState(''); const [selectedRegimens, setSelectedRegimens] = useState([]); const matchingCategory = standardRegimen.find((item) => item.categoryCode === category); + const { patient } = usePatient(); + const patientAge = calculateAge(patient?.birthDate); + const filteredRegimenLineByAge = filterRegimenData(matchingCategory?.category, patientAge); useEffect(() => { const matchingRegimenLine = matchingCategory?.category.find( @@ -61,7 +66,7 @@ const StandardRegimen: React.FC = ({ {!selectedRegimenLine || selectedRegimenLine == '--' ? ( ) : null} - {matchingCategory?.category.map((line) => ( + {filteredRegimenLineByAge.map((line) => ( {line.regimenline} diff --git a/packages/esm-care-panel-app/src/regimen-editor/utils.tsx b/packages/esm-care-panel-app/src/regimen-editor/utils.tsx index c2cec6a2..d2b52a1c 100755 --- a/packages/esm-care-panel-app/src/regimen-editor/utils.tsx +++ b/packages/esm-care-panel-app/src/regimen-editor/utils.tsx @@ -1,3 +1,5 @@ +import { type RegimenLineGroup } from '../types'; + export function addOrUpdateObsObject(objectToAdd, obsArray, setObjectArray) { if (doesObjectExistInArray(obsArray, objectToAdd)) { setObjectArray((prevObsArray) => @@ -10,3 +12,34 @@ export function addOrUpdateObsObject(objectToAdd, obsArray, setObjectArray) { const doesObjectExistInArray = (obsArray, objectToCheck) => obsArray.some((obs) => obs.concept === objectToCheck.concept); + +export function filterRegimenData(regimenData: RegimenLineGroup[] | undefined, patientAge: number): RegimenLineGroup[] { + if (!regimenData) { + return []; + } + + const filterCriterion = patientAge > 14 ? 'Adult' : 'Child'; + return regimenData.filter((group) => group.regimenline.startsWith(filterCriterion)); +} + +export function calculateAge(birthDateString: string | null | undefined): number { + if (!birthDateString) { + return 0; + } + + const today = new Date(); + const birthDate = new Date(birthDateString); + + if (isNaN(birthDate.getTime())) { + return 0; + } + + let age = today.getFullYear() - birthDate.getFullYear(); + const m = today.getMonth() - birthDate.getMonth(); + + if (m < 0 || (m === 0 && today.getDate() < birthDate.getDate())) { + age--; + } + + return age; +} diff --git a/packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx b/packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx new file mode 100755 index 00000000..933c2af7 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx @@ -0,0 +1,43 @@ +import React from 'react'; +import { render, screen } from '@testing-library/react'; +import { useRegimenHistory } from '../hooks/useRegimenHistory'; +import RegimenHistory, { type RegimenHistoryProps } from './regimen-history.component'; + +jest.mock('../hooks/useRegimenHistory'); + +const mockedUseRegimenHistory = jest.mocked(useRegimenHistory); + +describe('RegimenHistory Component', () => { + const mockProps: RegimenHistoryProps = { + patientUuid: 'patient-123', + category: 'HIV Program', + }; + + const mockData = [ + { + regimenShortDisplay: 'ShortDisplay', + regimenLine: 'Line1', + changeReasons: ['Reason1'], + regimenUuid: 'regimen-123', + startDate: '2021-01-01', + endDate: '', + regimenLongDisplay: 'LongDisplay', + current: false, + }, + ]; + + beforeEach(() => { + mockedUseRegimenHistory.mockReturnValue({ + regimen: mockData, + isLoading: false, + error: false, + }); + }); + + it('displays regimen history details correctly', () => { + render(); + + expect(screen.getByText(mockData[0].regimenLine)).toBeInTheDocument(); + expect(screen.getByText(mockData[0].regimenShortDisplay)).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-care-panel-app/src/types/index.ts b/packages/esm-care-panel-app/src/types/index.ts index c49f7175..4ad693e0 100755 --- a/packages/esm-care-panel-app/src/types/index.ts +++ b/packages/esm-care-panel-app/src/types/index.ts @@ -219,3 +219,29 @@ export interface UpdateObs { value: string | number; }>; } + +export interface RegimenItem { + name: string; + conceptRef: string; +} + +export interface RegimenLineGroup { + regimenline: string; + regimenLineValue: string; + regimen: RegimenItem[]; +} + +export interface IITRiskScore { + riskScore: string; + evaluationDate: string; + description: string; + riskFactors: string; +} + +export interface Enrollment { + uuid: string; + program: { + name: string; + uuid: string; + }; +} diff --git a/packages/esm-care-panel-app/translations/en.json b/packages/esm-care-panel-app/translations/en.json index 8c523f96..b6dd7c0a 100755 --- a/packages/esm-care-panel-app/translations/en.json +++ b/packages/esm-care-panel-app/translations/en.json @@ -68,6 +68,7 @@ "lmp": "LMP", "loading": "Loading", "loadingDescription": "Loading data...", + "machineLearning": "Machine Learning", "maritalStatus": "Marital status", "mflCode": "MFL code", "milestonesAttained": "Milestones Attained", diff --git a/packages/esm-patient-clinical-view-app/package.json b/packages/esm-patient-clinical-view-app/package.json index fa21e7c0..f3f4bacc 100755 --- a/packages/esm-patient-clinical-view-app/package.json +++ b/packages/esm-patient-clinical-view-app/package.json @@ -6,7 +6,7 @@ "main": "src/index.ts", "source": true, "license": "MPL-2.0", - "homepage": "https://github.com/palladiumkenya/ampath-esm-core#readme", + "homepage": "https://github.com/AMPATH/ampath-esm-core#readme", "scripts": { "start": "openmrs develop", "serve": "webpack serve --mode=development", @@ -31,10 +31,10 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/palladiumkenya/ampath-esm-core#readme" + "url": "git+https://github.com/AMPATH/ampath-esm-core#readme" }, "bugs": { - "url": "https://github.com/palladiumkenya/ampath-esm-core/issues" + "url": "https://github.com/AMPATH/ampath-esm-core/issues" }, "dependencies": { "@carbon/charts-react": "^1.5.2", diff --git a/packages/esm-patient-flags-app/src/amrs-link/amrs-link.component.tsx b/packages/esm-patient-flags-app/src/amrs-link/amrs-link.component.tsx index a424ecf8..9389cfce 100755 --- a/packages/esm-patient-flags-app/src/amrs-link/amrs-link.component.tsx +++ b/packages/esm-patient-flags-app/src/amrs-link/amrs-link.component.tsx @@ -4,5 +4,5 @@ import { useTranslation } from 'react-i18next'; export default function AMRSLink() { const { t } = useTranslation(); - return {t('AMRSHome', 'AMRS POC Home')}; + return {t('AMRSHome', 'AMRS POC Home')}; } From 2b0407e078b0ebd5f768cf150abd75d5a85498f4 Mon Sep 17 00:00:00 2001 From: Rugute Date: Wed, 24 Jul 2024 19:09:43 +0300 Subject: [PATCH 2/2] patient summaries bugs --- .../patient-summary.component.test.tsx | 120 ------------------ .../patient-summary.component.tsx | 46 ------- 2 files changed, 166 deletions(-) delete mode 100755 packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx diff --git a/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx b/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx deleted file mode 100755 index 1a25fc4b..00000000 --- a/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import React from 'react'; -import dayjs from 'dayjs'; -import userEvent from '@testing-library/user-event'; -import { render, screen } from '@testing-library/react'; -import { useReactToPrint } from 'react-to-print'; -import { useConfig } from '@openmrs/esm-framework'; -import { usePatientSummary } from '../hooks/usePatientSummary'; -import { mockPatient } from '../../../../__mocks__/patient-summary.mock'; -import PatientSummary from './patient-summary.component'; - -jest.mock('../hooks/usePatientSummary'); - -const mockedUseConfig = useConfig as jest.Mock; -const mockedUsePatientSummary = usePatientSummary as jest.Mock; -const mockedUseReactToPrint = jest.mocked(useReactToPrint); - -jest.mock('@openmrs/esm-framework', () => { - return { - ...jest.requireActual('@openmrs/esm-framework'), - formatDate: jest.fn().mockImplementation((mockDate) => `${dayjs(mockDate).format('DD-MMM-YYYY')}`), - }; -}); - -jest.mock('react-to-print', () => { - const originalModule = jest.requireActual('react-to-print'); - - return { - ...originalModule, - useReactToPrint: jest.fn(), - }; -}); - -describe('PatientSummary', () => { - beforeEach(() => { - mockedUsePatientSummary.mockReturnValue({ - data: null, - error: false, - isLoading: true, - }); - }); - - afterEach(() => jest.clearAllMocks()); - - it('renders a skeleton loader when loading', () => { - render(); - const skeletonLoader = screen.getByRole('progressbar'); - expect(skeletonLoader).toBeInTheDocument(); - }); - - it('renders an error message when data retrieval fails', () => { - const mockError = { - message: 'You are not logged in', - response: { - status: 401, - statusText: 'Unauthorized', - }, - }; - - mockedUsePatientSummary.mockReturnValue({ - data: null, - error: mockError, - isLoading: false, - }); - - render(); - const errorMessage = screen.getByText('Error loading patient summary'); - expect(errorMessage).toBeInTheDocument(); - }); - - it('renders patient summary data when loaded', () => { - mockedUsePatientSummary.mockReturnValue({ - data: mockPatient, - error: null, - isLoading: false, - }); - - render(); - expect(screen.getByText(mockPatient.patientName)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.birthDate)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.reportDate)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.age)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.maritalStatus)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.mflCode)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.clinicName)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.nationalUniquePatientIdentifier)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.weight)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.height)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.bmi)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.bloodPressure)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.oxygenSaturation)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.pulseRate)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.familyProtection)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.respiratoryRate)).toBeInTheDocument(); - expect(screen.getByText(mockPatient.tbScreeningOutcome)).toBeInTheDocument(); - // TODO: Extend the test to check all the values - }); - - it('triggers print when print button is clicked', async () => { - const user = userEvent.setup(); - - mockedUsePatientSummary.mockReturnValue({ - data: mockPatient, - error: null, - isLoading: false, - }); - - mockedUseConfig.mockReturnValue({ logo: {} }); - - render(); - - const printButton = screen.getByRole('button', { name: /print/i }); - expect(printButton).toBeInTheDocument(); - - await screen.findByText(/patient summary/i); - await user.click(printButton); - - // FIXME: Why does this happen twice? - expect(mockedUseReactToPrint).toHaveBeenCalled(); - }); -}); diff --git a/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.tsx b/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.tsx index 43a50f71..7a1ca98f 100755 --- a/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.tsx +++ b/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.tsx @@ -477,11 +477,6 @@ const PatientSummary: React.FC = ({ patientUuid }) => {

{t('currentArtRegimenDate', 'Current Art regimen date')}

-

- - {data?.currentArtRegimen ? formatDate(new Date(data?.currentArtRegimen?.startDate)) : '--'} - -

@@ -604,47 +599,6 @@ const PatientSummary: React.FC = ({ patientUuid }) => { -
-
-

{t('viralLoadTrends', 'Viral load trends')}

- {data?.allVlResults?.value?.length > 0 - ? data?.allVlResults?.value?.map((vl, index) => { - return ( -
- {vl.vl} - {vl?.vlDate === 'N/A' || vl?.vlDate === '' ? None : {vl.vlDate}} -
-
- ); - }) - : '--'} -
-
-

{t('cd4Trends', 'CD4 Trends')}

- {data?.allCd4CountResults?.length > 0 - ? data?.allCd4CountResults?.map((cd4, index) => { - let formattedDate: Date; - - if (dayjs(cd4.cd4CountDate, 'DD/MM/YYYY', true).isValid()) { - const parts = cd4.cd4CountDate?.split('/'); - const day = parseInt(parts[0], 10); - const month = parseInt(parts[1], 10) - 1; // Subtract 1 since months are zero-based - const year = parseInt(parts[2], 10); - formattedDate = new Date(year, month, day); - - return ( -
- {cd4.cd4Count} - {cd4.cd4CountDate ? formatDate(formattedDate) : '--'} -
-
- ); - } - }) - : '--'} -
-
-