From 929d91526c550fe0df74b2c6015061c7391b8f00 Mon Sep 17 00:00:00 2001 From: Rugute Date: Tue, 16 Jul 2024 15:22:23 +0300 Subject: [PATCH 1/3] added HIE CR patient verification --- package.json | 8 +- packages/esm-billing-app/src/routes.json | 2 +- packages/esm-care-panel-app/README.md | 5 + .../currentProphylaxisUsed/en.json | 1 + packages/esm-care-panel-app/hivStatus/en.json | 1 + packages/esm-care-panel-app/jest.config.js | 3 + packages/esm-care-panel-app/package.json | 54 ++ .../care-panel-dashboard.component.tsx | 50 ++ .../care-panel-dashboard.scss | 32 + .../src/care-panel/care-panel.component.tsx | 82 +++ .../src/care-panel/care-panel.scss | 45 ++ .../care-programs/care-programs.component.tsx | 170 +++++ .../src/care-programs/care-programs.scss | 14 + .../src/care-programs/care-programs.test.tsx | 150 ++++ .../esm-care-panel-app/src/config-schema.ts | 17 + .../esm-care-panel-app/src/dashboard.meta.ts | 13 + .../esm-care-panel-app/src/declarations.d.ts | 4 + .../src/hooks/useCarePrograms.tsx | 23 + .../src/hooks/useEnrollmentHistory.ts | 18 + .../src/hooks/useNonStandardRegimen.ts | 18 + .../src/hooks/usePatientSummary.ts | 14 + .../src/hooks/useProgramSummary.ts | 14 + .../src/hooks/useRegimenEncounter.ts | 11 + .../src/hooks/useRegimenHistory.ts | 30 + .../src/hooks/useRegimenReason.ts | 22 + .../src/hooks/useStandardRegimen.ts | 29 + packages/esm-care-panel-app/src/index.ts | 47 ++ .../patient-summary.component.test.tsx | 120 +++ .../patient-summary.component.tsx | 684 ++++++++++++++++++ .../src/patient-summary/patient-summary.scss | 84 +++ .../src/print-layout/print.component.tsx | 16 + .../src/print-layout/print.scss | 9 + .../program-enrollment.component.tsx | 199 +++++ .../program-enrollment.scss | 105 +++ .../program-summary.component.tsx | 286 ++++++++ .../src/program-summary/program-summary.scss | 79 ++ .../program-summary/program-summary.test.tsx | 101 +++ .../delete-regimen-modal.component.tsx | 79 ++ .../non-standard-regimen.component.tsx | 119 +++ .../regimen-button.component.tsx | 38 + .../regimen-editor/regimen-form.component.tsx | 360 +++++++++ .../regimen-reason.component.tsx | 48 ++ .../src/regimen-editor/regimen.resource.tsx | 40 + .../standard-regimen.component.tsx | 95 +++ .../src/regimen-editor/standard-regimen.scss | 69 ++ .../src/regimen-editor/utils.tsx | 12 + .../regimen-history.component.test.tsx | 43 ++ .../src/regimen/regimen-history.component.tsx | 83 +++ .../src/regimen/regimen-history.scss | 40 + packages/esm-care-panel-app/src/routes.json | 66 ++ .../esm-care-panel-app/src/types/index.ts | 221 ++++++ .../esm-care-panel-app/translations/en.json | 131 ++++ .../treatmentNumber/en.json | 1 + packages/esm-care-panel-app/tsconfig.json | 5 + packages/esm-care-panel-app/webpack.config.js | 1 + .../src/hooks/usePatientFlags.tsx | 2 +- .../src/hooks/usePatientId.tsx | 2 +- .../esm-patient-flags-app/src/routes.json | 2 +- .../assets/counties.json | 236 ++++++ .../assets/verification-assets.ts | 11 + .../patient-verification-hook.tsx | 181 +++++ .../patient-verification-utils.ts | 179 +++++ .../patient-verification.component.tsx | 124 ++++ .../patient-verification.scss | 25 + .../confirm-prompt.component.tsx | 72 ++ .../empty-prompt.component.tsx | 35 + .../verification-types.ts | 50 ++ .../patient-verification-hook.tsx | 2 +- .../pre-appointment.resource.tsx | 4 +- .../PBC/Project_Beyond_Clients.scss | 23 + .../PBC/Project_Beyond_Clients.tsx | 141 ++++ .../PBD/Project_Beyond_Deliveries.scss | 23 + .../PBD/Project_Beyond_Deliveries.tsx | 141 ++++ .../src/about/about.component.tsx | 2 +- packages/esm-version-app/src/routes.json | 2 +- yarn.lock | 229 +++--- 76 files changed, 5378 insertions(+), 119 deletions(-) create mode 100644 packages/esm-care-panel-app/README.md create mode 100644 packages/esm-care-panel-app/currentProphylaxisUsed/en.json create mode 100644 packages/esm-care-panel-app/hivStatus/en.json create mode 100644 packages/esm-care-panel-app/jest.config.js create mode 100644 packages/esm-care-panel-app/package.json create mode 100644 packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.component.tsx create mode 100644 packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.scss create mode 100644 packages/esm-care-panel-app/src/care-panel/care-panel.component.tsx create mode 100644 packages/esm-care-panel-app/src/care-panel/care-panel.scss create mode 100644 packages/esm-care-panel-app/src/care-programs/care-programs.component.tsx create mode 100644 packages/esm-care-panel-app/src/care-programs/care-programs.scss create mode 100644 packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx create mode 100644 packages/esm-care-panel-app/src/config-schema.ts create mode 100644 packages/esm-care-panel-app/src/dashboard.meta.ts create mode 100644 packages/esm-care-panel-app/src/declarations.d.ts create mode 100644 packages/esm-care-panel-app/src/hooks/useCarePrograms.tsx create mode 100644 packages/esm-care-panel-app/src/hooks/useEnrollmentHistory.ts create mode 100644 packages/esm-care-panel-app/src/hooks/useNonStandardRegimen.ts create mode 100644 packages/esm-care-panel-app/src/hooks/usePatientSummary.ts create mode 100644 packages/esm-care-panel-app/src/hooks/useProgramSummary.ts create mode 100644 packages/esm-care-panel-app/src/hooks/useRegimenEncounter.ts create mode 100644 packages/esm-care-panel-app/src/hooks/useRegimenHistory.ts create mode 100644 packages/esm-care-panel-app/src/hooks/useRegimenReason.ts create mode 100644 packages/esm-care-panel-app/src/hooks/useStandardRegimen.ts create mode 100644 packages/esm-care-panel-app/src/index.ts create mode 100644 packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx create mode 100644 packages/esm-care-panel-app/src/patient-summary/patient-summary.component.tsx create mode 100644 packages/esm-care-panel-app/src/patient-summary/patient-summary.scss create mode 100644 packages/esm-care-panel-app/src/print-layout/print.component.tsx create mode 100644 packages/esm-care-panel-app/src/print-layout/print.scss create mode 100644 packages/esm-care-panel-app/src/program-enrollment/program-enrollment.component.tsx create mode 100644 packages/esm-care-panel-app/src/program-enrollment/program-enrollment.scss create mode 100644 packages/esm-care-panel-app/src/program-summary/program-summary.component.tsx create mode 100644 packages/esm-care-panel-app/src/program-summary/program-summary.scss create mode 100644 packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/delete-regimen-modal.component.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/non-standard-regimen.component.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/regimen-button.component.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/regimen-form.component.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/regimen-reason.component.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/regimen.resource.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/standard-regimen.component.tsx create mode 100644 packages/esm-care-panel-app/src/regimen-editor/standard-regimen.scss create mode 100644 packages/esm-care-panel-app/src/regimen-editor/utils.tsx create mode 100644 packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx create mode 100644 packages/esm-care-panel-app/src/regimen/regimen-history.component.tsx create mode 100644 packages/esm-care-panel-app/src/regimen/regimen-history.scss create mode 100644 packages/esm-care-panel-app/src/routes.json create mode 100644 packages/esm-care-panel-app/src/types/index.ts create mode 100644 packages/esm-care-panel-app/translations/en.json create mode 100644 packages/esm-care-panel-app/treatmentNumber/en.json create mode 100644 packages/esm-care-panel-app/tsconfig.json create mode 100644 packages/esm-care-panel-app/webpack.config.js create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/assets/counties.json create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/assets/verification-assets.ts create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-hook.tsx create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-utils.ts create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.component.tsx create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.scss create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/confirm-prompt.component.tsx create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/empty-prompt.component.tsx create mode 100644 packages/esm-patient-registration-app/src/patient-verification-HIE/verification-types.ts create mode 100644 packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.scss create mode 100644 packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.tsx create mode 100644 packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.scss create mode 100644 packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.tsx diff --git a/package.json b/package.json index bc51fe8f..1a045e35 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,9 @@ "packages/*" ], "scripts": { - "start": "openmrs develop --backend https://ngx.ampath.or.ke --sources packages/esm-report-app --api-url /amrs --spa-path /amrs/spa/ --port 8040", + "start": "openmrs develop --backend http://10.50.80.110:8084 --sources packages/esm-*-app --api-url /amrs --spa-path /amrs/spa/ --port 8040", "dev": "openmrs develop --backend http://10.50.80.110:8084 --sources packages/esm-report-app --api-url /amrs --spa-path /amrs/spa/ --port 8040", - "start:core": "openmrs develop --backend https://ngx.ampath.or.ke --sources packages/esm-ampath-core-app --api-url /amrs --spa-path /amrs/spa/ --port 8030", + "start:core": "openmrs develop --backend http://10.50.80.110:8084 --sources packages/esm-ampath-core-app --api-url /amrs --spa-path /amrs/spa/ --port 8030", "ci:publish": "yarn workspaces foreach --all --topological --exclude @ampath/esm-3.x-app npm publish --access public --tag latest", "ci:prepublish": "yarn workspaces foreach --all --topological --exclude @ampath/esm-3.x-app npm publish --access public --tag next", "release": "yarn workspaces foreach --all --topological version", @@ -38,7 +38,7 @@ "@babel/core": "^7.11.6", "@carbon/react": "~1.37.0", "@ohri/openmrs-esm-ohri-commons-lib": "next", - "@openmrs/esm-framework": "^5.6.1-pre.1979", + "@openmrs/esm-framework": "^5.6.1-pre.2069", "@openmrs/esm-patient-common-lib": "next", "@playwright/test": "1.40.1", "@swc/core": "^1.2.165", @@ -75,7 +75,7 @@ "jest-cli": "^29.7.0", "jest-environment-jsdom": "^29.7.0", "lint-staged": "^15.2.1", - "openmrs": "^5.6.1-pre.1979", + "openmrs": "^5.6.1-pre.2069", "prettier": "^3.1.1", "react": "^18.1.0", "react-dom": "^18.1.0", diff --git a/packages/esm-billing-app/src/routes.json b/packages/esm-billing-app/src/routes.json index e49d3c65..c8373308 100644 --- a/packages/esm-billing-app/src/routes.json +++ b/packages/esm-billing-app/src/routes.json @@ -1,7 +1,7 @@ { "$schema": "https://json.openmrs.org/routes.schema.json", "backendDependencies": { - "amrscore": "^1.0.0", + "amrs": "^1.0.0", "amrsreporting": "^1.0.0" }, "pages": [ diff --git a/packages/esm-care-panel-app/README.md b/packages/esm-care-panel-app/README.md new file mode 100644 index 00000000..a2a214c0 --- /dev/null +++ b/packages/esm-care-panel-app/README.md @@ -0,0 +1,5 @@ +![Node.js CI](https://github.com/palladiumkenya/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 diff --git a/packages/esm-care-panel-app/currentProphylaxisUsed/en.json b/packages/esm-care-panel-app/currentProphylaxisUsed/en.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/esm-care-panel-app/currentProphylaxisUsed/en.json @@ -0,0 +1 @@ +{} diff --git a/packages/esm-care-panel-app/hivStatus/en.json b/packages/esm-care-panel-app/hivStatus/en.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/esm-care-panel-app/hivStatus/en.json @@ -0,0 +1 @@ +{} diff --git a/packages/esm-care-panel-app/jest.config.js b/packages/esm-care-panel-app/jest.config.js new file mode 100644 index 00000000..0352f621 --- /dev/null +++ b/packages/esm-care-panel-app/jest.config.js @@ -0,0 +1,3 @@ +const rootConfig = require('../../jest.config.js'); + +module.exports = rootConfig; diff --git a/packages/esm-care-panel-app/package.json b/packages/esm-care-panel-app/package.json new file mode 100644 index 00000000..430f089d --- /dev/null +++ b/packages/esm-care-panel-app/package.json @@ -0,0 +1,54 @@ +{ + "name": "@ampath/esm-care-panel-app", + "version": "5.2.0", + "description": "Patient care panels microfrontend for the OpenMRS SPA", + "browser": "dist/ampath-esm-care-panel-app.js", + "main": "src/index.ts", + "source": true, + "license": "MPL-2.0", + "homepage": "https://github.com/palladiumkenya/ampath-esm-core#readme", + "scripts": { + "start": "openmrs develop", + "serve": "webpack serve --mode=development", + "debug": "npm run serve", + "build": "webpack --mode production", + "analyze": "webpack --mode=production --env.analyze=true", + "lint": "eslint src --ext ts,tsx", + "typescript": "tsc", + "extract-translations": "i18next 'src/**/*.component.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js", + "test": "cross-env TZ=UTC jest --config jest.config.js --verbose false --passWithNoTests", + "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js", + "coverage": "yarn test --coverage" + }, + "browserslist": [ + "extends browserslist-config-openmrs" + ], + "keywords": [ + "openmrs" + ], + "publishConfig": { + "access": "public" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/palladiumkenya/ampath-esm-core#readme" + }, + "bugs": { + "url": "https://github.com/palladiumkenya/ampath-esm-core/issues" + }, + "dependencies": { + "@carbon/react": "^1.42.1", + "lodash-es": "^4.17.15", + "react-to-print": "^2.14.13" + }, + "peerDependencies": { + "@openmrs/esm-framework": "5.x", + "react": "^18.1.0", + "react-i18next": "11.x", + "react-router-dom": "6.x", + "swr": "2.x" + }, + "devDependencies": { + "webpack": "^5.74.0" + } +} 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 new file mode 100644 index 00000000..044f8130 --- /dev/null +++ b/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.component.tsx @@ -0,0 +1,50 @@ +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 styles from './care-panel-dashboard.scss'; + +type CarePanelDashboardProps = { patientUuid: string; formEntrySub: any; launchPatientWorkspace: Function }; + +const CarePanelDashboard: React.FC = ({ + formEntrySub, + patientUuid, + launchPatientWorkspace, +}) => { + const { t } = useTranslation(); + return ( + + +
+

{t('careProgramsEnrollement', 'Care panel')}

+
+
+
+ + + {t('panelSummary', 'Panel summary')} + {t('enrollments', 'Program enrollment')} + + + + + + + + + + +
+
+ ); +}; + +export default CarePanelDashboard; diff --git a/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.scss b/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.scss new file mode 100644 index 00000000..358f44d1 --- /dev/null +++ b/packages/esm-care-panel-app/src/care-panel-dashboard/care-panel-dashboard.scss @@ -0,0 +1,32 @@ +@use '@carbon/layout'; +@use '@carbon/type'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.container { + background-color: colors.$white; +} + +.tabs { + margin: 0 layout.$spacing-05; +} + +.desktopHeading, +.tabletHeading { + text-align: left; + text-transform: capitalize; + + h4 { + @include type.type-style('heading-compact-02'); + color: $text-02; + + &:after { + content: ''; + display: block; + width: 2rem; + padding-top: 3px; + border-bottom: 0.375rem solid; + @include brand-03(border-bottom-color); + } + } +} diff --git a/packages/esm-care-panel-app/src/care-panel/care-panel.component.tsx b/packages/esm-care-panel-app/src/care-panel/care-panel.component.tsx new file mode 100644 index 00000000..70a50a88 --- /dev/null +++ b/packages/esm-care-panel-app/src/care-panel/care-panel.component.tsx @@ -0,0 +1,82 @@ +import React, { useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { StructuredListSkeleton, ContentSwitcher, Switch } from '@carbon/react'; +import styles from './care-panel.scss'; +import { useEnrollmentHistory } from '../hooks/useEnrollmentHistory'; +import ProgramSummary from '../program-summary/program-summary.component'; +import ProgramEnrollment from '../program-enrollment/program-enrollment.component'; +import { CardHeader, EmptyState } from '@openmrs/esm-patient-common-lib'; +import RegimenHistory from '../regimen/regimen-history.component'; +import first from 'lodash/first'; +import sortBy from 'lodash/sortBy'; +import { ErrorState } from '@openmrs/esm-framework'; +import CarePrograms from '../care-programs/care-programs.component'; + +interface CarePanelProps { + patientUuid: string; + formEntrySub: any; + launchPatientWorkspace: Function; +} + +type SwitcherItem = { + index: number; + name?: string; + text?: string; +}; + +const CarePanel: React.FC = ({ patientUuid, formEntrySub, launchPatientWorkspace }) => { + const { t } = useTranslation(); + const { isLoading, error, enrollments } = useEnrollmentHistory(patientUuid); + const switcherHeaders = sortBy(Object.keys(enrollments || {})); + const [switchItem, setSwitcherItem] = useState({ index: 0 }); + const patientEnrollments = useMemo( + () => (isLoading ? [] : enrollments[switchItem?.name || first(switcherHeaders)]), + [enrollments, isLoading, switchItem?.name, switcherHeaders], + ); + + if (isLoading) { + return ( +
+ +
+ ); + } + + if (error) { + return ; + } + + if (Object.keys(enrollments).length === 0) { + return ( + <> + + + ); + } + + return ( + <> +
+ +
+ + {switcherHeaders?.map((enrollment) => )} + +
+
+
+ + + +
+
+ + ); +}; + +export default CarePanel; diff --git a/packages/esm-care-panel-app/src/care-panel/care-panel.scss b/packages/esm-care-panel-app/src/care-panel/care-panel.scss new file mode 100644 index 00000000..b59bce20 --- /dev/null +++ b/packages/esm-care-panel-app/src/care-panel/care-panel.scss @@ -0,0 +1,45 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.bodyShort01 { + @include type.type-style('body-compact-01'); +} + +.widgetCard { + background-color: $ui-background; + border: 1px solid $ui-03; + position: relative; +} + +.contextSwitcherContainer { + width: 20rem; + min-width: 60%; + margin-right: 1rem; +} + +.desktopContentSwitcher { + width: 100%; +} + +.tabletContentSwitcher { + width: 100%; +} + +.helperContainer { + color: $color-gray-70; + @extend .bodyShort01; + text-align: left; + padding-left: 1.3rem; +} + +.labelRed { + color: $danger; +} + +.careProgramContainer { + margin-top: spacing.$spacing-05; + & > div { + padding: 0; + } +} diff --git a/packages/esm-care-panel-app/src/care-programs/care-programs.component.tsx b/packages/esm-care-panel-app/src/care-programs/care-programs.component.tsx new file mode 100644 index 00000000..f1984033 --- /dev/null +++ b/packages/esm-care-panel-app/src/care-programs/care-programs.component.tsx @@ -0,0 +1,170 @@ +import React, { useCallback, useMemo } from 'react'; +import { + InlineLoading, + Button, + DataTable, + Table, + TableHeader, + TableRow, + TableHead, + TableBody, + TableCell, + TableContainer, + Tile, +} from '@carbon/react'; +import { Close, DocumentAdd } from '@carbon/react/icons'; +import { + CardHeader, + EmptyState, + launchStartVisitPrompt, + ErrorState, + launchPatientWorkspace, +} from '@openmrs/esm-patient-common-lib'; +import { useTranslation } from 'react-i18next'; +import { type PatientCarePrograms, useCarePrograms } from '../hooks/useCarePrograms'; +import { formatDate, useLayoutType, useVisit } from '@openmrs/esm-framework'; +import capitalize from 'lodash/capitalize'; +import { mutate } from 'swr'; + +import styles from './care-programs.scss'; + +type CareProgramsProps = { + patientUuid: string; +}; + +const CarePrograms: React.FC = ({ patientUuid }) => { + const { t } = useTranslation(); + const { currentVisit } = useVisit(patientUuid); + const { carePrograms, isLoading, isValidating, error } = useCarePrograms(patientUuid); + const isTablet = useLayoutType() === 'tablet'; + + const handleCareProgramClick = useCallback( + (careProgram: PatientCarePrograms) => { + const isEnrolled = careProgram.enrollmentStatus === 'active'; + const formUuid = isEnrolled ? careProgram.discontinuationFormUuid : careProgram.enrollmentFormUuid; + + const workspaceTitle = isEnrolled + ? `${careProgram.display} Discontinuation form` + : `${careProgram.display} Enrollment form`; + + currentVisit + ? launchPatientWorkspace('patient-form-entry-workspace', { + workspaceTitle: workspaceTitle, + mutateForm: () => { + mutate((key) => true, undefined, { + revalidate: true, + }); + }, + formInfo: { + encounterUuid: '', + formUuid, + additionalProps: { enrollmenrDetails: careProgram.enrollmentDetails } ?? {}, + }, + }) + : launchStartVisitPrompt(); + }, + [currentVisit], + ); + + const rows = useMemo( + () => + carePrograms.map((careProgram) => { + return { + id: `${careProgram.uuid}`, + programName: careProgram.display, + status: ( +
+ + {capitalize( + `${careProgram.enrollmentStatus} ${ + careProgram.enrollmentDetails?.dateEnrolled && careProgram.enrollmentStatus === 'active' + ? `Since (${formatDate(new Date(careProgram.enrollmentDetails.dateEnrolled))})` + : '' + }`, + )} + + +
+ ), + }; + }), + [carePrograms, handleCareProgramClick], + ); + + const headers = [ + { + key: 'programName', + header: 'Program name', + }, + { + key: 'status', + header: 'Status', + }, + ]; + + if (isLoading) { + return ( + + ); + } + + if (error) { + return ; + } + + if (carePrograms.length === 0) { + return ; + } + + return ( + + + {isValidating && ( + + )} + + + {({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => ( + + + + + {headers.map((header) => ( + {header.header} + ))} + + + + {rows.map((row) => ( + + {row.cells.map((cell) => ( + {cell.value} + ))} + + ))} + +
+
+ )} +
+
+ ); +}; + +export default CarePrograms; diff --git a/packages/esm-care-panel-app/src/care-programs/care-programs.scss b/packages/esm-care-panel-app/src/care-programs/care-programs.scss new file mode 100644 index 00000000..afb7001f --- /dev/null +++ b/packages/esm-care-panel-app/src/care-programs/care-programs.scss @@ -0,0 +1,14 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; + +.careProgramButtonContainer { + display: flex; + justify-content: space-between; + width: 100%; + align-items: center; +} + +.container { + border: 1px solid colors.$cool-gray-20; +} diff --git a/packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx b/packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx new file mode 100644 index 00000000..304a4929 --- /dev/null +++ b/packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx @@ -0,0 +1,150 @@ +import React from 'react'; +import { screen, render } from '@testing-library/react'; +import CarePrograms from './care-programs.component'; +import * as careProgramsHook from '../hooks/useCarePrograms'; +import { launchPatientWorkspace, launchStartVisitPrompt } from '@openmrs/esm-patient-common-lib'; +import { useVisit, launchWorkspace } from '@openmrs/esm-framework'; +import { type PatientCarePrograms } from '../hooks/useCarePrograms'; +import userEvent from '@testing-library/user-event'; + +jest.mock('../hooks/useCarePrograms'); + +const mockUseVisit = useVisit as jest.Mock; + +const testProps = { + isLoading: true, + isValidating: false, + error: null, + carePrograms: [], + mutate: jest.fn(), +}; + +const mockAPIResponse: Array = [ + { + uuid: 'dfdc6d40-2f2f-463d-ba90-cc97350441a8', + display: 'HIV', + enrollmentFormUuid: 'e4b506c1-7379-42b6-a374-284469cba8da', + discontinuationFormUuid: 'e3237ede-fa70-451f-9e6c-0908bc39f8b9', + enrollmentStatus: 'active', + enrollmentDetails: { + uuid: '561f0766-6496-4f59-abc2-a4030788b3cc', + dateEnrolled: '2023-10-25 03:27:15.0', + dateCompleted: '', + location: 'Moi Teaching Refferal Hospital', + }, + }, + { + uuid: '9f144a34-3a4a-44a9-8486-6b7af6cc64f6', + display: 'TB', + enrollmentFormUuid: '89994550-9939-40f3-afa6-173bce445c79', + discontinuationFormUuid: '4b296dd0-f6be-4007-9eb8-d0fd4e94fb3a', + enrollmentStatus: 'eligible', + }, +]; + +jest.mock('@openmrs/esm-framework', () => ({ + ...jest.requireActual('@openmrs/esm-framework'), + useVisit: jest.fn().mockReturnValue({ currentVisit: { uuid: 'some-visitUuid' } }), + useLayoutType: jest.fn().mockReturnValue('tablet'), + launchWorkspace: jest.fn(), +})); + +jest.mock('@openmrs/esm-patient-common-lib', () => ({ + ...jest.requireActual('@openmrs/esm-patient-common-lib'), + launchStartVisitPrompt: jest.fn(), + launchPatientWorkspace: jest.fn(), +})); + +describe('CarePrograms', () => { + xtest('should render loading spinner while fetching care programs', () => { + jest.spyOn(careProgramsHook, 'useCarePrograms').mockReturnValueOnce({ ...testProps }); + renderCarePrograms(); + const loadingSpinner = screen.getByText('Loading data...'); + expect(loadingSpinner).toBeInTheDocument(); + }); + + xtest('should render error state message in API has error', () => { + jest + .spyOn(careProgramsHook, 'useCarePrograms') + .mockReturnValueOnce({ ...testProps, isLoading: false, error: new Error('Internal error 500') }); + renderCarePrograms(); + const errorMessage = screen.getByText( + 'Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above.', + ); + expect(errorMessage).toBeInTheDocument(); + }); + + xtest('should display empty state if the patient is not eligible to any program', () => { + jest + .spyOn(careProgramsHook, 'useCarePrograms') + .mockReturnValueOnce({ ...testProps, isLoading: false, carePrograms: [] }); + + renderCarePrograms(); + const emptyStateMessage = screen.getByText('There are no {{displayText}} to display for this patient'); + const displayTitle = screen.getByRole('heading', { name: 'Care program' }); + expect(emptyStateMessage).toBeInTheDocument(); + expect(displayTitle).toBeInTheDocument(); + }); + + test('should display patient eligible programs and launch enrollment or discontinuation form', async () => { + const user = userEvent.setup(); + jest + .spyOn(careProgramsHook, 'useCarePrograms') + .mockReturnValueOnce({ ...testProps, isLoading: false, carePrograms: mockAPIResponse }); + mockUseVisit.mockReturnValue({ currentVisit: { uuid: 'some-visitUuid' } }); + renderCarePrograms(); + + const tableHeaders = ['Program name', 'Status']; + tableHeaders.forEach((tableHeader) => expect(screen.getByText(tableHeader)).toBeInTheDocument()); + const cardTitle = screen.getByRole('heading', { name: 'Care Programs' }); + expect(cardTitle).toBeInTheDocument(); + + const enrollButton = screen.getByRole('button', { name: /Enroll/ }); + const discontinueButton = screen.getByRole('button', { name: /Discontinue/ }); + await user.click(enrollButton); + expect(launchPatientWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', { + formInfo: { + additionalProps: { enrollmenrDetails: undefined }, + encounterUuid: '', + formUuid: '89994550-9939-40f3-afa6-173bce445c79', + }, + mutateForm: expect.anything(), + workspaceTitle: 'TB Enrollment form', + }); + + await user.click(discontinueButton); + expect(launchPatientWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', { + formInfo: { + additionalProps: { + enrollmenrDetails: { + dateCompleted: '', + dateEnrolled: '2023-10-25 03:27:15.0', + location: 'Moi Teaching Refferal Hospital', + uuid: '561f0766-6496-4f59-abc2-a4030788b3cc', + }, + }, + encounterUuid: '', + formUuid: 'e3237ede-fa70-451f-9e6c-0908bc39f8b9', + }, + mutateForm: expect.anything(), + workspaceTitle: 'HIV Discontinuation form', + }); + }); + + xtest('should prompt user to start Visit before filling any enrollment form', async () => { + const user = userEvent.setup(); + mockUseVisit.mockReturnValue({ currentVisit: null }); + jest + .spyOn(careProgramsHook, 'useCarePrograms') + .mockReturnValueOnce({ ...testProps, isLoading: false, carePrograms: mockAPIResponse }); + renderCarePrograms(); + + const enrollButton = screen.getByRole('button', { name: /Enroll/ }); + await user.click(enrollButton); + expect(launchStartVisitPrompt).toHaveBeenCalled(); + }); +}); + +const renderCarePrograms = () => { + render(); +}; diff --git a/packages/esm-care-panel-app/src/config-schema.ts b/packages/esm-care-panel-app/src/config-schema.ts new file mode 100644 index 00000000..093ca4f5 --- /dev/null +++ b/packages/esm-care-panel-app/src/config-schema.ts @@ -0,0 +1,17 @@ +import { Type } from '@openmrs/esm-framework'; + +export interface CarePanelConfig { + regimenObs: { + encounterProviderRoleUuid: string; + }; +} + +export const configSchema = { + regimenObs: { + encounterProviderRoleUuid: { + _type: Type.UUID, + _default: 'a0b03050-c99b-11e0-9572-0800200c9a66', + _description: "The provider role to use for the regimen encounter. Default is 'Unkown'.", + }, + }, +}; diff --git a/packages/esm-care-panel-app/src/dashboard.meta.ts b/packages/esm-care-panel-app/src/dashboard.meta.ts new file mode 100644 index 00000000..ad7d8b50 --- /dev/null +++ b/packages/esm-care-panel-app/src/dashboard.meta.ts @@ -0,0 +1,13 @@ +export const dashboardMeta = { + slot: 'patient-chart-care-panel-dashboard-slot', + columns: 1, + title: 'Care panel', + path: 'Care panel', +}; + +export const hivPatientSummaryDashboardMeta = { + slot: 'patient-chart-hiv-patient-summary-slot', + columns: 1, + title: 'HIV Patient Summary', + path: 'HIV Patient Summary', +}; diff --git a/packages/esm-care-panel-app/src/declarations.d.ts b/packages/esm-care-panel-app/src/declarations.d.ts new file mode 100644 index 00000000..dbad8404 --- /dev/null +++ b/packages/esm-care-panel-app/src/declarations.d.ts @@ -0,0 +1,4 @@ +declare module '*.css'; +declare module '*.scss'; +declare module '@carbon/react'; +declare type SideNavProps = object; diff --git a/packages/esm-care-panel-app/src/hooks/useCarePrograms.tsx b/packages/esm-care-panel-app/src/hooks/useCarePrograms.tsx new file mode 100644 index 00000000..ff60d442 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useCarePrograms.tsx @@ -0,0 +1,23 @@ +import useSWR from 'swr'; +import { openmrsFetch } from '@openmrs/esm-framework'; + +export type PatientCarePrograms = { + uuid: string; + display: string; + enrollmentFormUuid: string; + enrollmentStatus: string; + discontinuationFormUuid: string; + enrollmentDetails?: { uuid: string; dateCompleted: string; location: string; dateEnrolled: string }; +}; + +export const useCarePrograms = (patientUuid: string) => { + const url = `/ws/rest/v1/amrs/eligiblePrograms?patientUuid=${patientUuid}`; + const { data, error, isLoading, isValidating } = useSWR<{ data: Array }>(url, openmrsFetch); + + return { + carePrograms: data?.data?.filter((careProgram) => careProgram.enrollmentStatus !== 'active') ?? [], + error, + isLoading, + isValidating, + }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/useEnrollmentHistory.ts b/packages/esm-care-panel-app/src/hooks/useEnrollmentHistory.ts new file mode 100644 index 00000000..fa7781f6 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useEnrollmentHistory.ts @@ -0,0 +1,18 @@ +import useSWR from 'swr'; +import groupBy from 'lodash/groupBy'; +import { openmrsFetch } from '@openmrs/esm-framework'; + +export const useEnrollmentHistory = (patientUuid: string) => { + const enrollmentHistoryUrl = `/ws/rest/v1/amrs/patientHistoricalEnrollment?patientUuid=${patientUuid}`; + const { data, isValidating, error, isLoading } = useSWR<{ data: Array> }>( + enrollmentHistoryUrl, + openmrsFetch, + ); + + return { + error: error, + isLoading: isLoading, + enrollments: groupBy(data?.data ?? [], 'programName') ?? [], + isValidating, + }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/useNonStandardRegimen.ts b/packages/esm-care-panel-app/src/hooks/useNonStandardRegimen.ts new file mode 100644 index 00000000..9eb4c2e2 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useNonStandardRegimen.ts @@ -0,0 +1,18 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; + +interface nonStandardRegimen { + name: string; + uuid: string; +} + +export const useNonStandardRegimen = () => { + const nonStandardRegimenUrl = `/ws/rest/v1/amrs/arvDrugs`; + const { data, mutate, error, isLoading } = useSWR<{ data: { results: Array } }>( + nonStandardRegimenUrl, + openmrsFetch, + ); + + const nonStandardRegimen = data?.data?.results ? data?.data?.results : []; + return { nonStandardRegimen, isLoading, error }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/usePatientSummary.ts b/packages/esm-care-panel-app/src/hooks/usePatientSummary.ts new file mode 100644 index 00000000..8909f599 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/usePatientSummary.ts @@ -0,0 +1,14 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import { type PatientSummary } from '../types/index'; +import useSWR from 'swr'; + +export const usePatientSummary = (patientUuid: string) => { + const programSummaryUrl = `/ws/rest/v1/amrs/patientSummary?patientUuid=${patientUuid}`; + const { data, mutate, error, isLoading } = useSWR<{ data: PatientSummary }>(programSummaryUrl, openmrsFetch); + + return { + data: data?.data ? data?.data : null, + error, + isLoading, + }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/useProgramSummary.ts b/packages/esm-care-panel-app/src/hooks/useProgramSummary.ts new file mode 100644 index 00000000..bc030c60 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useProgramSummary.ts @@ -0,0 +1,14 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { type ProgramSummary } from '../types/index'; + +export const useProgramSummary = (patientUuid: string) => { + const programSummaryUrl = `/ws/rest/v1/amrs/currentProgramDetails?patientUuid=${patientUuid}`; + const { data, mutate, error, isLoading } = useSWR<{ data: ProgramSummary }>(programSummaryUrl, openmrsFetch); + + return { + data: data?.data ? data?.data : null, + isError: error, + isLoading: isLoading, + }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/useRegimenEncounter.ts b/packages/esm-care-panel-app/src/hooks/useRegimenEncounter.ts new file mode 100644 index 00000000..204ba2c8 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useRegimenEncounter.ts @@ -0,0 +1,11 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; +import { RegimenEncounter } from '../types'; + +export const useRegimenEncounter = (category: string, patientUuid: string) => { + const regimenEncounterUrl = `/ws/rest/v1/amrs/lastRegimenEncounter?patientUuid=${patientUuid}&category=${category}`; + const { data, mutate, error, isLoading } = useSWR<{ data: { results } }>(regimenEncounterUrl, openmrsFetch); + + const regimenEncounter = data?.data?.results ? data?.data?.results : ''; + return { regimenEncounter, isLoading, error }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/useRegimenHistory.ts b/packages/esm-care-panel-app/src/hooks/useRegimenHistory.ts new file mode 100644 index 00000000..f0d3e9c6 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useRegimenHistory.ts @@ -0,0 +1,30 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; + +interface PatientRegimenReturnType { + patientRegimen: RegimenHistory; + isLoading: boolean; + error: Error; +} + +interface RegimenHistory { + startDate: string; + endDate: string; + regimenShortDisplay: string; + regimenLine: string; + regimenLongDisplay: string; + changeReasons: Array; + regimenUuid: string; + current: boolean; +} + +export const useRegimenHistory = (patientUuid: string, category: string) => { + const regimenHistoryHistoryUrl = `/ws/rest/v1/amrs/regimenHistory?patientUuid=${patientUuid}&category=${category}`; + const { data, mutate, error, isLoading } = useSWR<{ data: { results: Array } }>( + regimenHistoryHistoryUrl, + openmrsFetch, + ); + + const regimen = data?.data?.results ? data?.data?.results : []; + return { regimen, isLoading, error }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/useRegimenReason.ts b/packages/esm-care-panel-app/src/hooks/useRegimenReason.ts new file mode 100644 index 00000000..082e8a81 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useRegimenReason.ts @@ -0,0 +1,22 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; + +interface RegimenReason { + category: string; + reason: Array; +} +interface Reason { + label: string; + value: string; +} + +export const useRegimenReason = () => { + const regimenReasonUrl = `/ws/rest/v1/amrs/regimenReason`; + const { data, mutate, error, isLoading } = useSWR<{ data: { results: Array } }>( + regimenReasonUrl, + openmrsFetch, + ); + + const regimenReason = data?.data?.results ? data?.data?.results : []; + return { regimenReason, isLoading, error }; +}; diff --git a/packages/esm-care-panel-app/src/hooks/useStandardRegimen.ts b/packages/esm-care-panel-app/src/hooks/useStandardRegimen.ts new file mode 100644 index 00000000..ec7475a9 --- /dev/null +++ b/packages/esm-care-panel-app/src/hooks/useStandardRegimen.ts @@ -0,0 +1,29 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import useSWR from 'swr'; + +interface StandardRegimen { + categoryCode: string; + category: Array; +} + +interface RegimenCategory { + regimenline: string; + regimenLineValue: string; + regimen: Array; +} + +interface Regimen { + name: string; + conceptRef: string; +} + +export const useStandardRegimen = () => { + const standardRegimenUrl = `/ws/rest/v1/amrs/standardRegimen`; + const { data, mutate, error, isLoading } = useSWR<{ data: { results: Array } }>( + standardRegimenUrl, + openmrsFetch, + ); + + const standardRegimen = data?.data?.results ? data?.data?.results : []; + return { standardRegimen, isLoading, error }; +}; diff --git a/packages/esm-care-panel-app/src/index.ts b/packages/esm-care-panel-app/src/index.ts new file mode 100644 index 00000000..e809dbda --- /dev/null +++ b/packages/esm-care-panel-app/src/index.ts @@ -0,0 +1,47 @@ +import { defineConfigSchema, getSyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework'; +import { configSchema } from './config-schema'; +import { dashboardMeta, hivPatientSummaryDashboardMeta } from './dashboard.meta'; +import { createDashboardLink } from '@openmrs/esm-patient-common-lib'; +import carePanelComponent from './care-panel/care-panel.component'; +import careProgramsComponent from './care-programs/care-programs.component'; +import deleteRegimenConfirmationDialogComponent from './regimen-editor/delete-regimen-modal.component'; +import regimenFormComponent from './regimen-editor/regimen-form.component'; +import CarePanelDashboard from './care-panel-dashboard/care-panel-dashboard.component'; +import PatientSummary from './patient-summary/patient-summary.component'; + +const moduleName = '@ampath/esm-care-panel-app'; + +const options = { + featureName: 'patient-care-panels', + moduleName, +}; + +export const importTranslation = require.context('../translations', false, /.json$/, 'lazy'); + +export function startupApp() { + registerBreadcrumbs([]); + defineConfigSchema(moduleName, configSchema); +} + +export const carePanelPatientSummary = getSyncLifecycle(CarePanelDashboard, options); + +export const deleteRegimenConfirmationDialog = getSyncLifecycle(deleteRegimenConfirmationDialogComponent, options); + +export const patientProgramSummary = getSyncLifecycle(carePanelComponent, options); + +export const patientCareProgram = getSyncLifecycle(careProgramsComponent, { + moduleName: 'patient-care-programs', + featureName: 'care-programs', +}); + +// t('carePanel', 'Care panel') +export const carePanelSummaryDashboardLink = getSyncLifecycle( + createDashboardLink({ ...dashboardMeta, moduleName }), + options, +); +export const hivPatientSummaryDashboardLink = getSyncLifecycle( + createDashboardLink({ ...hivPatientSummaryDashboardMeta, moduleName }), + options, +); +export const hivPatientSummary = getSyncLifecycle(PatientSummary, options); +export const regimenFormWorkspace = getSyncLifecycle(regimenFormComponent, options); 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 100644 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/patient-summary/patient-summary.component.tsx b/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.tsx new file mode 100644 index 00000000..43a50f71 --- /dev/null +++ b/packages/esm-care-panel-app/src/patient-summary/patient-summary.component.tsx @@ -0,0 +1,684 @@ +import React, { useRef, useState } from 'react'; +import dayjs from 'dayjs'; +import { useTranslation } from 'react-i18next'; +import { Button, StructuredListSkeleton } from '@carbon/react'; +import { formatDate, useLayoutType, useSession } from '@openmrs/esm-framework'; +import { usePatientSummary } from '../hooks/usePatientSummary'; +import { Printer } from '@carbon/react/icons'; +import { useReactToPrint } from 'react-to-print'; +import PrintComponent from '../print-layout/print.component'; +import styles from './patient-summary.scss'; + +interface PatientSummaryProps { + patientUuid: string; +} + +const PatientSummary: React.FC = ({ patientUuid }) => { + const { data, error, isLoading } = usePatientSummary(patientUuid); + const currentUserSession = useSession(); + const componentRef = useRef(null); + const [printMode, setPrintMode] = useState(false); + + const { t } = useTranslation(); + const isTablet = useLayoutType() == 'tablet'; + + const printRef = useReactToPrint({ + content: () => componentRef.current, + onBeforeGetContent: () => setPrintMode(true), + onAfterPrint: () => setPrintMode(false), + pageStyle: styles.pageStyle, + documentTitle: data?.patientName, + }); + + const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + const handlePrint = async () => { + await delay(500); + printRef(); + }; + + if (isLoading) { + return ; + } + + if (error) { + return {t('errorPatientSummary', 'Error loading patient summary')}; + } + + if (Object.keys(data)?.length === 0) { + return; + } + + if (Object.keys(data).length > 0) { + return ( +
+ {printMode === true && } +
+
+

{t('patientSummary', 'Patient summary')}

+ {printMode === false && ( + + )} +
+
+
+

{t('reportDate', 'Report date')}

+

+ {data?.reportDate ? formatDate(new Date(data?.reportDate)) : '--'} +

+
+
+

{t('clinicName', 'Clinic name')}

+

+ {data?.clinicName ? data?.clinicName : '--'} +

+
+
+

{t('mflCode', 'MFL code')}

+

+ {data?.mflCode} +

+
+
+ +
+
+

{t('uniquePatientIdentifier', 'Unique patient identifier')}

+

+ + {data?.uniquePatientIdentifier ? data?.uniquePatientIdentifier : '--'} + +

+
+
+

+ {t('nationalUniquePatientIdentifier', 'National unique patient identifier')} +

+

+ + {data?.nationalUniquePatientIdentifier ? data?.nationalUniquePatientIdentifier : '--'} + +

+
+
+

{t('patientName', 'Patient name')}

+

+ {data?.patientName ? data?.patientName : '--'} +

+
+
+ +
+
+

{t('birthDate', 'Birth date')}

+

+ {data?.birthDate ? formatDate(new Date(data?.birthDate)) : '--'} +

+
+
+

{t('age', 'Age')}

+

+ {data?.age ? data?.age : '--'} +

+
+
+

{t('gender', 'Gender')}

+

+ + {data?.gender === 'F' ? 'Female' : data?.gender === 'M' ? 'Male' : 'Unknown'} + +

+
+
+ +
+
+

{t('maritalStatus', 'Marital status')}

+

+ {data?.maritalStatus ? data?.maritalStatus : '--'} +

+
+
+ +
+ +
+
+

{t('dateConfirmedPositive', 'Date confirmed HIV positive')}

+

+ + {data?.dateConfirmedHIVPositive ? formatDate(new Date(data?.dateConfirmedHIVPositive)) : '--'} + +

+
+
+

{t('firstCD4', 'First CD4')}

+

+ {data?.firstCd4 ? data?.firstCd4 : '--'} +

+
+
+

{t('dateFirstCD4', 'Date first CD4')}

+

+ + {data?.firstCd4Date === 'N/A' || data?.firstCd4Date === '' ? ( + None + ) : ( + {formatDate(new Date(data?.firstCd4Date))} + )} + +

+
+
+ +
+
+

{t('dateEnrolledToCare', 'Date enrolled into care')}

+

+ + {data?.dateEnrolledIntoCare ? formatDate(new Date(data?.dateEnrolledIntoCare)) : '--'} + +

+
+
+

{t('whoAtEnrollment', 'WHO stage at enrollment')}

+

+ + {data?.whoStagingAtEnrollment ? data?.whoStagingAtEnrollment : '--'} + +

+
+
+

{t('transferInDate', 'Transfer in date')}

+

+ + {data?.transferInDate === 'N/A' || data?.transferInDate === '' ? ( + None + ) : ( + {data?.transferInDate} + )} + +

+
+
+ +
+
+

{t('entryPoint', 'Entry point')}

+

+ {data?.patientEntryPoint ? data?.patientEntryPoint : '--'} +

+
+
+

{t('dateOfEntryPoint', 'Date of entry point')}

+

+ + {data?.patientEntryPointDate ? formatDate(new Date(data?.patientEntryPointDate)) : '--'} + +

+
+
+

{t('facilityTransferredFrom', 'Facility transferred from')}

+

+ {data?.transferInFacility ? data?.transferInFacility : '--'} +

+
+
+ +
+ +
+
+

{t('weight', 'Weight')}

+

+ {data?.weight ? data?.weight : '--'} +

+
+
+

{t('height', 'Height')}

+

+ {data?.height ? data?.height : '--'} +

+
+
+

{t('bmi', 'BMI')}

+

+ {data?.bmi ? data?.bmi : '--'} +

+
+
+ +
+
+

{t('bloodPressure', 'Blood pressure')}

+

+ {data?.bloodPressure ? data?.bloodPressure : '--'} +

+
+
+

{t('oxygenSaturation', 'Oxygen saturation')}

+

+ {data?.oxygenSaturation ? data?.oxygenSaturation : '--'} +

+
+
+

{t('respiratoryRate', 'Respiratory rate')}

+

+ {data?.respiratoryRate ? data?.respiratoryRate : '--'} +

+
+
+ +
+
+

{t('pulseRate', 'Pulse rate')}

+

+ {data?.pulseRate ? data?.pulseRate : '--'} +

+
+
+

{t('familyProtection', 'FP method')}

+

+ {data?.familyProtection ? data?.familyProtection : '--'} +

+
+
+

{t('tbScreeningOutcome', 'TB screening outcome')}

+

+ {data?.tbScreeningOutcome ? data?.tbScreeningOutcome : '--'} +

+
+
+ +
+
+

{t('chronicDisease', 'Chronic disease')}

+

+ {data?.chronicDisease ? data?.chronicDisease : '--'} +

+
+
+

{t('ioHistory', ' OI history')}

+

+ {data?.iosResults ? data?.iosResults : '--'} +

+
+
+

{t('stiScreeningOutcome', 'Sti screening')}

+

+ {data?.stiScreeningOutcome ? data?.stiScreeningOutcome : '--'} +

+
+
+ +
+ {data?.gender === 'F' && ( +
+

{t('caxcScreeningOutcome', 'Caxc screening')}

+

+ {data?.caxcScreeningOutcome ? data?.caxcScreeningOutcome : '--'} +

+
+ )} + +
+

{t('dateEnrolledInTb', 'TPT start date')}

+

+ + {data?.dateEnrolledInTb === 'None' || data?.dateEnrolledInTb === '' ? ( + None + ) : ( + {formatDate(new Date(data?.dateEnrolledInTb))} + )} + +

+
+
+

{t('dateCompletedInTb', 'TPT completion date')}

+

+ + {data?.dateCompletedInTb === 'None' || data?.dateCompletedInTb === '' ? ( + None + ) : ( + {formatDate(new Date(data?.dateCompletedInTb))} + )} + +

+
+ {data?.gender === 'F' && ( +
+

{t('lmp', 'LMP')}

+

+ {data?.lmp ? formatDate(new Date(data?.lmp)) : '--'} +

+
+ )} +
+ +
+ +
+
+

{t('treatmentSupporterName', 'Treatment supporter name')}

+

+ + {data?.nameOfTreatmentSupporter ? data?.nameOfTreatmentSupporter : '--'} + +

+
+
+

{t('treatmentSupporterRelationship', 'Treatment supporter relationship')}

+

+ + {data?.relationshipToTreatmentSupporter ? data?.relationshipToTreatmentSupporter : '--'} + +

+
+
+

{t('treatmentSupporterContact', 'Treatment Supporter contact')}

+

+ + {data?.contactOfTreatmentSupporter ? data?.contactOfTreatmentSupporter : '--'} + +

+
+
+ +
+ +
+
+

{t('drugAllergies', 'Drug allergies')}

+

+ {data?.allergies ? data?.allergies : '--'} +

+
+
+ +
+ +
+
+

{t('previousART', 'Previous ART')}

+

+ {data?.previousArtStatus ? data?.previousArtStatus : '--'} +

+
+
+

{t('dateStartedART', 'Date started ART')}

+

+ + {data?.dateStartedArt ? formatDate(new Date(data?.dateStartedArt)) : '--'} + +

+
+
+

{t('clinicalStageART', 'Clinical stage at ART')}

+

+ {data?.whoStageAtArtStart ? data?.whoStageAtArtStart : '--'} +

+
+
+ +
+
+

{t('purposeDrugs', 'Purpose drugs')}

+

+ {data?.purposeDrugs ? data?.purposeDrugs : 'None'} +

+
+
+

{t('purposeDate', 'Purpose drugs date')}

+

+ + {data?.purposeDate ? formatDate(new Date(data?.purposeDate)) : 'None'} + +

+
+
+

{t('cd4AtArtStart', 'CD4 at ART start')}

+

+ {data?.cd4AtArtStart ? data?.cd4AtArtStart : '--'} +

+
+
+ +
+
+

{t('initialRegimen', 'Initial regimen')}

+

+ + {data?.firstRegimen ? data?.firstRegimen?.regimenShortDisplay : '--'} + +

+
+
+

{t('initialRegimenDate', 'Initial regimen date')}

+

+ {data?.firstRegimen ? data?.firstRegimen?.startDate : '--'} +

+
+
+

{t('currentArtRegimen', 'Current Art regimen')}

+

+ + {data?.currentArtRegimen ? data?.currentArtRegimen?.regimenShortDisplay : '--'} + +

+
+
+

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

+

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

+
+
+ +
+ +
+
+

{t('artInterruptionReason', 'ART interruptions reason')}

+

+ -- + +

+
+
+

+ {t('substitutionWithin1stLineRegimen', 'Substitution within 1st line regimen')} +

+

+ -- + +

+
+
+

{t('switchTo2ndLineRegimen', 'Switch to 2nd line regimen')}

+

+ -- + +

+
+
+ +
+
+

{t('Dapsone', 'Dapsone')}

+

+ {data?.dapsone ? data?.dapsone : '--'} +

+
+
+

{t('tpt', 'TPT')}

+

+ {data?.onIpt ? data?.onIpt : '--'} +

+
+
+

{t('clinicsEnrolled', 'Clinics enrolled')}

+

+ {data?.clinicsEnrolled ? data?.clinicsEnrolled : '--'} +

+
+
+ +
+
+

{t('transferOutDate', 'Transfer out date')}

+

+ + {data?.transferOutDate === 'N/A' || data?.transferOutDate === '' ? ( + None + ) : ( + {formatDate(new Date(data?.transferOutDate))} + )} + +

+
+
+

{t('transferOutFacility', 'Transfer out facility')}

+

+ {data?.transferOutFacility ? data?.transferOutFacility : '--'} +

+
+
+

{t('deathDate', 'Death date')}

+

+ + {data?.deathDate === 'N/A' || data?.deathDate === '' ? ( + None + ) : ( + {formatDate(new Date(data?.deathDate))} + )} + +

+
+
+ +
+
+

{t('mostRecentCD4', 'Most recent CD4')}

+

+ {data?.mostRecentCd4 ? data?.mostRecentCd4 : ''} + + {data?.mostRecentCd4Date === 'N/A' || data?.mostRecentCd4Date === '' ? ( + None + ) : ( + {formatDate(new Date(data?.mostRecentCd4Date))} + )} + +

+
+
+

{t('mostRecentVL', 'Most recent VL')}

+

+ {data?.viralLoadValue ? data?.viralLoadValue : '--'} + + {data?.viralLoadDate === 'N/A' || data?.viralLoadDate === '' ? ( + None + ) : ( + {data?.viralLoadDate} + )} + +

+
+
+

{t('nextAppointmentDate', ' Next appointment')}

+

+ + {data?.nextAppointmentDate ? formatDate(new Date(data?.nextAppointmentDate)) : '--'} + +

+
+
+ +
+
+

{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) : '--'} +
+
+ ); + } + }) + : '--'} +
+
+ +
+ +
+
+

{t('clinicalNotes', 'Clinical notes')}

+

+ -- +

+
+
+

{t('clinicianName', 'Clinician name')}

+

+ + {currentUserSession?.user?.person?.display + ? currentUserSession?.user?.person?.display + : t('none', 'None')} + +

+
+
+

{t('clinicianSignature', 'Clinician signature')}

+

+ + {currentUserSession?.user?.person?.display + ? currentUserSession?.user?.person?.display + : t('none', 'None')} + +

+
+
+
+
+ ); + } +}; + +export default PatientSummary; diff --git a/packages/esm-care-panel-app/src/patient-summary/patient-summary.scss b/packages/esm-care-panel-app/src/patient-summary/patient-summary.scss new file mode 100644 index 00000000..ee5d92db --- /dev/null +++ b/packages/esm-care-panel-app/src/patient-summary/patient-summary.scss @@ -0,0 +1,84 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.bodyContainer { + margin: spacing.$spacing-03; + border: 1px solid colors.$cool-gray-20; +} + +.desktopHeading { + h4 { + @include type.type-style('heading-compact-02'); + color: $text-02; + } +} + +.tabletHeading { + h4 { + @include type.type-style('heading-03'); + color: $text-02; + } +} + +.desktopHeading, +.tabletHeading { + display: flex; + justify-content: space-between; + text-transform: capitalize; + margin-bottom: spacing.$spacing-02; + margin-top: spacing.$spacing-02; + + h4:after { + content: ''; + display: block; + width: 2rem; + padding-top: 0.188rem; + border-bottom: 0.375rem solid var(--brand-03); + } +} + +.heading:after { + content: ''; + display: block; + width: 2rem; + padding-top: 0.188rem; + border-bottom: 0.375rem solid var(--brand-03); +} + +.card { + background-color: $ui-02; + padding: spacing.$spacing-04; +} + +.value { + @include type.type-style('productive-heading-03'); + color: colors.$gray-100; +} + +.label { + @include type.type-style('label-01'); + color: $text-02; +} + +.container { + display: flex; + flex-direction: row; + padding-left: 1rem; + flex-wrap: wrap; +} + +.content { + margin-top: spacing.$spacing-05; + width: 30%; + margin-left: spacing.$spacing-03; +} + +.btnHidden { + visibility: hidden; +} + +.btnShow { + visibility: visible; +} diff --git a/packages/esm-care-panel-app/src/print-layout/print.component.tsx b/packages/esm-care-panel-app/src/print-layout/print.component.tsx new file mode 100644 index 00000000..3f7e03d6 --- /dev/null +++ b/packages/esm-care-panel-app/src/print-layout/print.component.tsx @@ -0,0 +1,16 @@ +import React, { useRef } from 'react'; +import styles from './print.scss'; +import { useConfig } from '@openmrs/esm-framework'; + +export function PrintComponent() { + const componentRef = useRef(null); + const { logo } = useConfig(); + + return ( +
+
{logo?.src ? {logo?.alt} : null}
+
+ ); +} + +export default PrintComponent; diff --git a/packages/esm-care-panel-app/src/print-layout/print.scss b/packages/esm-care-panel-app/src/print-layout/print.scss new file mode 100644 index 00000000..764b4863 --- /dev/null +++ b/packages/esm-care-panel-app/src/print-layout/print.scss @@ -0,0 +1,9 @@ +.logo { + margin: 2rem; +} + +.printPage { + display: flex; + justify-content: center; + align-items: center; +} 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 new file mode 100644 index 00000000..2b47bb36 --- /dev/null +++ b/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.component.tsx @@ -0,0 +1,199 @@ +import React, { useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + Tile, + DataTable, + Table, + TableHead, + TableRow, + TableHeader, + TableBody, + TableCell, + OverflowMenu, + OverflowMenuItem, + TableContainer, +} from '@carbon/react'; +import styles from './program-enrollment.scss'; +import isEmpty from 'lodash/isEmpty'; +import dayjs from 'dayjs'; +import { formatDate } from '@openmrs/esm-framework'; +import orderBy from 'lodash/orderBy'; +import { mutate } from 'swr'; +import { launchPatientWorkspace } from '@openmrs/esm-patient-common-lib'; + +export interface ProgramEnrollmentProps { + patientUuid: string; + programName: string; + enrollments: Array; + formEntrySub: any; +} +const shareObjProperty = { dateEnrolled: 'Enrolled on', dateCompleted: 'Date Completed' }; +const programDetailsMap = { + HIV: { + dateEnrolled: 'Enrolled on', + whoStage: 'WHO Stage', + entryPoint: 'Entry Point', + regimenShortDisplay: 'Regimen', + changeReasons: 'Reason for regimen change', + reason: 'Reason for discontinuation', + }, + TB: { + ...shareObjProperty, + startDate: 'Date started regimen', + regimenShortName: 'Regimen', + }, + TPT: { + ...shareObjProperty, + tptDrugName: 'Regimen', + tptDrugStartDate: 'Date started regimen', + tptIndication: 'Indication for TPT', + }, + 'MCH - Mother Services': { + ...shareObjProperty, + lmp: 'LMP', + eddLmp: 'EDD', + gravida: 'Gravida', + parity: 'Parity', + gestationInWeeks: 'Gestation in weeks', + }, + 'MCH - Child Services': { ...shareObjProperty, entryPoint: 'Entry Point' }, + mchMother: {}, + mchChild: {}, + VMMC: { + ...shareObjProperty, + }, +}; + +const ProgramEnrollment: React.FC = ({ enrollments = [], programName }) => { + const { t } = useTranslation(); + const orderedEnrollments = orderBy(enrollments, 'dateEnrolled', 'desc'); + const headers = useMemo( + () => + Object.entries(programDetailsMap[programName] ?? { ...shareObjProperty }).map(([key, value]) => ({ + key, + header: value, + })), + [programName], + ); + const rows = useMemo( + () => + orderedEnrollments?.map((enrollment) => { + const firstEncounter = enrollment?.firstEncounter ?? {}; + const enrollmentEncounterUuid = enrollment?.enrollmentEncounterUuid; + return { + id: `${enrollment.enrollmentUuid}`, + ...enrollment, + ...firstEncounter, + changeReasons: enrollment?.firstEncounter?.changeReasons?.join(', '), + enrollmentEncounterUuid: enrollmentEncounterUuid, + }; + }), + [orderedEnrollments], + ); + + const handleDiscontinue = (enrollment) => { + launchPatientWorkspace('patient-form-entry-workspace', { + workspaceTitle: enrollment?.discontinuationFormName, + mutateForm: () => { + mutate((key) => true, undefined, { + revalidate: true, + }); + }, + formInfo: { + encounterUuid: '', + formUuid: enrollment?.discontinuationFormUuid, + additionalProps: + { enrollmentDetails: { dateEnrolled: new Date(enrollment.dateEnrolled), uuid: enrollment.enrollmentUuid } } ?? + {}, + }, + }); + }; + + const handleEditEnrollment = (enrollment) => { + launchPatientWorkspace('patient-form-entry-workspace', { + workspaceTitle: enrollment?.enrollmentFormName, + mutateForm: () => { + mutate((key) => true, undefined, { + revalidate: true, + }); + }, + formInfo: { + encounterUuid: enrollment?.enrollmentEncounterUuid, + formUuid: enrollment?.enrollmentFormUuid, + additionalProps: + { enrollmentDetails: { dateEnrolled: new Date(enrollment.dateEnrolled), uuid: enrollment.enrollmentUuid } } ?? + {}, + }, + }); + }; + + if (orderedEnrollments?.length === 0) { + return null; + } + + return ( + +
+ + {({ rows, headers, getHeaderProps, getRowProps, getTableProps }) => ( + + + + + {headers.map((header) => ( + + {header.header} + + ))} + + + + + {rows.map((row, index) => ( + + {row.cells.map((cell) => ( + + {isEmpty(cell.value) + ? '--' + : dayjs(cell.value).isValid() + ? formatDate(new Date(cell.value)) + : cell.value} + + ))} + + {isEmpty(orderedEnrollments[index]?.dateCompleted) && ( + + handleEditEnrollment(orderedEnrollments[index])} + /> + handleDiscontinue(orderedEnrollments[index])} + /> + + )} + + + ))} + +
+
+ )} +
+
+
+ ); +}; +export default ProgramEnrollment; diff --git a/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.scss b/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.scss new file mode 100644 index 00000000..b289c554 --- /dev/null +++ b/packages/esm-care-panel-app/src/program-enrollment/program-enrollment.scss @@ -0,0 +1,105 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.desktopHeading { + h4 { + @include type.type-style('heading-compact-02'); + color: $text-02; + background-color: $ui-background; + padding-top: spacing.$spacing-02; + } +} + +.tabletHeading { + h4 { + @include type.type-style('heading-03'); + color: $text-02; + } +} + +.desktopHeading, +.tabletHeading { + display: flex; + justify-content: space-between; + text-align: left; + text-transform: capitalize; + background-color: $ui-background; + padding-top: spacing.$spacing-02; + padding-bottom: spacing.$spacing-03; +} + +.title { + @include type.type-style('productive-heading-03'); + color: colors.$gray-100; + margin-left: spacing.$spacing-05; +} + +.card { + background-color: $ui-02; + padding: spacing.$spacing-03; +} + +.btnEdit { + margin-left: auto; +} + +.buttonContainer { + display: flex; + justify-content: flex-end; +} + +.labelContainer { + display: flex; + flex-direction: row; + flex-wrap: wrap; +} + +.value { + @include type.type-style('productive-heading-03'); + color: colors.$gray-100; +} + +.content { + width: 30%; + margin: spacing.$spacing-01 0; + + & > p:first-child { + @include type.type-style('label-01'); + color: colors.$gray-70; + margin-bottom: spacing.$spacing-02; + } + + & span { + @include type.type-style('body-short-02'); + color: colors.$gray-100; + } +} + +.programTitle { + display: flex; + align-items: center; + justify-content: space-between; + + & > h4 { + @include type.type-style('productive-heading-02'); + color: colors.$gray-80; + } +} + +.tileWrapper { + gap: spacing.$spacing-05; +} + +.layer { + height: 100%; + + :global(.cds--btn--primary) { + background-color: unset; + } +} + +.menuItem { + max-width: none; +} diff --git a/packages/esm-care-panel-app/src/program-summary/program-summary.component.tsx b/packages/esm-care-panel-app/src/program-summary/program-summary.component.tsx new file mode 100644 index 00000000..169da1bd --- /dev/null +++ b/packages/esm-care-panel-app/src/program-summary/program-summary.component.tsx @@ -0,0 +1,286 @@ +import React from 'react'; +import styles from './program-summary.scss'; +import { useProgramSummary } from '../hooks/useProgramSummary'; +import { useTranslation } from 'react-i18next'; +import { formatDate, useLayoutType } from '@openmrs/esm-framework'; +import { StructuredListSkeleton, Tile } from '@carbon/react'; +import { ProgramType, RegimenType } from '../types'; +import RegimenButton from '../regimen-editor/regimen-button.component'; +import { useRegimenEncounter } from '../hooks/useRegimenEncounter'; +export interface ProgramSummaryProps { + patientUuid: string; + programName: string; +} +const ProgramSummary: React.FC = ({ patientUuid, programName }) => { + const { data, isError, isLoading } = useProgramSummary(patientUuid); + const { t } = useTranslation(); + const { regimenEncounter } = useRegimenEncounter(RegimenType[programName], patientUuid); + + const isTablet = useLayoutType() == 'tablet'; + if (isLoading) { + return ; + } + if (isError) { + return {t('errorProgramSummary', 'Error loading HIV summary')}; + } + if (Object.keys(data ?? {})?.length === 0) { + return; + } + if (Object.keys(data ?? {}).length > 0) { + return ( + <> + {Object.entries(data).map(([key, val]) => + key == ProgramType.HIV && programName == ProgramType.HIV ? ( + +
+
+

{t('currentStatus', 'Current status')}

+
+
+
+

{t('lastViralLoad', 'Last viral load')}

+

+ {' '} + {data?.HIV?.ldlValue ? data?.HIV?.ldlValue : '--'} + {data?.HIV?.ldlDate ? ({formatDate(new Date(data?.HIV?.ldlDate))}) : ''} +

+
+
+

{t('lastCd4Count', 'Last CD4 count')}

+

+ {data?.HIV?.cd4 ? data?.HIV?.cd4 : '--'} + {data?.HIV?.cd4Date ? ({formatDate(new Date(data?.HIV?.cd4Date))}) : ''} +

+
+
+

{t('CD4Percentage', 'CD4 percentage')}

+

+ {data?.HIV?.cd4Percent ? data?.HIV?.cd4Percent : '--'} + {data?.HIV?.cd4PercentDate ? ( + ({formatDate(new Date(data?.HIV?.cd4PercentDate))}) + ) : ( + '' + )} +

+
+
+

{t('lastWhoStage', 'Last WHO stage')}

+

+ + {t('whoStage', 'WHO STAGE')} {data?.HIV?.whoStage ? data?.HIV?.whoStage : '--'} + + {data?.HIV?.whoStageDate ? ({formatDate(new Date(data?.HIV?.whoStageDate))}) : ''} +

+
+
+

{t('regimen', 'Regimen')}

+

+ {data?.HIV?.lastEncDetails?.regimenShortDisplay + ? data?.HIV?.lastEncDetails?.regimenShortDisplay + : t('neverOnArvRegimen', 'Never on ARVs')} + + + +

+
+
+

{t('regimenStartDate', ' Date started regimen')}

+

+ {data?.HIV?.lastEncDetails?.startDate + ? formatDate(new Date(data?.HIV?.lastEncDetails?.startDate)) + : '--'} +

+
+
+
+
+ ) : key == ProgramType.TB && programName == ProgramType.TB ? ( + +
+
+

{t('currentStatus', 'Current status')}

+
+
+
+

{t('treatmentNumber:', 'Treatment number')}

+

+ + {data?.TB?.tbTreatmentNumber ? data?.TB?.tbTreatmentNumber : '--'} + +

+
+
+

{t('diseaseClassification', 'Disease classification')}

+

+ + {data?.TB?.tbDiseaseClassification ? data?.TB?.tbDiseaseClassification : '--'} + + {data?.TB?.tbDiseaseClassificationDate ? ( + ({formatDate(new Date(data?.TB?.tbDiseaseClassificationDate.toString()))}) + ) : ( + '' + )} +

+
+
+

{t('patientClassification', 'Patient classification')}

+

+ + {data?.TB?.tbPatientClassification ? data?.TB?.tbPatientClassification : '--'} + +

+
+
+

{t('regimen', 'Regimen')}

+

+ + {data?.TB?.lastTbEncounter + ? data?.TB?.lastTbEncounter?.regimenShortDisplay + : t('neverOnTbRegimen', 'Never on TB regimen')} + + +

+
+
+

{t('regimenStartDate', ' Date started regimen')}

+

+ + {data?.HIV?.lastEncDetails?.startDate + ? formatDate(new Date(data?.HIV?.lastEncDetails?.startDate)) + : '--'} + +

+
+
+
+
+ ) : key == ProgramType.MCHMOTHER && programName == ProgramType.MCHMOTHER ? ( + +
+
+

{t('currentStatus', 'Current status')}

+
+
+
+

{t('hivStatus:', 'HIV status')}

+

+ + {data?.mchMother?.hivStatus ? data?.mchMother?.hivStatus : '--'} + + {data?.mchMother?.hivStatusDate ? ( + ({formatDate(new Date(data?.mchMother?.hivStatusDate))}) + ) : ( + '' + )} +

+
+
+

{t('onART', 'On ART')}

+

+ {data?.mchMother?.onHaart ? data?.mchMother?.onHaart : '--'} + {data?.mchMother?.onHaartDate ? ( + ({formatDate(new Date(data?.mchMother?.onHaartDate))}) + ) : ( + '' + )} +

+
+
+
+
+ ) : key == ProgramType.MCHCHILD && programName == ProgramType.MCHCHILD ? ( + +
+
+

{t('currentStatus', 'Current status')}

+
+
+
+

{t('currentProphylaxisUsed:', 'Current prophylaxis used')}

+

+ + {data?.mchChild?.currentProphylaxisUsed ? data?.mchChild?.currentProphylaxisUsed : '--'} + + {data?.mchChild?.currentProphylaxisUsedDate ? ( + {formatDate(new Date(data?.mchChild?.currentProphylaxisUsedDate))} + ) : ( + '' + )} +

+
+
+

{t('currentFeedingOption', 'Current feeding option')}

+

+ + {data?.mchChild?.currentFeedingOption ? data?.mchChild?.currentFeedingOption : '--'} + + {data?.mchChild?.currentFeedingOptionDate ? ( + {formatDate(new Date(data?.mchChild?.currentFeedingOptionDate))} + ) : ( + '' + )} +

+
+
+

{t('milestonesAttained', 'Milestones Attained')}

+

+ + {data?.mchChild?.milestonesAttained ? data?.mchChild?.milestonesAttained : '--'} + + {data?.mchChild?.milestonesAttainedDate ? ( + {formatDate(new Date(data?.mchChild?.milestonesAttainedDate))} + ) : ( + '' + )} +

+
+
+

{t('heiOutcome', 'HEI Outcome')}

+

+ + {data?.mchChild?.heiOutcome ? data?.mchChild?.heiOutcome : '--'} + + {data?.mchChild?.heiOutcomeDate ? ( + ({formatDate(new Date(data?.mchChild?.heiOutcomeDate))}) + ) : ( + '' + )} +

+
+
+

{t('hivStatus', 'HIV Status')}

+

+ + {data?.mchChild?.hivStatus ? data?.mchChild?.hivStatus : '--'} + + {data?.mchChild?.hivStatusDate ? ( + ({formatDate(new Date(data?.mchChild?.hivStatusDate))}) + ) : ( + '' + )} +

+
+
+
+
+ ) : null, + )} + + ); + } +}; +export default ProgramSummary; diff --git a/packages/esm-care-panel-app/src/program-summary/program-summary.scss b/packages/esm-care-panel-app/src/program-summary/program-summary.scss new file mode 100644 index 00000000..f6564ffa --- /dev/null +++ b/packages/esm-care-panel-app/src/program-summary/program-summary.scss @@ -0,0 +1,79 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.desktopHeading { + h4 { + @include type.type-style('heading-compact-02'); + color: $text-02; + padding-top: spacing.$spacing-02; + } +} + +.tabletHeading { + h4 { + @include type.type-style('heading-03'); + color: $text-02; + } +} + +.desktopHeading, +.tabletHeading { + text-align: left; + text-transform: capitalize; + padding-top: spacing.$spacing-02; +} + +.heading:after { + content: ''; + display: block; + width: 2rem; + padding-top: 0.188rem; + border-bottom: 0.375rem solid var(--brand-03); +} + +.card { + background-color: $ui-02; + padding-bottom: spacing.$spacing-04; +} + +.title { + @include type.type-style('productive-heading-03'); + color: colors.$gray-100; + margin-left: spacing.$spacing-05; +} + +.label { + font-weight: bold; + @include type.type-style('label-01'); + color: colors.$gray-70; +} + +.container { + display: flex; + flex-direction: row; + padding-left: 1rem; + flex-wrap: wrap; +} + +.value { + @include type.type-style('body-short-02'); + color: colors.$gray-100; +} + +.content { + width: 30%; + margin: spacing.$spacing-03 0; + + & > p:first-child { + @include type.type-style('label-01'); + color: colors.$gray-70; + margin-bottom: spacing.$spacing-02; + } + + & span { + @include type.type-style('body-short-02'); + color: colors.$gray-100; + } +} 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 100644 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/delete-regimen-modal.component.tsx b/packages/esm-care-panel-app/src/regimen-editor/delete-regimen-modal.component.tsx new file mode 100644 index 00000000..f926faba --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/delete-regimen-modal.component.tsx @@ -0,0 +1,79 @@ +import React, { useCallback, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react'; +import { showNotification, showSnackbar } from '@openmrs/esm-framework'; +import { deleteEncounter } from './regimen.resource'; +import { mutate } from 'swr'; + +interface DeleteRegimenModalProps { + closeCancelModal: () => void; + regimenEncounterUuid: string; + patientUuid: string; + category: string; + closeWorkspace: () => void; +} + +const DeleteRegimenModal: React.FC = ({ + closeCancelModal, + regimenEncounterUuid, + patientUuid, + category, + closeWorkspace, +}) => { + const { t } = useTranslation(); + const [isSubmitting, setIsSubmitting] = useState(false); + + const handleCancel = useCallback( + (event) => { + event.preventDefault(); + setIsSubmitting(true); + + deleteEncounter(regimenEncounterUuid) + .then((response) => { + if (response.status === 204) { + closeCancelModal(); + showSnackbar({ + title: t('regimenDeleted', 'Regimen deleted'), + subtitle: t('regimenDeletedSuccessfully', 'Regimen deleted successfully'), + kind: 'success', + timeoutInMs: 3500, + isLowContrast: true, + }); + mutate(`/ws/rest/v1/amrs/regimenHistory?patientUuid=${patientUuid}&category=${category}`); + mutate(`/ws/rest/v1/amrs/currentProgramDetails?patientUuid=${patientUuid}`); + mutate(`/ws/rest/v1/amrs/patientSummary?patientUuid=${patientUuid}`); + mutate(`/ws/rest/v1/amrs/lastRegimenEncounter?patientUuid=${patientUuid}&category=${category}`); + closeWorkspace?.(); + } + }) + .catch((err) => { + showNotification({ + title: t('regimenDeletedError', 'Error deleting regimen'), + kind: 'error', + critical: true, + description: err?.message, + }); + }); + }, + [t, regimenEncounterUuid, closeCancelModal, category, patientUuid, closeWorkspace], + ); + + return ( +
+ + +

{t('deleteRegimenModalConfirmationText', 'Are you sure you want to delete regimen?')}

+
+ + + + +
+ ); +}; + +export default DeleteRegimenModal; 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 new file mode 100644 index 00000000..64ad3812 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/non-standard-regimen.component.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Select, SelectItem } from '@carbon/react'; +import { useStandardRegimen } from '../hooks/useStandardRegimen'; +import styles from './standard-regimen.scss'; +import { useNonStandardRegimen } from '../hooks/useNonStandardRegimen'; +import { Regimen } from '../types'; + +interface NonStandardRegimenProps { + category: string; + setNonStandardRegimens: (value: any) => void; + setStandardRegimenLine: (value: any) => void; + selectedRegimenType: string; +} + +const NonStandardRegimen: React.FC = ({ + category, + selectedRegimenType, + setNonStandardRegimens, + setStandardRegimenLine, +}) => { + const { t } = useTranslation(); + const { standardRegimen } = useStandardRegimen(); + const { nonStandardRegimen, isLoading, error } = useNonStandardRegimen(); + const [selectedRegimenLine, setSelectedRegimenLine] = useState(''); + 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 handleRegimenLineChange = (e) => { + setSelectedRegimenLine(e.target.value); + setStandardRegimenLine(e.target.value); + }; + + const handleRegimenChange = (index, value) => { + const newSelectedRegimens = [...selectedRegimens]; + newSelectedRegimens[index] = value; + setSelectedRegimens(newSelectedRegimens); + }; + + useEffect(() => { + const generateRegimenObjects = () => { + return selectedRegimens + .map((uuid) => { + const regimen = nonStandardRegimen.find((r) => r.uuid === uuid); + if (regimen) { + return { + value: regimen.uuid, + concept: Regimen.nonStandardRegimenConcept, + }; + } + return null; + }) + .filter((obj) => obj); + }; + + setStandardRegimenObjects(generateRegimenObjects()); + }, [selectedRegimens, nonStandardRegimen]); + + useEffect(() => { + if (selectedRegimenLine) { + setNonStandardRegimens(nonStandardRegimenObjects); + } + }, [selectedRegimenLine, nonStandardRegimenObjects, setNonStandardRegimens]); + + return ( +
+ <> + {selectedRegimenType === 'nonStandardUuid' ? ( + + ) : null} + + {selectedRegimenLine && ( +
+ {selectedRegimens.map((selectedRegimen, index) => { + const availableRegimens = nonStandardRegimen.filter( + (regimen) => !selectedRegimens.includes(regimen.uuid) || regimen.uuid === selectedRegimen, + ); + + return ( + + ); + })} +
+ )} + +
+ ); +}; + +export default NonStandardRegimen; diff --git a/packages/esm-care-panel-app/src/regimen-editor/regimen-button.component.tsx b/packages/esm-care-panel-app/src/regimen-editor/regimen-button.component.tsx new file mode 100644 index 00000000..dc0a9c97 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/regimen-button.component.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { Link } from '@carbon/react'; +import { useTranslation } from 'react-i18next'; +import { RegimenType } from '../types'; +import styles from './standard-regimen.scss'; +import { launchWorkspace } from '@openmrs/esm-framework'; + +interface RegimenButtonProps { + patientUuid: string; + category: string; + onRegimen: string; + lastRegimenEncounter: { + uuid: string; + startDate: string; + endDate: string; + event: string; + }; +} + +const RegimenButton: React.FC = ({ category, patientUuid, onRegimen, lastRegimenEncounter }) => { + const { t } = useTranslation(); + return ( + + launchWorkspace('patient-regimen-workspace', { + category: RegimenType[category], + patientUuid: patientUuid, + onRegimen: onRegimen, + lastRegimenEncounter: lastRegimenEncounter, + }) + }> + {t('editRegimen', 'Edit')} + + ); +}; + +export default RegimenButton; diff --git a/packages/esm-care-panel-app/src/regimen-editor/regimen-form.component.tsx b/packages/esm-care-panel-app/src/regimen-editor/regimen-form.component.tsx new file mode 100644 index 00000000..6fc16ca8 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/regimen-form.component.tsx @@ -0,0 +1,360 @@ +import React, { useEffect, useState, useCallback, useMemo } from 'react'; +import dayjs from 'dayjs'; +import { + Button, + ButtonSet, + DatePicker, + DatePickerInput, + Form, + Stack, + RadioButtonGroup, + RadioButton, +} from '@carbon/react'; +import { useTranslation } from 'react-i18next'; +import { + useSession, + useLayoutType, + toOmrsIsoString, + toDateObjectStrict, + showNotification, + useConfig, + showModal, + showSnackbar, +} from '@openmrs/esm-framework'; +import styles from './standard-regimen.scss'; +import StandardRegimen from './standard-regimen.component'; +import RegimenReason from './regimen-reason.component'; +import { type Encounter, Regimen, type UpdateObs } from '../types'; +import { saveEncounter, updateEncounter } from './regimen.resource'; +import { useRegimenEncounter } from '../hooks/useRegimenEncounter'; +import { type CarePanelConfig } from '../config-schema'; +import { mutate } from 'swr'; +import NonStandardRegimen from './non-standard-regimen.component'; +import { addOrUpdateObsObject } from './utils'; + +interface RegimenFormProps { + patientUuid: string; + category: string; + onRegimen: string; + lastRegimenEncounter: { + uuid: string; + startDate: string; + endDate: string; + event: string; + }; + closeWorkspace: () => void; +} + +const RegimenForm: React.FC = ({ + patientUuid, + category, + onRegimen, + lastRegimenEncounter, + closeWorkspace, +}) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() === 'tablet'; + const sessionUser = useSession(); + const config = useConfig() as CarePanelConfig; + const { regimenEncounter, isLoading, error } = useRegimenEncounter(category, patientUuid); + const [isSubmitting, setIsSubmitting] = useState(false); + const [visitDate, setVisitDate] = useState(new Date()); + const [regimenEvent, setRegimenEvent] = useState(''); + const [standardRegimen, setStandardRegimen] = useState(''); + const [standardRegimenLine, setStandardRegimenLine] = useState(''); + const [nonStandardRegimens, setNonStandardRegimens] = useState([]); + const [regimenReason, setRegimenReason] = useState(''); + const [selectedRegimenType, setSelectedRegimenType] = useState(''); + const [obsArray, setObsArray] = useState([]); + const [obsArrayForPrevEncounter, setObsArrayForPrevEncounter] = useState([]); + + useEffect(() => { + const regimenLineObs = { + concept: Regimen.RegimenLineConcept, + value: standardRegimenLine, + }; + const standardRegimenObs = { + concept: Regimen.standardRegimenConcept, + value: standardRegimen, + }; + const regimenReasonObs = { + concept: Regimen.reasonCodedConcept, + value: regimenReason, + }; + const dateStoppedRegObs = { + concept: Regimen.dateDrugStoppedCon, + value: toDateObjectStrict( + toOmrsIsoString(new Date(dayjs(visitDate).year(), dayjs(visitDate).month(), dayjs(visitDate).date())), + ), + }; + const categoryObs = { + concept: category === 'ARV' ? Regimen.arvCategoryConcept : Regimen.tbCategoryConcept, + value: regimenEvent, + }; + + if (standardRegimenLine && regimenEvent !== Regimen.stopRegimenConcept) { + addOrUpdateObsObject(regimenLineObs, obsArray, setObsArray); + } + + if (standardRegimen && regimenEvent !== Regimen.stopRegimenConcept) { + addOrUpdateObsObject(standardRegimenObs, obsArray, setObsArray); + } + + if ( + regimenReason && + (regimenEvent === Regimen.stopRegimenConcept || regimenEvent === Regimen.changeRegimenConcept) + ) { + addOrUpdateObsObject(regimenReasonObs, obsArrayForPrevEncounter, setObsArrayForPrevEncounter); + } + + if (visitDate && (regimenEvent === Regimen.stopRegimenConcept || regimenEvent === Regimen.changeRegimenConcept)) { + addOrUpdateObsObject(dateStoppedRegObs, obsArrayForPrevEncounter, setObsArrayForPrevEncounter); + } + + if (regimenEvent && category) { + if (regimenEvent === Regimen.stopRegimenConcept) { + addOrUpdateObsObject(categoryObs, obsArrayForPrevEncounter, setObsArrayForPrevEncounter); + } else { + addOrUpdateObsObject(categoryObs, obsArray, setObsArray); + } + } + }, [ + standardRegimenLine, + regimenReason, + standardRegimen, + category, + regimenEvent, + visitDate, + obsArray, + obsArrayForPrevEncounter, + ]); + + useEffect(() => { + if ( + selectedRegimenType === 'nonStandardUuid' && + nonStandardRegimens.length > 0 && + regimenEvent !== Regimen.stopRegimenConcept + ) { + setObsArray((prevObsArray) => { + const distinctValuesMap = new Map(); + prevObsArray.forEach((item) => { + distinctValuesMap.set(item.value, item); + }); + nonStandardRegimens.forEach((item) => { + distinctValuesMap.set(item.value, item); + }); + const uniqueObsArray = Array.from(distinctValuesMap.values()); + return uniqueObsArray; + }); + } + }, [selectedRegimenType, nonStandardRegimens, regimenEvent]); + + const handleSubmit = useCallback( + (event) => { + event.preventDefault(); + setIsSubmitting(true); + + const encounterToSave: Encounter = { + encounterDatetime: toDateObjectStrict( + toOmrsIsoString(new Date(dayjs(visitDate).year(), dayjs(visitDate).month(), dayjs(visitDate).date())), + ), + patient: patientUuid, + encounterType: Regimen.regimenEncounterType, + location: sessionUser?.sessionLocation?.uuid, + encounterProviders: [ + { + provider: sessionUser?.currentProvider?.uuid, + encounterRole: config.regimenObs.encounterProviderRoleUuid, + }, + ], + form: Regimen.regimenForm, + obs: obsArray, + }; + + const encounterToUpdate: UpdateObs = { + obs: obsArrayForPrevEncounter, + }; + + if (regimenEncounter.uuid) { + updateEncounter(encounterToUpdate, regimenEncounter.uuid); + closeWorkspace(); + } + + if (obsArray.length > 0) { + saveEncounter(encounterToSave).then( + (response) => { + if (response.status === 201) { + showSnackbar({ + title: t('regimenUpdated', 'Regimen updated'), + subtitle: t('regimenUpdatedSuccessfully', 'Regimen updated successfully'), + kind: 'success', + timeoutInMs: 3500, + isLowContrast: true, + }); + setIsSubmitting(false); + mutate(`/ws/rest/v1/amrs/currentProgramDetails?patientUuid=${patientUuid}`); + mutate(`/ws/rest/v1/amrs/patientSummary?patientUuid=${patientUuid}`); + mutate(`/ws/rest/v1/amrs/regimenHistory?patientUuid=${patientUuid}&category=${category}`); + mutate(`/ws/rest/v1/amrs/lastRegimenEncounter?patientUuid=${patientUuid}&category=${category}`); + + closeWorkspace(); + } + }, + (error) => { + showNotification({ + title: t('regimenError', 'Error updating regimen'), + kind: 'error', + critical: true, + description: error?.message, + }); + setIsSubmitting(false); + }, + ); + } else { + setIsSubmitting(false); + } + }, + [ + patientUuid, + t, + category, + visitDate, + obsArray, + obsArrayForPrevEncounter, + sessionUser, + config, + regimenEncounter.uuid, + closeWorkspace, + ], + ); + + const launchDeleteRegimenDialog = () => { + const dispose = showModal('delete-regimen-confirmation-dialog', { + closeCancelModal: () => dispose(), + regimenEncounterUuid: regimenEncounter.uuid, + patientUuid, + category, + closeWorkspace, + }); + }; + + const regimenDatePicker = useMemo( + () => ( + setVisitDate(date)} + value={visitDate}> + + + ), + [visitDate, t], + ); + + return ( +
+
+ +

Current Regimen: {onRegimen}

+
+
{t('regimenEvent', 'Regimen event')}
+ setRegimenEvent(uuid)}> + + + + + + + {regimenEvent ? ( + <> + {regimenEvent !== 'undo' && regimenDatePicker} + {regimenEvent && regimenEvent !== Regimen.stopRegimenConcept && regimenEvent !== 'undo' ? ( + <> + setSelectedRegimenType(uuid)}> + + + + {selectedRegimenType === 'standardUuid' ? ( + + ) : ( + + )} + + ) : null} + {(regimenEvent === Regimen.stopRegimenConcept || + (regimenEvent === Regimen.changeRegimenConcept && selectedRegimenType)) && ( + + )} + + ) : null} +
+
+
+ + + + +
+ ); +}; + +export default RegimenForm; diff --git a/packages/esm-care-panel-app/src/regimen-editor/regimen-reason.component.tsx b/packages/esm-care-panel-app/src/regimen-editor/regimen-reason.component.tsx new file mode 100644 index 00000000..8eb115c1 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/regimen-reason.component.tsx @@ -0,0 +1,48 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Select, SelectItem } from '@carbon/react'; +import styles from './standard-regimen.scss'; +import { useRegimenReason } from '../hooks/useRegimenReason'; + +interface RegimenReasonProps { + category: string; + setRegimenReason: (value: any) => void; +} + +const RegimenReason: React.FC = ({ category, setRegimenReason }) => { + const { t } = useTranslation(); + const { regimenReason, isLoading, error } = useRegimenReason(); + + const [selectedRegimenReason, setSelectedRegimenReason] = useState(''); + const matchingCategory = regimenReason.find((item) => item.category === category); + + const handleRegimenReasonChange = (e) => { + setSelectedRegimenReason(e.target.value); + setRegimenReason(e.target.value); + }; + + return ( +
+ <> + + +
+ ); +}; + +export default RegimenReason; diff --git a/packages/esm-care-panel-app/src/regimen-editor/regimen.resource.tsx b/packages/esm-care-panel-app/src/regimen-editor/regimen.resource.tsx new file mode 100644 index 00000000..4e54d83c --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/regimen.resource.tsx @@ -0,0 +1,40 @@ +import { openmrsFetch } from '@openmrs/esm-framework'; +import { type Encounter, type UpdateObs } from '../types'; + +export function saveEncounter(encounter: Encounter) { + const abortController = new AbortController(); + + return openmrsFetch(`/ws/rest/v1/encounter`, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: encounter, + signal: abortController.signal, + }); +} + +export function updateEncounter(encounter: UpdateObs, encounterUuid) { + const abortController = new AbortController(); + + return openmrsFetch(`/ws/rest/v1/encounter/${encounterUuid}`, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'POST', + body: encounter, + signal: abortController.signal, + }); +} + +export function deleteEncounter(encounterUuid) { + const abortController = new AbortController(); + + return openmrsFetch(`/ws/rest/v1/encounter/${encounterUuid}`, { + headers: { + 'Content-Type': 'application/json', + }, + method: 'DELETE', + signal: abortController.signal, + }); +} 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 new file mode 100644 index 00000000..79587b7c --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.component.tsx @@ -0,0 +1,95 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Select, SelectItem } from '@carbon/react'; +import { useStandardRegimen } from '../hooks/useStandardRegimen'; +import styles from './standard-regimen.scss'; + +interface StandardRegimenProps { + category: string; + setStandardRegimen: (value: any) => void; + setStandardRegimenLine: (value: any) => void; + selectedRegimenType: string; +} + +const StandardRegimen: React.FC = ({ + category, + setStandardRegimen, + setStandardRegimenLine, + selectedRegimenType, +}) => { + const { t } = useTranslation(); + const { standardRegimen, isLoading, error } = useStandardRegimen(); + + const [selectedRegimenLine, setSelectedRegimenLine] = useState(''); + const [selectedRegimen, setSelectedRegimen] = useState(''); + const [selectedRegimens, setSelectedRegimens] = useState([]); + const matchingCategory = standardRegimen.find((item) => item.categoryCode === category); + + useEffect(() => { + const matchingRegimenLine = matchingCategory?.category.find( + (line) => line.regimenLineValue === selectedRegimenLine, + ); + if (matchingRegimenLine) { + setSelectedRegimens(matchingRegimenLine.regimen); + } else { + setSelectedRegimens([]); + } + }, [selectedRegimenLine, standardRegimen, matchingCategory]); + + const handleRegimenLineChange = (e) => { + setSelectedRegimenLine(e.target.value); + setStandardRegimenLine(e.target.value); + setSelectedRegimen(''); // Reset selected regimen when regimen line changes + }; + + const handleRegimenChange = (e) => { + setSelectedRegimen(e.target.value); + setStandardRegimen(e.target.value); + }; + + return ( +
+ <> + {selectedRegimenType === 'standardUuid' ? ( + + ) : null} + + {selectedRegimenLine && ( + + )} + +
+ ); +}; + +export default StandardRegimen; diff --git a/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.scss b/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.scss new file mode 100644 index 00000000..075e7bc0 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/standard-regimen.scss @@ -0,0 +1,69 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.inputContainer { + margin: spacing.$spacing-05 0; +} +.container { + margin: spacing.$spacing-05; + background-color: $ui-background; + + & section { + margin: spacing.$spacing-02 spacing.$spacing-05 0; + } +} +.sectionTitle { + @include type.type-style('heading-compact-02'); + color: $text-02; + margin-bottom: spacing.$spacing-05; +} + +.dateTimeSection { + display: flex; +} + +.tablet { + padding: spacing.$spacing-06 spacing.$spacing-05; + background-color: $ui-02; +} + +.desktop { + padding: 0rem; +} +.button { + height: 4rem; + display: flex; + align-content: flex-start; + align-items: baseline; + min-width: 50%; +} + +.tablet { + padding: spacing.$spacing-06 spacing.$spacing-05; + background-color: $ui-02; +} + +.linkName { + padding-left: 2rem; + + &:active { + text-decoration: none; + } + + &:hover { + text-decoration: none; + cursor: pointer; + } +} + +.radioButtonWrapper { + margin-bottom: spacing.$spacing-05; +} + +.regimenTitle { + @include type.type-style('productive-heading-02'); + color: colors.$gray-80; + margin-left: 1rem; +} diff --git a/packages/esm-care-panel-app/src/regimen-editor/utils.tsx b/packages/esm-care-panel-app/src/regimen-editor/utils.tsx new file mode 100644 index 00000000..c2cec6a2 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen-editor/utils.tsx @@ -0,0 +1,12 @@ +export function addOrUpdateObsObject(objectToAdd, obsArray, setObjectArray) { + if (doesObjectExistInArray(obsArray, objectToAdd)) { + setObjectArray((prevObsArray) => + prevObsArray.map((obs) => (obs.concept === objectToAdd.concept ? objectToAdd : obs)), + ); + } else { + setObjectArray((prevObsArray) => [...prevObsArray, objectToAdd]); + } +} + +const doesObjectExistInArray = (obsArray, objectToCheck) => + obsArray.some((obs) => obs.concept === objectToCheck.concept); 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 100644 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/regimen/regimen-history.component.tsx b/packages/esm-care-panel-app/src/regimen/regimen-history.component.tsx new file mode 100644 index 00000000..7a4e7dc0 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen/regimen-history.component.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { + StructuredListSkeleton, + StructuredListRow, + StructuredListCell, + StructuredListWrapper, + StructuredListHead, + StructuredListBody, + Tile, +} from '@carbon/react'; +import styles from './regimen-history.scss'; +import { useRegimenHistory } from '../hooks/useRegimenHistory'; +import { formatDate, parseDate, useLayoutType } from '@openmrs/esm-framework'; +import { RegimenType } from '../types'; + +export interface RegimenHistoryProps { + patientUuid: string; + category: string; +} + +const RegimenHistory: React.FC = ({ patientUuid, category }) => { + const { t } = useTranslation(); + const isTablet = useLayoutType() == 'tablet'; + const { regimen, isLoading, error } = useRegimenHistory(patientUuid, RegimenType[category]); + + if (isLoading) { + return ; + } + + if (error) { + return {t('errorRegimenHistory', 'Error loading regimen history')}; + } + + if (regimen?.length === 0) { + return; + } + + if (regimen?.length) { + const structuredListBodyRowGenerator = () => { + return regimen.map((regimen, i) => ( + + {formatDate(parseDate(regimen.startDate), { mode: 'wide' })} + + {regimen.endDate ? formatDate(parseDate(regimen.endDate), { mode: 'wide' }) : ''} + + {regimen.regimenShortDisplay ? regimen.regimenShortDisplay : '——'} + {regimen.regimenLine ? regimen.regimenLine : '——'} + {regimen.changeReasons ? regimen.changeReasons.slice(1, -1) : '——'} + + + )); + }; + + return ( +
+ +
+

{t('regimenHistory', 'Regimen History')}

+
+
+ + + + {t('start', 'Start')} + {t('end', 'End')} + {t('regimen', 'Regimen')} + {t('regimenLine', 'Regimen line')} + {t('changeReason', 'Change reason')} + + + + {structuredListBodyRowGenerator()} + + +
+
+
+ ); + } +}; + +export default RegimenHistory; diff --git a/packages/esm-care-panel-app/src/regimen/regimen-history.scss b/packages/esm-care-panel-app/src/regimen/regimen-history.scss new file mode 100644 index 00000000..69acc890 --- /dev/null +++ b/packages/esm-care-panel-app/src/regimen/regimen-history.scss @@ -0,0 +1,40 @@ +@use '@carbon/styles/scss/type'; +@use '@carbon/styles/scss/spacing'; +@use '@carbon/colors'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.bodyShort01 { + @include type.type-style('body-compact-01'); +} + +.desktopHeading { + h4 { + @include type.type-style('heading-compact-02'); + color: $text-02; + background-color: $ui-background; + padding-top: spacing.$spacing-02; + } +} +.desktopHeading, +.tabletHeading { + text-align: left; + text-transform: capitalize; + background-color: $ui-background; + padding-top: spacing.$spacing-02; +} +.structuredList { + padding: 0.5rem 0.5rem 0.5rem; +} + +.title { + @include type.type-style('productive-heading-03'); + color: colors.$gray-100; + margin-left: spacing.$spacing-05; +} + +.structuredListBody { + :global(.cds--structured-list-td) { + padding: 0.2rem 0.2rem 0.5rem; + } + background-color: $ui-background; +} diff --git a/packages/esm-care-panel-app/src/routes.json b/packages/esm-care-panel-app/src/routes.json new file mode 100644 index 00000000..299b51dd --- /dev/null +++ b/packages/esm-care-panel-app/src/routes.json @@ -0,0 +1,66 @@ +{ + "$schema": "https://json.openmrs.org/routes.schema.json", + "backendDependencies": { + "amrs": "^1.0.0" + }, + "pages": [], + "extensions": [ + { + "name": "care-panel-patient-summary", + "component": "carePanelPatientSummary", + "slot": "patient-chart-care-panel-dashboard-slot", + "order": 10, + "meta": { + "columnSpan": 4 + } + }, + { + "name": "care-panel-summary-dashboard-link", + "component": "carePanelSummaryDashboardLink", + "slot": "patient-chart-dashboard-slot", + "order": 3, + "meta": { + "columns": 1, + "columnSpan": 1, + "slot": "patient-chart-care-panel-dashboard-slot", + "layoutMode": "anchored", + "path": "Care panel" + } + }, + { + "name": "delete-regimen-confirmation-dialog", + "component": "deleteRegimenConfirmationDialog" + }, + { + "name": "hiv-patient-visit-summary-dashboard-link", + "component": "hivPatientSummaryDashboardLink", + "slot": "hiv-care-and-treatment-slot", + "meta": { + "columns": 1, + "columnSpan": 1, + "slot": "patient-chart-hiv-patient-summary-slot", + "path": "HIV Patient Summary", + "layoutMode": "anchored" + } + }, + { + "name": "hiv-patient-visit-summary", + "slot": "patient-chart-hiv-patient-summary-slot", + "component": "hivPatientSummary", + "order": 3, + "online": true, + "offline": false + } + ], + "workspaces": [ + { + "name": "patient-regimen-workspace", + "title": "Patient Regimen", + "component":"regimenFormWorkspace", + "type":"form", + "canMaximize": true, + "canHide": true, + "width": "wider" + } + ] +} diff --git a/packages/esm-care-panel-app/src/types/index.ts b/packages/esm-care-panel-app/src/types/index.ts new file mode 100644 index 00000000..c49f7175 --- /dev/null +++ b/packages/esm-care-panel-app/src/types/index.ts @@ -0,0 +1,221 @@ +type HIVData = { + whoStage: number; + whoStageDate: string; + cd4: string; + cd4Date: string; + cd4Percent: string; + cd4PercentDate: string; + ldlValue: string; + ldlDate: string; + enrolledInHiv: boolean; + lastEncDetails: { + startDate: string; + endDate: string; + regimenShortDisplay: string; + regimenLine: string; + regimenLongDisplay: string; + changeReasons: Array; + regimenUuid: string; + current: boolean; + }; +}; + +type TBData = { + tbDiseaseClassification: string; + tbPatientClassification: string; + tbTreatmentNumber: string; + lastTbEncounter: { + startDate: string; + endDate: string; + regimenShortDisplay: string; + regimenLine: string; + regimenLongDisplay: string; + changeReasons: Array; + regimenUuid: string; + current: boolean; + }; + tbDiseaseClassificationDate: String; +}; + +type MCHMotherData = { + hivStatus: string; + hivStatusDate: string; + onHaart: string; + onHaartDate: string; +}; + +export type MCHChildData = { + currentProphylaxisUsed: string; + currentProphylaxisUsedDate: string; + currentFeedingOption: string; + currentFeedingOptionDate: string; + milestonesAttained: string; + milestonesAttainedDate: string; + heiOutcome: string; + heiOutcomeDate: string; + hivStatus: string; + hivStatusDate: string; +}; + +export type ProgramSummary = { + HIV?: HIVData; + TB?: TBData; + mchMother?: MCHMotherData; + mchChild?: MCHChildData; +}; + +export enum ProgramType { + HIV = 'HIV', + TB = 'TB', + TPT = 'TPT', + MCH_MOTHER = 'MCH - Mother Services', + MCH_CHILD = 'MCH - Child Services', + MCHMOTHER = 'mchMother', + MCHCHILD = 'mchChild', +} + +export type PatientSummary = { + reportDate: string; + clinicName: string; + mflCode: string; + patientName: string; + birthDate: string; + age: string; + gender: string; + uniquePatientIdentifier: string; + nationalUniquePatientIdentifier: string; + maritalStatus: string; + height: string; + weight: string; + bmi: string; + oxygenSaturation: string; + pulseRate: string; + bloodPressure: string; + bpDiastolic: string; + lmp: string; + respiratoryRate: string; + dateConfirmedHIVPositive: string; + firstCd4: string; + firstCd4Date: string; + dateEnrolledIntoCare: string; + whoStagingAtEnrollment: string; + caxcScreeningOutcome: string; + stiScreeningOutcome: string; + familyProtection: string; + transferInFacility: string; + patientEntryPoint: string; + patientEntryPointDate: string; + nameOfTreatmentSupporter: string; + relationshipToTreatmentSupporter: string; + transferInDate: string; + contactOfTreatmentSupporter: string; + dateEnrolledInTb: string; + dateCompletedInTb: string; + tbScreeningOutcome: string; + chronicDisease: string; + previousArtStatus: string; + dateStartedArt: string; + whoStageAtArtStart: string; + cd4AtArtStart: string; + heightArtInitiation: string; + firstRegimen: { + startDate: string; + endDate: string; + regimenShortDisplay: string; + regimenLine: string; + regimenLongDisplay: string; + changeReasons: Array; + regimenUuid: string; + current: boolean; + }; + purposeDrugs: string; + purposeDate: string; + iosResults: string; + currentArtRegimen: { + startDate: string; + endDate: string; + regimenShortDisplay: string; + regimenLine: string; + regimenLongDisplay: string; + changeReasons: Array; + regimenUuid: string; + current: boolean; + }; + currentWhoStaging: string; + ctxValue: string; + dapsone: string; + onIpt: string; + allergies: string; + clinicsEnrolled: string; + mostRecentCd4: string; + mostRecentCd4Date: string; + deathDate: string; + nextAppointmentDate: string; + transferOutDate: string; + transferOutFacility: string; + viralLoadValue: string; + viralLoadDate: string; + allCd4CountResults: Array; + allVlResults: vlResults; +}; + +type cd4Results = { + cd4Count: string; + cd4CountDate: string; +}; + +type vlResults = { + value: Array; +}; + +type vl = { + vl?: string; + vlDate?: string; +}; + +export enum RegimenType { + HIV = 'ARV', + TB = 'TB', +} + +export type RegimenEncounter = { + uuid: string; +}; + +export enum Regimen { + RegimenLineConcept = '163104AAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + reasonCodedConcept = '1252AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + reasonNonCodedConcept = '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + standardRegimenConcept = '1193AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + nonStandardRegimenConcept = '1088AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + arvCategoryConcept = '1255AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + tbCategoryConcept = '1268AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + changeRegimenConcept = '1259AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + stopRegimenConcept = '1260AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + startOrRestartConcept = '1256AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', + regimenForm = 'da687480-e197-11e8-9f32-f2801f1b9fd1', + regimenEncounterType = '7dffc392-13e7-11e9-ab14-d663bd873d93', + dateDrugStoppedCon = '1191AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA', +} + +export interface Encounter { + encounterDatetime: Date; + patient: string; + encounterType: string; + location: string; + encounterProviders: Array<{ + provider: string; + encounterRole: string; + }>; + form: string; + obs: Array<{ + concept: string; + value: string | number; + }>; +} +export interface UpdateObs { + obs: Array<{ + concept: string; + value: string | number; + }>; +} diff --git a/packages/esm-care-panel-app/translations/en.json b/packages/esm-care-panel-app/translations/en.json new file mode 100644 index 00000000..8c523f96 --- /dev/null +++ b/packages/esm-care-panel-app/translations/en.json @@ -0,0 +1,131 @@ +{ + "age": "Age", + "artInterruptionReason": "ART interruptions reason", + "birthDate": "Birth date", + "bloodPressure": "Blood pressure", + "bmi": "BMI", + "carePanel": "Care panel", + "carePanelError": "Care panel", + "careProgram": "care program", + "carePrograms": "Care Programs", + "careProgramsEnrollement": "Care panel", + "caxcScreeningOutcome": "Caxc screening", + "cd4AtArtStart": "CD4 at ART start", + "CD4Percentage": "CD4 percentage", + "cd4Trends": "CD4 Trends", + "changeReason": "Change reason", + "changeRegimen": "Change", + "chronicDisease": "Chronic disease", + "clinicalNotes": "Clinical notes", + "clinicalStageART": "Clinical stage at ART", + "clinicianName": "Clinician name", + "clinicianSignature": "Clinician signature", + "clinicName": "Clinic name", + "clinicsEnrolled": "Clinics enrolled", + "currentArtRegimen": "Current Art regimen", + "currentArtRegimenDate": "Current Art regimen date", + "currentFeedingOption": "Current feeding option", + "currentStatus": "Current status", + "Dapsone": "Dapsone", + "date": "Date", + "dateCompletedInTb": "TPT completion date", + "dateConfirmedPositive": "Date confirmed HIV positive", + "dateEnrolledInTb": "TPT start date", + "dateEnrolledToCare": "Date enrolled into care", + "dateFirstCD4": "Date first CD4", + "dateOfEntryPoint": "Date of entry point", + "dateStartedART": "Date started ART", + "deathDate": "Death date", + "deleteRegimen": "Delete Regimen", + "deleteRegimenModalConfirmationText": "Are you sure you want to delete regimen?", + "discard": "Discard", + "discontinue": "Discontinue", + "diseaseClassification": "Disease classification", + "drugAllergies": "Drug allergies", + "edit": "Edit", + "editRegimen": "Edit", + "end": "End", + "EnrollmentDetails": "Enrollment History", + "enrollments": "Program enrollment", + "entryPoint": "Entry point", + "errorCarePrograms": "Care programs", + "errorPatientSummary": "Error loading patient summary", + "errorProgramSummary": "Error loading HIV summary", + "errorRegimenHistory": "Error loading regimen history", + "facilityTransferredFrom": "Facility transferred from", + "familyProtection": "FP method", + "firstCD4": "First CD4", + "gender": "Gender", + "height": "Height", + "heiOutcome": "HEI Outcome", + "hivStatus": "HIV Status", + "initialRegimen": "Initial regimen", + "initialRegimenDate": "Initial regimen date", + "ioHistory": " OI history", + "lastCd4Count": "Last CD4 count", + "lastViralLoad": "Last viral load", + "lastWhoStage": "Last WHO stage", + "lmp": "LMP", + "loading": "Loading", + "loadingDescription": "Loading data...", + "maritalStatus": "Marital status", + "mflCode": "MFL code", + "milestonesAttained": "Milestones Attained", + "mostRecentCD4": "Most recent CD4", + "mostRecentVL": "Most recent VL", + "nationalUniquePatientIdentifier": "National unique patient identifier", + "neverOnArvRegimen": "Never on ARVs", + "neverOnTbRegimen": "Never on TB regimen", + "nextAppointmentDate": " Next appointment", + "none": "None", + "onART": "On ART", + "oxygenSaturation": "Oxygen saturation", + "panelSummary": "Panel summary", + "patientClassification": "Patient classification", + "patientName": "Patient name", + "patientSummary": "Patient summary", + "previousART": "Previous ART", + "print": "Print", + "pulseRate": "Pulse rate", + "purposeDate": "Purpose drugs date", + "purposeDrugs": "Purpose drugs", + "regimen": "Regimen", + "regimenDeleted": "Regimen deleted", + "regimenDeletedError": "Error deleting regimen", + "regimenDeletedSuccessfully": "Regimen successfully", + "regimenError": "Error updating regimen", + "regimenEvent": "Regimen event", + "regimenHistory": "Regimen History", + "regimenLine": "Regimen line", + "regimenStartDate": " Date started regimen", + "regimenUpdated": "Regimen updated", + "regimenUpdatedSuccessfully": "Regimen updated successfully.", + "reportDate": "Report date", + "respiratoryRate": "Respiratory rate", + "restartRegimen": "Restart", + "save": "Save", + "selectReason": "Select Reason", + "selectRegimen": "Select Regimen", + "selectRegimenLine": "Select Regimen Line", + "start": "Start", + "startRegimen": "Start", + "stiScreeningOutcome": "Sti screening", + "stopRegimen": "Stop", + "substitutionWithin1stLineRegimen": "Substitution within 1st line regimen", + "switchTo2ndLineRegimen": "Switch to 2nd line regimen", + "tbScreeningOutcome": "TB screening outcome", + "tpt": "TPT", + "transferInDate": "Transfer in date", + "transferOutDate": "Transfer out date", + "transferOutFacility": "Transfer out facility", + "treatmentSupporterContact": "Treatment Supporter contact", + "treatmentSupporterName": "Treatment supporter name", + "treatmentSupporterRelationship": "Treatment supporter relationship", + "undoRegimen": "Undo", + "uniquePatientIdentifier": "Unique patient identifier", + "validating": "Validating data...", + "viralLoadTrends": "Viral load trends", + "weight": "Weight", + "whoAtEnrollment": "WHO stage at enrollment", + "whoStage": "WHO STAGE" +} diff --git a/packages/esm-care-panel-app/treatmentNumber/en.json b/packages/esm-care-panel-app/treatmentNumber/en.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/packages/esm-care-panel-app/treatmentNumber/en.json @@ -0,0 +1 @@ +{} diff --git a/packages/esm-care-panel-app/tsconfig.json b/packages/esm-care-panel-app/tsconfig.json new file mode 100644 index 00000000..72a7d91b --- /dev/null +++ b/packages/esm-care-panel-app/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["src/**/*"], + "exclude": ["src/**/*.test.tsx", "src/**/*.outdated.tsx"] +} diff --git a/packages/esm-care-panel-app/webpack.config.js b/packages/esm-care-panel-app/webpack.config.js new file mode 100644 index 00000000..2c74029c --- /dev/null +++ b/packages/esm-care-panel-app/webpack.config.js @@ -0,0 +1 @@ +module.exports = require('openmrs/default-webpack-config'); diff --git a/packages/esm-patient-flags-app/src/hooks/usePatientFlags.tsx b/packages/esm-patient-flags-app/src/hooks/usePatientFlags.tsx index 9015fa13..29bfa830 100644 --- a/packages/esm-patient-flags-app/src/hooks/usePatientFlags.tsx +++ b/packages/esm-patient-flags-app/src/hooks/usePatientFlags.tsx @@ -14,7 +14,7 @@ interface PatientFlagsReturnType { * @returns An array of patient identifiers */ export const usePatientFlags = (patientUuid: string): PatientFlagsReturnType => { - const patientFlagsUrl = `/ws/rest/v1/amrscore/flags?patientUuid=${patientUuid}`; + const patientFlagsUrl = `/ws/rest/v1/amrs/flags?patientUuid=${patientUuid}`; const { data, mutate, error, isLoading } = useSWR<{ data: { results: Array } }>( patientFlagsUrl, openmrsFetch, diff --git a/packages/esm-patient-flags-app/src/hooks/usePatientId.tsx b/packages/esm-patient-flags-app/src/hooks/usePatientId.tsx index 6763a257..8756e698 100644 --- a/packages/esm-patient-flags-app/src/hooks/usePatientId.tsx +++ b/packages/esm-patient-flags-app/src/hooks/usePatientId.tsx @@ -3,7 +3,7 @@ import useSWR from 'swr'; export const usePatientId = (patientUuid: string) => { const { isLoading, data, error } = useSWR<{ data: { results: { patientId: string; age: number } } }>( - `/ws/rest/v1/amrscore/patient?patientUuid=${patientUuid}`, + `/ws/rest/v1/amrs/patient?patientUuid=${patientUuid}`, openmrsFetch, ); return { patient: data?.data?.results, error, isLoading }; diff --git a/packages/esm-patient-flags-app/src/routes.json b/packages/esm-patient-flags-app/src/routes.json index f126a3c1..ea91e9c3 100644 --- a/packages/esm-patient-flags-app/src/routes.json +++ b/packages/esm-patient-flags-app/src/routes.json @@ -1,7 +1,7 @@ { "$schema": "https://json.openmrs.org/routes.schema.json", "backendDependencies": { - "amrscore": "^1.0.0-SNAPSHOT" + "amrs": "^1.0.0-SNAPSHOT" }, "pages": [], "extensions": [ diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/assets/counties.json b/packages/esm-patient-registration-app/src/patient-verification-HIE/assets/counties.json new file mode 100644 index 00000000..c0ca4e0e --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/assets/counties.json @@ -0,0 +1,236 @@ +[ + { + "name": "Mombasa", + "code": 1, + "capital": "Mombasa City" + }, + { + "name": "Kwale", + "code": 2, + "capital": "Kwale" + }, + { + "name": "Kilifi", + "code": 3, + "capital": "Kilifi" + }, + { + "name": "Tana River", + "code": 4, + "capital": "Hola" + }, + { + "name": "Lamu", + "code": 5, + "capital": "Lamu" + }, + { + "name": "Taita taveta", + "code": 6, + "capital": "Voi" + }, + { + "name": "Garissa", + "code": 7, + "capital": "Garissa" + }, + { + "name": "Wajir", + "code": 8, + "capital": "Wajir" + }, + { + "name": "Mandera", + "code": 9, + "capital": "Mandera" + }, + { + "name": "Marsabit", + "code": 10, + "capital": "Marsabit" + }, + { + "name": "Isiolo", + "code": 11, + "capital": "Isiolo" + }, + { + "name": "Meru", + "code": 12, + "capital": "Meru" + }, + { + "name": "Tharaka nithi", + "code": 13, + "capital": "Chuka" + }, + { + "name": "Embu", + "code": 14, + "capital": "Embu" + }, + { + "name": "Kitui", + "code": 15, + "capital": "Kitui" + }, + { + "name": "Machakos", + "code": 16, + "capital": "Machakos" + }, + { + "name": "Makueni", + "code": 17, + "capital": "Wote" + }, + { + "name": "Nyandarua", + "code": 18, + "capital": "Ol Kalou" + }, + { + "name": "Nyeri", + "code": 19, + "capital": "Nyeri" + }, + { + "name": "Kirinyaga", + "code": 20, + "capital": "Kerugoya/Kutus" + }, + { + "name": "Murang'a", + "code": 21, + "capital": "Murang'a" + }, + { + "name": "Kiambu", + "code": 22, + "capital": "Kiambu" + }, + { + "name": "Turkana", + "code": 23, + "capital": "Lodwar" + }, + { + "name": "West pokot", + "code": 24, + "capital": "Kapenguria" + }, + { + "name": "Samburu", + "code": 25, + "capital": "Maralal" + }, + { + "name": "Trans nzoia", + "code": 26, + "capital": "Kitale" + }, + { + "name": "Uasin gishu", + "code": 27, + "capital": "Eldoret" + }, + { + "name": "Elgeyo marakwet", + "code": 28, + "capital": "Iten" + }, + { + "name": "Nandi", + "code": 29, + "capital": "Kapsabet" + }, + { + "name": "Baringo", + "code": 30, + "capital": "Kabarnet" + }, + { + "name": "Laikipia", + "code": 31, + "capital": "Rumuruti" + }, + { + "name": "Nakuru", + "code": 32, + "capital": "Nakuru" + }, + { + "name": "Narok", + "code": 33, + "capital": "Narok" + }, + { + "name": "Kajiado", + "code": 34 + }, + { + "name": "Kericho", + "code": 35, + "capital": "Kericho" + }, + { + "name": "Bomet", + "code": 36, + "capital": "Bomet" + }, + { + "name": "Kakamega", + "code": 37, + "capital": "Kakamega" + }, + { + "name": "Vihiga", + "code": 38, + "capital": "Vihiga" + }, + { + "name": "Bungoma", + "code": 39, + "capital": "Bungoma" + }, + { + "name": "Busia", + "code": 40, + "capital": "Busia" + }, + { + "name": "Siaya", + "code": 41, + "capital": "Siaya" + }, + { + "name": "Kisumu", + "code": 42, + "capital": "Kisumu" + }, + { + "name": "Homa bay", + "code": 43, + "capital": "Homa Bay" + }, + { + "name": "Migori", + "code": 44, + "capital": "Migori" + }, + { + "name": "Kisii", + "code": 45, + "capital": "Kisii" + }, + { + "name": "Nyamira", + "code": 46, + "capital": "Nyamira" + }, + { + "name": "Nairobi", + "code": 47, + "capital": "Nairobi City" + } +] \ No newline at end of file diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/assets/verification-assets.ts b/packages/esm-patient-registration-app/src/patient-verification-HIE/assets/verification-assets.ts new file mode 100644 index 00000000..630c9a90 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/assets/verification-assets.ts @@ -0,0 +1,11 @@ +export const countries = [ + { name: 'Kenya', initials: 'KE' }, + { name: 'Uganda', initials: 'UG' }, + { name: 'Tanzania', initials: 'TZ' }, +]; + +export const verificationIdentifierTypes = [ + { name: 'National ID', value: 'national-id' }, + { name: 'Passport', value: 'passport' }, + { name: 'Birth certificate number', value: 'birth-certificate' }, +]; diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-hook.tsx b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-hook.tsx new file mode 100644 index 00000000..ab725ce0 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-hook.tsx @@ -0,0 +1,181 @@ +import { type FetchResponse, openmrsFetch, showNotification, showToast, showSnackbar } from '@openmrs/esm-framework'; +import { generateNUPIPayload, handleClientRegistryResponse } from './patient-verification-utils'; +import useSWR from 'swr'; +import useSWRImmutable from 'swr/immutable'; +import { + type ConceptAnswers, + type ConceptResponse, + type FormValues, +} from '../patient-registration/patient-registration.types'; + +export function searchClientRegistry( + identifierType: string, + searchTerm: string, + token: string, + countryCode: string = 'KE', +) { + const url = `https://afyakenyaapi.health.go.ke/partners/registry/search/${countryCode}/${identifierType}/${searchTerm}`; + return fetch(url, { headers: { Authorization: `Bearer ${token}` } }).then((r) => r.json()); +} + +export function savePatientToClientRegistry(formValues: FormValues) { + const createdRegistryPatient = generateNUPIPayload(formValues); + return fetch(`https://afyakenyaapi.health.go.ke/partners/registry`, { + headers: { Authorization: `Bearer ${formValues.token}`, 'Content-Type': 'application/json' }, + method: 'POST', + body: JSON.stringify(createdRegistryPatient), + }); +} + +export async function handleSavePatientToClientRegistry( + formValues: FormValues, + setValues: (values: FormValues, shouldValidate?: boolean) => void, + inEditMode: boolean, +) { + const mode = inEditMode ? 'edit' : 'new'; + switch (mode) { + case 'edit': { + try { + const searchResponse = await searchClientRegistry( + 'national-id', + formValues.identifiers['nationalId'].identifierValue, + formValues.token, + ); + + // if client does not exists post client to registry + if (searchResponse?.clientExists === false) { + postToRegistry(formValues, setValues); + } + } catch (error) { + showSnackbar({ + title: 'Client registry error', + subtitle: `${error}`, + timeoutInMs: 10000, + kind: 'error', + isLowContrast: true, + }); + } + return; + } + case 'new': { + postToRegistry(formValues, setValues); + } + } +} + +export function useConceptAnswers(conceptUuid: string): { data: Array; isLoading: boolean } { + const { data, error, isLoading } = useSWR, Error>( + `/ws/rest/v1/concept/${conceptUuid}`, + openmrsFetch, + ); + if (error) { + showToast({ + title: error.name, + description: error.message, + kind: 'error', + }); + } + return { data: data?.data?.answers ?? [], isLoading }; +} + +const urlencoded = new URLSearchParams(); +// urlencoded.append('client_id', 'palladium.partner.client'); +// urlencoded.append('client_secret', '28f95b2a'); +// urlencoded.append('grant_type', 'client_credentials'); +// urlencoded.append('scope', 'DHP.Gateway DHP.Partners'); + +urlencoded.append('client_id', 'ampath.partner.client'); +urlencoded.append('client_secret', '3ae8a7f6'); +urlencoded.append('grant_type', 'client_credentials'); +urlencoded.append('scope', 'DHP.Gateway DHP.Partners'); + +const swrFetcher = async (url) => { + const res = await fetch(url, { + method: 'POST', + body: urlencoded, + redirect: 'follow', + }); + + // If the status code is not in the range 200-299, + // we still try to parse and throw it. + if (!res.ok) { + const error = new Error('An error occurred while fetching the data.') as any; + // Attach extra info to the error object. + error.info = await res.json(); + error.status = res.status; + throw error; + } + + return res.json(); +}; + +export function useGlobalProperties() { + const { data, isLoading, error } = useSWRImmutable( + `https://afyakenyaidentityapi.health.go.ke/connect/token`, + swrFetcher, + { refreshInterval: 864000 }, + ); + return { data: data, isLoading, error }; +} + +async function postToRegistry( + formValues: FormValues, + setValues: (values: FormValues, shouldValidate?: boolean) => void, +) { + try { + const clientRegistryResponse = await savePatientToClientRegistry(formValues); + if (clientRegistryResponse.ok) { + const savedValues = await clientRegistryResponse.json(); + const nupiIdentifier = { + ['nationalUniquePatientIdentifier']: { + identifierTypeUuid: 'cba702b9-4664-4b43-83f1-9ab473cbd64d', + identifierName: 'National Unique Patient Identifier (NUPI)', + identifierValue: savedValues['clientNumber'], + initialValue: savedValues['clientNumber'], + identifierUuid: undefined, + selectedSource: { uuid: '', name: '' }, + preferred: false, + required: false, + }, + }; + setValues({ ...formValues, identifiers: { ...formValues.identifiers, ...nupiIdentifier } }); + showToast({ + title: 'Posted patient to client registry successfully', + description: `The patient has been saved to client registry`, + kind: 'success', + }); + } else { + const responseError = await clientRegistryResponse.json(); + const errorMessage = Object.values(responseError.errors ?? {}) + .map((error: any) => error.join()) + .toString(); + setValues({ + ...formValues, + attributes: { + ...formValues.attributes, + ['5553d509-f03a-4982-8e16-0d6f3d70fb8b']: 'Failed validation', + ['d8f4d295-3f31-47ec-b377-825bd38820b2']: 'Failed', + }, + }); + showNotification({ + title: responseError.title, + description: errorMessage, + kind: 'warning', + millis: 150000, + }); + } + } catch (error) { + showNotification({ kind: 'error', title: 'NUPI Post failed', description: JSON.stringify(error) }); + } +} + +export const useFacilityName = (facilityCode) => { + const apiUrl = `/ws/rest/v1/amrs/facilityName?facilityCode=${facilityCode}`; + const { data, error, isLoading } = useSWRImmutable(apiUrl, openmrsFetch); + + return { + facilityName: data ? data?.data : null, + isLoading: isLoading, + isError: error, + }; +}; diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-utils.ts b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-utils.ts new file mode 100644 index 00000000..c57e72fb --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification-utils.ts @@ -0,0 +1,179 @@ +import { showModal } from '@openmrs/esm-framework'; +import { type FormikProps } from 'formik'; +import { type ClientRegistryPatient, type RegistryPatient } from './verification-types'; +import counties from './assets/counties.json'; +import { type FormValues } from '../patient-registration/patient-registration.types'; +import { capitalize } from 'lodash-es'; + +export function handleClientRegistryResponse( + clientResponse: ClientRegistryPatient, + props: FormikProps, + searchTerm: string, +) { + if (clientResponse?.clientExists === false) { + const nupiIdentifiers = { + ['nationalId']: { + initialValue: searchTerm, + identifierUuid: undefined, + selectedSource: { uuid: '', name: '' }, + preferred: false, + required: false, + identifierTypeUuid: '58a47054-1359-11df-a1f1-0026b9348838', + identifierName: 'Kenyan National ID Number', + identifierValue: searchTerm, + }, + }; + const dispose = showModal('empty-client-registry-modal', { + onConfirm: () => { + props.setValues({ ...props.values, identifiers: { ...props.values.identifiers, ...nupiIdentifiers } }); + dispose(); + }, + close: () => dispose(), + }); + } + + if (clientResponse?.clientExists) { + const { + client: { + middleName, + lastName, + firstName, + contact, + country, + countyOfBirth, + residence, + identifications, + gender, + dateOfBirth, + isAlive, + clientNumber, + educationLevel, + occupation, + maritalStatus, + }, + } = clientResponse; + + const nupiIdentifiers = { + ['nationalId']: { + initialValue: identifications !== undefined && identifications[0]?.identificationNumber, + identifierUuid: undefined, + selectedSource: { uuid: '', name: '' }, + preferred: false, + required: false, + identifierTypeUuid: '58a47054-1359-11df-a1f1-0026b9348838', + identifierName: 'Kenyan National ID Number', + identifierValue: identifications !== undefined && identifications[0]?.identificationNumber, + }, + + ['nationalUniquePatientIdentifier']: { + identifierTypeUuid: 'cba702b9-4664-4b43-83f1-9ab473cbd64d', + identifierName: 'National Unique Patient Identifier (NUPI)', + identifierValue: clientNumber, + initialValue: clientNumber, + identifierUuid: undefined, + selectedSource: { uuid: '', name: '' }, + preferred: false, + required: false, + }, + }; + + const dispose = showModal('confirm-client-registry-modal', { + onConfirm: () => { + props.setValues({ + ...props.values, + familyName: lastName, + middleName: middleName, + givenName: firstName, + gender: clientResponse.client.gender, + birthdate: new Date(dateOfBirth), + isDead: !isAlive, + attributes: { + '72a759a8-1359-11df-a1f1-0026b9348838': contact?.primaryPhone, + 'b0a08406-09c0-4f8b-8cb5-b22b6d4a8e46': contact?.secondaryPhone, + '2f65dbcb-3e58-45a3-8be7-fd1dc9aa0faa': contact?.emailAddress ?? '', + }, + address: { + address1: residence?.address, + address2: '', + address4: capitalize(residence?.ward ?? ''), + cityVillage: residence?.village, + stateProvince: capitalize(residence?.subCounty ?? ''), + countyDistrict: counties.find((county) => county.code === parseInt(residence?.county))?.name, + country: 'Kenya', + postalCode: residence?.address, + }, + identifiers: { ...props.values.identifiers, ...nupiIdentifiers }, + obs: { + 'a899a9f2-1350-11df-a1f1-0026b9348838': + props.values.concepts.find((concept) => + concept.display?.toLowerCase()?.includes(clientResponse.client.maritalStatus?.toLowerCase()), + )?.uuid ?? '', + 'a89e48ae-1350-11df-a1f1-0026b9348838': + props.values.concepts.find((concept) => + concept.display?.toLowerCase()?.includes(clientResponse.client.educationLevel?.toLowerCase()), + )?.uuid ?? '', + 'a8a0a00e-1350-11df-a1f1-0026b9348838': + clientResponse.client.occupation === undefined || clientResponse.client.occupation === null + ? 'a899e0ac-1350-11df-a1f1-0026b9348838' + : props.values.concepts.find( + (concept) => concept.display?.toLowerCase() === clientResponse.client.occupation?.toLowerCase(), + )?.uuid ?? 'a8aaf3e2-1350-11df-a1f1-0026b9348838', + }, + }); + dispose(); + }, + close: () => dispose(), + patient: clientResponse.client, + }); + } +} + +export function generateNUPIPayload(formValues: FormValues): RegistryPatient { + const educationLevel = formValues.concepts.find( + (concept) => concept.uuid === formValues.obs['a89e48ae-1350-11df-a1f1-0026b9348838'], + ); + const occupation = formValues.concepts.find( + (concept) => concept.uuid === formValues.obs['a8a0a00e-1350-11df-a1f1-0026b9348838'], + ); + const maritalStatus = formValues.concepts.find( + (concept) => concept.uuid === formValues.obs['a899a9f2-1350-11df-a1f1-0026b9348838'], + ); + + let createRegistryPatient: RegistryPatient = { + firstName: formValues?.givenName, + middleName: formValues?.middleName, + lastName: formValues?.familyName, + gender: formValues?.gender === 'Male' ? 'male' : 'female', + dateOfBirth: new Date(formValues.birthdate).toISOString(), + isAlive: !formValues.isDead, + residence: { + county: `0${counties.find((county) => county.name.includes(formValues.address['countyDistrict']))?.code}`, + subCounty: formValues.address['stateProvince']?.toLocaleLowerCase(), + ward: formValues.address['address4']?.toLocaleLowerCase(), + village: formValues.address['cityVillage'], + landmark: formValues.address['address2'], + address: formValues.address['postalCode'], + }, + nextOfKins: [], + contact: { + primaryPhone: formValues.attributes['72a759a8-1359-11df-a1f1-0026b9348838'], + secondaryPhone: formValues.attributes['b0a08406-09c0-4f8b-8cb5-b22b6d4a8e46'], + emailAddress: formValues.attributes['2f65dbcb-3e58-45a3-8be7-fd1dc9aa0faa'], + }, + country: 'KE', + countyOfBirth: `0${counties.find((county) => county.name.includes(formValues.address['countyDistrict']))?.code}`, + educationLevel: educationLevel?.display?.toLowerCase() ?? '', + religion: '', + occupation: occupation?.display?.toLowerCase() ?? '', + maritalStatus: maritalStatus?.display?.toLowerCase() ?? '', + originFacilityKmflCode: '', + nascopCCCNumber: '', + identifications: [ + { + identificationType: 'national-id', + identificationNumber: formValues.identifiers['nationalId']?.identifierValue, + }, + ], + }; + return createRegistryPatient; +} diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.component.tsx b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.component.tsx new file mode 100644 index 00000000..acf83222 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.component.tsx @@ -0,0 +1,124 @@ +import React, { useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Tile, ComboBox, Layer, Button, Search, InlineLoading, InlineNotification } from '@carbon/react'; +import styles from './patient-verification.scss'; +import { countries, verificationIdentifierTypes } from './assets/verification-assets'; +import { searchClientRegistry, useGlobalProperties } from './patient-verification-hook'; +import { showSnackbar, showToast } from '@openmrs/esm-framework'; +import { handleClientRegistryResponse } from './patient-verification-utils'; +import { type FormikProps } from 'formik'; +import { type FormValues } from '../patient-registration/patient-registration.types'; + +interface PatientVerificationProps { + props: FormikProps; + setInitialFormValues: React.Dispatch; +} + +const PatientVerification: React.FC = ({ props }) => { + const { t } = useTranslation(); + const { data, isLoading, error } = useGlobalProperties(); + const [verificationCriteria, setVerificationCriteria] = useState({ + searchTerm: '', + identifierType: '', + countryCode: 'KE', + }); + const [isLoadingSearch, setIsLoadingSearch] = useState(false); + + const handleSearch = async () => { + setIsLoadingSearch(true); + try { + const clientRegistryResponse = await searchClientRegistry( + verificationCriteria.identifierType, + verificationCriteria.searchTerm, + props.values.token, + verificationCriteria.countryCode, + ); + setIsLoadingSearch(false); + + handleClientRegistryResponse(clientRegistryResponse, props, verificationCriteria.searchTerm); + } catch (error) { + showSnackbar({ + title: 'Client registry error', + subtitle: `Please reload the registration page and re-try again, if the issue persist contact system administrator`, + timeoutInMs: 10000, + kind: 'error', + isLowContrast: true, + }); + setIsLoadingSearch(false); + } + }; + + return ( +
+

+ {t('clientVerificationWithClientRegistry', 'Client verification with client registry')} +

+
+ + {isLoading && } + + {error && ( + + )} + + + item?.name ?? ''} + label="Select country" + titleText={t('selectCountry', 'Select country')} + initialSelectedItem={countries[0]} + onChange={({ selectedItem }) => + setVerificationCriteria({ ...verificationCriteria, countryCode: selectedItem?.initials }) + } + /> + + + item?.name ?? ''} + label="Select identifier type" + titleText={t('selectIdentifierType', 'Select identifier type')} + onChange={({ selectedItem }) => + setVerificationCriteria({ ...verificationCriteria, identifierType: selectedItem?.value }) + } + /> + + + setVerificationCriteria({ ...verificationCriteria, searchTerm: event.target.value })} + /> + + {!isLoadingSearch ? ( + + ) : ( + + )} + +
+
+ ); +}; + +export default PatientVerification; diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.scss b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.scss new file mode 100644 index 00000000..044c0356 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/patient-verification.scss @@ -0,0 +1,25 @@ +@use '@carbon/colors'; +@import '../patient-registration/patient-registration.scss'; + +/* Desktop */ +:global(.omrs-breakpoint-gt-tablet) { + .verificationWrapper { + display: grid; + grid-template-columns: 1fr 1fr 1fr 1fr; + column-gap: 0.325rem; + align-items: flex-end; + } +} + +/* Tablet */ +:global(.omrs-breakpoint-lt-desktop) { + .verificationWrapper { + row-gap: 0.5rem; + display: flex; + flex-direction: column; + } +} + +.errorWrapper { + margin: 0 0 1rem 0; +} diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/confirm-prompt.component.tsx b/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/confirm-prompt.component.tsx new file mode 100644 index 00000000..f3634b1f --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/confirm-prompt.component.tsx @@ -0,0 +1,72 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@carbon/react'; +import { age, ExtensionSlot, formatDate } from '@openmrs/esm-framework'; +import capitalize from 'lodash-es/capitalize'; +import { useFacilityName } from '../patient-verification-hook'; + +const PatientInfo: React.FC<{ label: string; value: string }> = ({ label, value }) => { + return ( +
+ {label} + {value} +
+ ); +}; + +interface ConfirmPromptProps { + onConfirm: void; + close: void; + patient: any; +} + +const ConfirmPrompt: React.FC = ({ close, onConfirm, patient }) => { + const { t } = useTranslation(); + return ( + <> +
+

+ {t('clientRegistryEmpty', `Patient ${patient?.firstName} ${patient?.lastName} found`)} +

+
+
+

+ {t( + 'patientDetailsFound', + 'Patient information found in the registry, do you want to use the information to continue with registration?', + )} +

+
+ +
+ + + + + + + +
+
+
+
+ + +
+ + ); +}; + +export default ConfirmPrompt; diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/empty-prompt.component.tsx b/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/empty-prompt.component.tsx new file mode 100644 index 00000000..528cae6f --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-modal/empty-prompt.component.tsx @@ -0,0 +1,35 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button } from '@carbon/react'; + +interface EmptyPromptProps { + onConfirm: void; + close: void; +} + +const EmptyPrompt: React.FC = ({ close, onConfirm }) => { + const { t } = useTranslation(); + return ( + <> +
+

{t('clientRegistryEmpty', 'Create & Post Patient')}

+
+
+

+ {t( + 'patientNotFound', + 'The patient records could not be found in Client registry, do you want to continue to create and post patient to registry', + )} +

+
+
+ + +
+ + ); +}; + +export default EmptyPrompt; diff --git a/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-types.ts b/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-types.ts new file mode 100644 index 00000000..e3626487 --- /dev/null +++ b/packages/esm-patient-registration-app/src/patient-verification-HIE/verification-types.ts @@ -0,0 +1,50 @@ +export interface ClientIdentification { + identificationType: string; + identificationNumber: string; +} + +interface ClientContact { + primaryPhone: string; + secondaryPhone?: string; + emailAddress?: string; +} + +export interface ClientRegistryPatient { + clientExists: boolean; + client?: RegistryPatient; +} + +export interface RegistryPatient { + clientNumber?: string; + firstName: string; + middleName: string; + lastName: string; + dateOfBirth: string; + maritalStatus?: string; + gender: string; + occupation?: string; + religion?: string; + educationLevel?: string; + country: string; + countyOfBirth?: string; + isAlive: boolean; + originFacilityKmflCode?: string; + isOnART?: string; + nascopCCCNumber?: string; + residence: { + county: string; + subCounty: string; + ward: string; + village: string; + landmark: string; + address: string; + }; + identifications: Array; + contact: ClientContact; + nextOfKins: Array<{ + name: string; + relationship: string; + residence: string; + contact: ClientContact; + }>; +} diff --git a/packages/esm-patient-registration-app/src/patient-verification/patient-verification-hook.tsx b/packages/esm-patient-registration-app/src/patient-verification/patient-verification-hook.tsx index 875c725e..ab725ce0 100644 --- a/packages/esm-patient-registration-app/src/patient-verification/patient-verification-hook.tsx +++ b/packages/esm-patient-registration-app/src/patient-verification/patient-verification-hook.tsx @@ -170,7 +170,7 @@ async function postToRegistry( } export const useFacilityName = (facilityCode) => { - const apiUrl = `/ws/rest/v1/amrscore/facilityName?facilityCode=${facilityCode}`; + const apiUrl = `/ws/rest/v1/amrs/facilityName?facilityCode=${facilityCode}`; const { data, error, isLoading } = useSWRImmutable(apiUrl, openmrsFetch); return { diff --git a/packages/esm-preappointment-app/src/pre-appointments/pre-appointment.resource.tsx b/packages/esm-preappointment-app/src/pre-appointments/pre-appointment.resource.tsx index c9a19a7b..9b17cbcf 100644 --- a/packages/esm-preappointment-app/src/pre-appointments/pre-appointment.resource.tsx +++ b/packages/esm-preappointment-app/src/pre-appointments/pre-appointment.resource.tsx @@ -28,12 +28,12 @@ const fetcher = async (url) => { }; export const usePreAppointments = (locationUuid: string, yearWeek: any, successCode?: any) => { - let url = `/ws/rest/v1/amrscore/preappointment?locationUUID=${locationUuid}&yearWeek=${yearWeek?.id}`; + let url = `/ws/rest/v1/amrs/preappointment?locationUUID=${locationUuid}&yearWeek=${yearWeek?.id}`; if (successCode.id !== '' && successCode) { url += successCode.id; } - //const patientFlagsUrl = `/ws/rest/v1/amrscore/preappointment?locationUuids=${locationUuid}&yearWeek=${yearWeek?.id}`; + //const patientFlagsUrl = `/ws/rest/v1/amrs/preappointment?locationUuids=${locationUuid}&yearWeek=${yearWeek?.id}`; //const { data, mutate, error, isLoading, isValidating } = useSWR(patientFlagsUrl, openmrsFetch); const { data, error, isLoading, isValidating } = useSWR(url, fetcher); // const { data, error, isLoading, isValidating } = useSWR(url, fetcher); diff --git a/packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.scss b/packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.scss new file mode 100644 index 00000000..103ff2b1 --- /dev/null +++ b/packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.scss @@ -0,0 +1,23 @@ +.table { + width: 100%; + margin: 20px; + border-collapse: collapse; + + .title { + font-size: 50; + text-align: center; + } + + th, + td { + border: 1px solid black; + padding: 8px; + text-align: center; + vertical-align: top; + font-weight: bold; + } + + // .row-letters { + // background-color: lightgray; + // } +} diff --git a/packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.tsx b/packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.tsx new file mode 100644 index 00000000..3e7aae47 --- /dev/null +++ b/packages/esm-report-app/src/registers/Project_Beyond/PBC/Project_Beyond_Clients.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import styles from './Project_Beyond_Clients.scss'; +import { TableContainer, TableHead, TableRow, TableHeader, Table, TableBody, TableCell } from '@carbon/react'; + +const DefaulterTracingRegister: React.FC<{ reportData: any }> = ({ reportData }) => { + return ( + + + + + +
Defaulter Tracing Register
+
+
+ Enter all current clients that did not come for their scheduled appointment +
+
+
+
+ + +
Section A
+
+ + +
Section B
+
+ +
+ + +
Client Tracking Information
+
+ + +
Defaulter Tracing Outcomes
+
+ +
+ + a + b + c + d + e + + f + g + h + i + + + + S/NO. + + + Client
Identification
Number/ + {/*
Unique Patient
Number (NUPI) */} +
+ + Client Name
(First, Middle, Last) +
+ + Village/Estate/
+ Landmark +
+ + Telephone number + + + + Date of missed
appointment +
+ + Return to care/
Dead/
Transfered/
Stopped Treatment/
On follow up +
+ + Date of outcome
(dd/mm/yy) +
+ + Remarks + +
+ + + Unique Patient
Number (NUPI) +
+
+
+ + {reportData && reportData.length > 0 ? ( + reportData.map((item, i) => ( + + + + {i} + + + {item.client_id_number} + + + {item.full_names} + + + {item.village_estate_landmark} + + + {item.telephone_no} + + + {item.date_of_missed_appointment} + + + {item.defaulter_tracing_outcomes} + + + {item.date_of_outcome} + + + {item.remarks} + + + + + {item.nupi} + + + + )) + ) : ( + + No data available + + )} + +
+
+ ); +}; + +export default DefaulterTracingRegister; diff --git a/packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.scss b/packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.scss new file mode 100644 index 00000000..103ff2b1 --- /dev/null +++ b/packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.scss @@ -0,0 +1,23 @@ +.table { + width: 100%; + margin: 20px; + border-collapse: collapse; + + .title { + font-size: 50; + text-align: center; + } + + th, + td { + border: 1px solid black; + padding: 8px; + text-align: center; + vertical-align: top; + font-weight: bold; + } + + // .row-letters { + // background-color: lightgray; + // } +} diff --git a/packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.tsx b/packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.tsx new file mode 100644 index 00000000..48a4cfef --- /dev/null +++ b/packages/esm-report-app/src/registers/Project_Beyond/PBD/Project_Beyond_Deliveries.tsx @@ -0,0 +1,141 @@ +import React from 'react'; +import styles from './Project_Beyond_Deliveries.scss'; +import { TableContainer, TableHead, TableRow, TableHeader, Table, TableBody, TableCell } from '@carbon/react'; + +const DefaulterTracingRegister: React.FC<{ reportData: any }> = ({ reportData }) => { + return ( + + + + + +
Defaulter Tracing Register
+
+
+ Enter all current clients that did not come for their scheduled appointment +
+
+
+
+ + +
Section A
+
+ + +
Section B
+
+ +
+ + +
Client Tracking Information
+
+ + +
Defaulter Tracing Outcomes
+
+ +
+ + a + b + c + d + e + + f + g + h + i + + + + S/NO. + + + Client
Identification
Number/ + {/*
Unique Patient
Number (NUPI) */} +
+ + Client Name
(First, Middle, Last) +
+ + Village/Estate/
+ Landmark +
+ + Telephone number + + + + Date of missed
appointment +
+ + Return to care/
Dead/
Transfered/
Stopped Treatment/
On follow up +
+ + Date of outcome
(dd/mm/yy) +
+ + Remarks + +
+ + + Unique Patient
Number (NUPI) +
+
+
+ + {reportData && reportData.length > 0 ? ( + reportData.map((item, i) => ( + + + + {i} + + + {item.client_id_number} + + + {item.full_names} + + + {item.village_estate_landmark} + + + {item.telephone_no} + + + {item.date_of_missed_appointment} + + + {item.defaulter_tracing_outcomes} + + + {item.date_of_outcome} + + + {item.remarks} + + + + + {item.nupi} + + + + )) + ) : ( + + No data available + + )} + +
+
+ ); +}; + +export default DefaulterTracingRegister; diff --git a/packages/esm-version-app/src/about/about.component.tsx b/packages/esm-version-app/src/about/about.component.tsx index b64debd0..5fb75943 100644 --- a/packages/esm-version-app/src/about/about.component.tsx +++ b/packages/esm-version-app/src/about/about.component.tsx @@ -10,7 +10,7 @@ interface AboutProps {} const About: React.FC = () => { const { modules, isLoading } = useModules(); - const AMRS = modules.find(({ uuid }) => uuid === 'amrscore'); + const AMRS = modules.find(({ uuid }) => uuid === 'amrs'); //const { mflCodeResource } = useSystemSetting('facility.mflcode'); //const mflCode = mflCodeResource ? `(${mflCodeResource?.value ?? ''})` : ''; const facilityName = useDefaultFacility(); diff --git a/packages/esm-version-app/src/routes.json b/packages/esm-version-app/src/routes.json index 5253a6fe..1a9cf76e 100644 --- a/packages/esm-version-app/src/routes.json +++ b/packages/esm-version-app/src/routes.json @@ -1,7 +1,7 @@ { "$schema": "https://json.openmrs.org/routes.schema.json", "backendDependencies": { - "amrscore": "^1.0.0", + "amrs": "^1.0.0", "amrsreporting": "^1.0.0" }, "pages": [ diff --git a/yarn.lock b/yarn.lock index dfd2d7f9..f932f451 100644 --- a/yarn.lock +++ b/yarn.lock @@ -22,7 +22,7 @@ __metadata: "@carbon/styles": "npm:^1.58.0" "@hookform/resolvers": "npm:^3.3.1" "@ohri/openmrs-esm-ohri-commons-lib": "npm:next" - "@openmrs/esm-framework": "npm:^5.6.1-pre.1979" + "@openmrs/esm-framework": "npm:^5.6.1-pre.2069" "@openmrs/esm-patient-common-lib": "npm:next" "@playwright/test": "npm:1.40.1" "@swc/core": "npm:^1.2.165" @@ -64,7 +64,7 @@ __metadata: jspdf: "npm:^2.5.1" lint-staged: "npm:^15.2.1" moment: "npm:^2.30.1" - openmrs: "npm:^5.6.1-pre.1979" + openmrs: "npm:^5.6.1-pre.2069" pdf-lib: "npm:^1.17.1" prettier: "npm:^3.1.1" react: "npm:^18.1.0" @@ -120,6 +120,23 @@ __metadata: languageName: unknown linkType: soft +"@ampath/esm-care-panel-app@workspace:packages/esm-care-panel-app": + version: 0.0.0-use.local + resolution: "@ampath/esm-care-panel-app@workspace:packages/esm-care-panel-app" + dependencies: + "@carbon/react": "npm:^1.42.1" + lodash-es: "npm:^4.17.15" + react-to-print: "npm:^2.14.13" + webpack: "npm:^5.74.0" + peerDependencies: + "@openmrs/esm-framework": 5.x + react: ^18.1.0 + react-i18next: 11.x + react-router-dom: 6.x + swr: 2.x + languageName: unknown + linkType: soft + "@ampath/esm-hts-app@workspace:packages/esm-hts-app": version: 0.0.0-use.local resolution: "@ampath/esm-hts-app@workspace:packages/esm-hts-app" @@ -3070,9 +3087,9 @@ __metadata: languageName: node linkType: hard -"@openmrs/esm-api@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-api@npm:5.6.1-pre.1979" +"@openmrs/esm-api@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-api@npm:5.6.1-pre.2069" dependencies: "@types/fhir": "npm:0.0.31" lodash-es: "npm:^4.17.21" @@ -3081,17 +3098,17 @@ __metadata: "@openmrs/esm-error-handling": 5.x "@openmrs/esm-navigation": 5.x "@openmrs/esm-offline": 5.x - checksum: 10/4b98a04bcdc6e32dc99abb1ddc722a636a6f5b896961ea974e53d417e6a248083e7dd9ef3feae2f14f09586bbb7ee5b603afb20a8ecfb98a8360d2fd569ad5ba + checksum: 10/dd1cfe608ee7335d2ba20c0fb8af258f8ccd5623d126617795d64e8f034e6ff79a9bcc9fa5257b813b2e7b69c8d3b6cdbd1ba734a2f9bd487b3e7a6b389720bd languageName: node linkType: hard -"@openmrs/esm-app-shell@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-app-shell@npm:5.6.1-pre.1979" +"@openmrs/esm-app-shell@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-app-shell@npm:5.6.1-pre.2069" dependencies: "@carbon/react": "npm:~1.37.0" - "@openmrs/esm-framework": "npm:5.6.1-pre.1979" - "@openmrs/esm-styleguide": "npm:5.6.1-pre.1979" + "@openmrs/esm-framework": "npm:5.6.1-pre.2069" + "@openmrs/esm-styleguide": "npm:5.6.1-pre.2069" dayjs: "npm:^1.10.4" dexie: "npm:^3.0.3" html-webpack-plugin: "npm:^5.5.0" @@ -3116,57 +3133,57 @@ __metadata: workbox-strategies: "npm:^6.1.5" workbox-webpack-plugin: "npm:^6.1.5" workbox-window: "npm:^6.1.5" - checksum: 10/d8179abde69f06024de5a55ce6a8947d14764c3b7c54fc50508b20bf751beeeed8204afb34d242da88cc2363ce415dd04b976b86294b2d677bb1096046bd50c5 + checksum: 10/690cd620a8b2aca3621fc3deca6d6a5c8a71805ebb7b9f23ad4d08af66ae3081d9e5e293a2b3e2c56cc8f4ccb306ead5d4bf5d0f0f0864c139007c9bdcfaff1f languageName: node linkType: hard -"@openmrs/esm-config@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-config@npm:5.6.1-pre.1979" +"@openmrs/esm-config@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-config@npm:5.6.1-pre.2069" dependencies: ramda: "npm:^0.26.1" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x single-spa: 5.x - checksum: 10/f1fb09105a57571003f2165319adb636ebd0814b09c59f8130775debfd775c862a83fabc3baa9ca0a5fb4165366b144339c9e517408fd46769fe8038a7337c9a + checksum: 10/1739a1dc83501f839b760d1999b70e3e40c1725c1ae11b18b22793b349e5e58563d5f4a4052561d5514a893325f1023c20940071064216777910cbd2952db704 languageName: node linkType: hard -"@openmrs/esm-context@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-context@npm:5.6.1-pre.1979" +"@openmrs/esm-context@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-context@npm:5.6.1-pre.2069" dependencies: immer: "npm:^10.0.4" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x - checksum: 10/f4caca4c256ef8a9c024d0220908967a0178e9c01ad040979ba4edb231f52cece1bbf7971a867aa3b6445ac1b1232f229cae404f91077043327e8e1d683c2e5e + checksum: 10/b03711cca22dccc3f706ab4ebc9caf59856df5b97d18af00e28f72c7cf047be3ba6d7be9fb18e877094bf4622e7be1843d67e95eda6ea7024563042c04bbf913 languageName: node linkType: hard -"@openmrs/esm-dynamic-loading@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-dynamic-loading@npm:5.6.1-pre.1979" +"@openmrs/esm-dynamic-loading@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-dynamic-loading@npm:5.6.1-pre.2069" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-translations": 5.x - checksum: 10/b1145978a1edaf4571dc754310658d3f0b0cb6e8fdfee61766de9512d5b79dc7d41b0e2e0a10863fa47d14d242fb7e43c42922dad3673cfa8ce55334d7c83d0f + checksum: 10/c21b4eb6e8af14e1063966fd0f722415b2a35d77a7a4ee761241dad4718b48fbd75551dcb488a08f6704866a6cc79db4a16611ada39efaabad64377dc09c98a9 languageName: node linkType: hard -"@openmrs/esm-error-handling@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-error-handling@npm:5.6.1-pre.1979" +"@openmrs/esm-error-handling@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-error-handling@npm:5.6.1-pre.2069" peerDependencies: "@openmrs/esm-globals": 5.x - checksum: 10/2f5310c01032570df87055a9303f1d3f5e2e920a5ac0f3b2b289270ec015f5074e68df3b6b65dc7bdcb18203a1c3461a59be5d71c9a3f4e4bfc5ce9bcd9b2e20 + checksum: 10/dfbd4c0b6d2545926b3e13a2006d5bd20d8c8e82faa96425405260699a779da9e64c97d3fa746c882970bff911bac2cc6084a78e980d3e6660f0a2bac67a0277 languageName: node linkType: hard -"@openmrs/esm-extensions@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-extensions@npm:5.6.1-pre.1979" +"@openmrs/esm-extensions@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-extensions@npm:5.6.1-pre.2069" dependencies: lodash-es: "npm:^4.17.21" peerDependencies: @@ -3176,43 +3193,43 @@ __metadata: "@openmrs/esm-state": 5.x "@openmrs/esm-utils": 5.x single-spa: 5.x - checksum: 10/c01f6abcbceb68a5e0a3e87487e746f5a08ce84c879e66527a26c0a6375cbfb43524fcff7ba824186485f84bdc0f807055a6deed340b740f5b2824654c94a814 + checksum: 10/b97d1e9654e58584f65550eb765de238336dc9e71d62c30f3c20a33ad867e6f1f38cb9dc140097942a778ca57f9e5f49ccde18587e7386d1f774eb419679954d languageName: node linkType: hard -"@openmrs/esm-feature-flags@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-feature-flags@npm:5.6.1-pre.1979" +"@openmrs/esm-feature-flags@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-feature-flags@npm:5.6.1-pre.2069" dependencies: ramda: "npm:^0.26.1" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x single-spa: 5.x - checksum: 10/d86558d612dc5f57a56712d0453eb35f0e5065ae0cf8302835e2bb6d60c738a856c514f3adbb060d39d6986db5c112499ba84c00d98f802f16c3aed6e439c0f8 - languageName: node - linkType: hard - -"@openmrs/esm-framework@npm:5.6.1-pre.1979, @openmrs/esm-framework@npm:^5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-framework@npm:5.6.1-pre.1979" - dependencies: - "@openmrs/esm-api": "npm:5.6.1-pre.1979" - "@openmrs/esm-config": "npm:5.6.1-pre.1979" - "@openmrs/esm-context": "npm:5.6.1-pre.1979" - "@openmrs/esm-dynamic-loading": "npm:5.6.1-pre.1979" - "@openmrs/esm-error-handling": "npm:5.6.1-pre.1979" - "@openmrs/esm-extensions": "npm:5.6.1-pre.1979" - "@openmrs/esm-feature-flags": "npm:5.6.1-pre.1979" - "@openmrs/esm-globals": "npm:5.6.1-pre.1979" - "@openmrs/esm-navigation": "npm:5.6.1-pre.1979" - "@openmrs/esm-offline": "npm:5.6.1-pre.1979" - "@openmrs/esm-react-utils": "npm:5.6.1-pre.1979" - "@openmrs/esm-routes": "npm:5.6.1-pre.1979" - "@openmrs/esm-state": "npm:5.6.1-pre.1979" - "@openmrs/esm-styleguide": "npm:5.6.1-pre.1979" - "@openmrs/esm-translations": "npm:5.6.1-pre.1979" - "@openmrs/esm-utils": "npm:5.6.1-pre.1979" + checksum: 10/5517e04a71de8ae2f13b4fbb1845fc5e152dae33ad1cbdafbeaf2862d584dc04b23a6e975707b5aaa4565bd8460c86e817335688089b24ee6081943315b83e6b + languageName: node + linkType: hard + +"@openmrs/esm-framework@npm:5.6.1-pre.2069, @openmrs/esm-framework@npm:^5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-framework@npm:5.6.1-pre.2069" + dependencies: + "@openmrs/esm-api": "npm:5.6.1-pre.2069" + "@openmrs/esm-config": "npm:5.6.1-pre.2069" + "@openmrs/esm-context": "npm:5.6.1-pre.2069" + "@openmrs/esm-dynamic-loading": "npm:5.6.1-pre.2069" + "@openmrs/esm-error-handling": "npm:5.6.1-pre.2069" + "@openmrs/esm-extensions": "npm:5.6.1-pre.2069" + "@openmrs/esm-feature-flags": "npm:5.6.1-pre.2069" + "@openmrs/esm-globals": "npm:5.6.1-pre.2069" + "@openmrs/esm-navigation": "npm:5.6.1-pre.2069" + "@openmrs/esm-offline": "npm:5.6.1-pre.2069" + "@openmrs/esm-react-utils": "npm:5.6.1-pre.2069" + "@openmrs/esm-routes": "npm:5.6.1-pre.2069" + "@openmrs/esm-state": "npm:5.6.1-pre.2069" + "@openmrs/esm-styleguide": "npm:5.6.1-pre.2069" + "@openmrs/esm-translations": "npm:5.6.1-pre.2069" + "@openmrs/esm-utils": "npm:5.6.1-pre.2069" dayjs: "npm:^1.10.7" peerDependencies: dayjs: 1.x @@ -3223,35 +3240,35 @@ __metadata: rxjs: 6.x single-spa: 5.x swr: 2.x - checksum: 10/b019c7d016c55aec3dc36655f05009b35f2772ea4b87a8522ab00ab4317add7ca628f9110abef2a85c44a2680289f5ee4cb57201c93d13a8913d54e96f5f77e3 + checksum: 10/032d3cf009b23eb3a190a1f79a2aa8a0df1d20e2611ed07d798a3f37d9ad4e2f8c0f57102ec50de784cd6c1e1f581c249b595e8fdd06c726c86bae2a8acd0c47 languageName: node linkType: hard -"@openmrs/esm-globals@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-globals@npm:5.6.1-pre.1979" +"@openmrs/esm-globals@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-globals@npm:5.6.1-pre.2069" dependencies: "@types/fhir": "npm:0.0.31" peerDependencies: single-spa: 5.x - checksum: 10/f2e8e5e1ba3436eb5eca27a2ca531d7542a351aeb9a6785419e959b7ad710358fb4abc3ed4a0661dc7ac78b47424e70add2722e569b4618e102b32f661d039ec + checksum: 10/46caee884af9fc45576da0c24a5d8aff29ed3ccbc77c719f7668e87f590bcac955806813728761adfd7bebfd94c20ec6260caf9262a132bd37ece1027d02d23d languageName: node linkType: hard -"@openmrs/esm-navigation@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-navigation@npm:5.6.1-pre.1979" +"@openmrs/esm-navigation@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-navigation@npm:5.6.1-pre.2069" dependencies: path-to-regexp: "npm:6.1.0" peerDependencies: "@openmrs/esm-state": 5.x - checksum: 10/d494b17a4566228178bfc232f000c536cd3e202ef1affb65740b06e81ca4f90f42d1babb7d9ec3ae4a9225c6a7617028dfc3403aa97c8e6b500db4735ba762ed + checksum: 10/deb57a7288412f7e4fbc12fda4080b918996069d22e64c944c4d04973b35f66e1fd3e0ebbcf983d140cd6881fbf672c3a8d96cd4d3178a2872b66039f171f737 languageName: node linkType: hard -"@openmrs/esm-offline@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-offline@npm:5.6.1-pre.1979" +"@openmrs/esm-offline@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-offline@npm:5.6.1-pre.2069" dependencies: dexie: "npm:^3.0.3" lodash-es: "npm:^4.17.21" @@ -3262,7 +3279,7 @@ __metadata: "@openmrs/esm-globals": 5.x "@openmrs/esm-state": 5.x rxjs: 6.x - checksum: 10/f7dd75029e6e44ebc54d8faac7aa7246080d107b9ebe67103dc44bd26fff323b5aa15a782aaf92a589a152401338e28302142328dd797ab4906b7cdc858adfe7 + checksum: 10/7db8526cdbf9d085d03811536fedd0d7e06a7fa734556598137aa6c6e6fbe89a1ef982aec30c9014666a3667081686bde29d52404b27f9c2f0e753c1bee99c75 languageName: node linkType: hard @@ -3281,9 +3298,9 @@ __metadata: languageName: node linkType: hard -"@openmrs/esm-react-utils@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-react-utils@npm:5.6.1-pre.1979" +"@openmrs/esm-react-utils@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-react-utils@npm:5.6.1-pre.2069" dependencies: lodash-es: "npm:^4.17.21" single-spa-react: "npm:^6.0.0" @@ -3304,34 +3321,34 @@ __metadata: react-i18next: 11.x rxjs: 6.x swr: 2.x - checksum: 10/a73d9320f523d7dc479d6dfbba6fcea1fba2c1cfcbff8e544b9e318492d9fdcd84b1087247a067ac2dd5faeec1c10f10ccf6642bca8aa0a895b9bd9e795c1b62 + checksum: 10/17fb7e8f0354cb22f53a499ecd07beec844e955e47161a98ca50769251f466e0671d1d625a4a03c21896fc4231120ee2e837daab62805a918ab35692adf01c47 languageName: node linkType: hard -"@openmrs/esm-routes@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-routes@npm:5.6.1-pre.1979" +"@openmrs/esm-routes@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-routes@npm:5.6.1-pre.2069" peerDependencies: "@openmrs/esm-globals": 5.x "@openmrs/esm-utils": 5.x - checksum: 10/d6d5d2aaa5e63d3e3895e218625541d99d6da309840f22fe253cd46df33fa4636f8cc2a1d8c1c022f9c7949436c76e264bcb35c1c76bc0a9ec1c06605c8c260a + checksum: 10/c6970f80086f9bd4eb273fd86e8326b0ef596a660053667fa1392f7f9f0b614b6bf34c65fb7170d90864daffb91e7961439dae1a9620272f19e65c75fe387421 languageName: node linkType: hard -"@openmrs/esm-state@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-state@npm:5.6.1-pre.1979" +"@openmrs/esm-state@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-state@npm:5.6.1-pre.2069" dependencies: zustand: "npm:^4.3.6" peerDependencies: "@openmrs/esm-globals": 5.x - checksum: 10/74e7923dbcd64d740a3c9fe1cc89c6e5616f36c0ed08c7457372050086087f59677850f35ca0958194a41118a3d7f3604ecc66408f6650c6c748cf80d23ce423 + checksum: 10/9caea1872de971972540295bbf5672a16080c61998bbaa4cfab85b3b481c76ec41010ebae22fa8d56464c2d612aeeef862256b6bed5f9fdab7e6445ea1a78d0f languageName: node linkType: hard -"@openmrs/esm-styleguide@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-styleguide@npm:5.6.1-pre.1979" +"@openmrs/esm-styleguide@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-styleguide@npm:5.6.1-pre.2069" dependencies: "@carbon/charts": "npm:^1.12.0" "@carbon/react": "npm:~1.37.0" @@ -3354,24 +3371,24 @@ __metadata: react: 18.x react-dom: 18.x rxjs: 6.x - checksum: 10/87015d51b232df9a4b94dabf0cf5237cedf464757c08c2153ee910aff055e878dd5dcee9bcf4721b7290aa91ac32ce7f6ea2add884bd7b98f85b5b6af3d1cb90 + checksum: 10/7e8066cc060eb71d0828dd695630435e2ace3637933ad002f9b2b37e728527c749a6d0c99534b1248db23f2a0cd289213001f6073068325383b3b05abc7c80a2 languageName: node linkType: hard -"@openmrs/esm-translations@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-translations@npm:5.6.1-pre.1979" +"@openmrs/esm-translations@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-translations@npm:5.6.1-pre.2069" dependencies: i18next: "npm:21.10.0" peerDependencies: i18next: 21.x - checksum: 10/a292d951901c90b7409eefce558a3cfdda147f786b443e4eafde913e5630d26f810dc3267f7e593bec42b3effe3514e8d7a49579d34e366360a615671e9e7d2f + checksum: 10/5db335026ede46b3d9f58e7a89f459f2a0cc10680f40871a7e90b20606874635ddb99640e82855bb207da1aa9a4cb7fbabf8e463433be03d0dcee555c750b093 languageName: node linkType: hard -"@openmrs/esm-utils@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/esm-utils@npm:5.6.1-pre.1979" +"@openmrs/esm-utils@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/esm-utils@npm:5.6.1-pre.2069" dependencies: "@internationalized/date": "npm:^3.5.4" semver: "npm:7.3.2" @@ -3380,7 +3397,7 @@ __metadata: dayjs: 1.x i18next: 21.x rxjs: 6.x - checksum: 10/199e0ed89220fb8c17263fee609744326e60b41483a58ad5135a39789f0ff3681d7e628f547da6ad1430d130b34fc0d438c4290c5cddb3cb38b1a483bbbd993f + checksum: 10/7b688a5fe8cdebe34f699ad0fec91a929fa8eb47c373c9dc4284f35dc6c573899f76ba6f2bae11cd9ab7f36dc8428d51a55ac159c2befe1350ff42bfe846ef0d languageName: node linkType: hard @@ -3412,9 +3429,9 @@ __metadata: languageName: node linkType: hard -"@openmrs/webpack-config@npm:5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "@openmrs/webpack-config@npm:5.6.1-pre.1979" +"@openmrs/webpack-config@npm:5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "@openmrs/webpack-config@npm:5.6.1-pre.2069" dependencies: "@swc/core": "npm:^1.3.58" clean-webpack-plugin: "npm:^4.0.0" @@ -3431,7 +3448,7 @@ __metadata: webpack-stats-plugin: "npm:^1.0.3" peerDependencies: webpack: 5.x - checksum: 10/41993ed842ad07cbe0d66262cdee8936627879c475fda4c7dda95f6921971b1668b5c5dd0a157a6b8df89515ed02aa0d61abc9d60514d4e04172bdbdcffa24f4 + checksum: 10/094b1d2aa5b2b93501308a5c0ff2247b4111be1e1bf3f8a5633b863e7206854793631273aad23ae0f96251a34a7f9709da92825c468aa72924f54160f0b5c45e languageName: node linkType: hard @@ -14868,12 +14885,12 @@ __metadata: languageName: node linkType: hard -"openmrs@npm:^5.6.1-pre.1979": - version: 5.6.1-pre.1979 - resolution: "openmrs@npm:5.6.1-pre.1979" +"openmrs@npm:^5.6.1-pre.2069": + version: 5.6.1-pre.2069 + resolution: "openmrs@npm:5.6.1-pre.2069" dependencies: - "@openmrs/esm-app-shell": "npm:5.6.1-pre.1979" - "@openmrs/webpack-config": "npm:5.6.1-pre.1979" + "@openmrs/esm-app-shell": "npm:5.6.1-pre.2069" + "@openmrs/webpack-config": "npm:5.6.1-pre.2069" "@pnpm/npm-conf": "npm:^2.1.0" "@swc/core": "npm:^1.3.58" autoprefixer: "npm:^10.4.2" @@ -14905,7 +14922,7 @@ __metadata: yargs: "npm:^17.6.2" bin: openmrs: ./dist/cli.js - checksum: 10/bf5bb4810972a947eaba23bc13daa6f6f5cbe10250137b480a10e2ff2f832922aa4ad37bc06c08524f288b0fbf7bfb9b01205ca061367acb0cf78ab3df31ab96 + checksum: 10/4579c3ec89aff07d47da479c6ec088acff96c735b130e8335e3a0f9244412927ec1eaada9b94d6181841381cbeb8fb5d49552f259ac0b20a76d1b0b3581882c4 languageName: node linkType: hard From 6ba8354857d4d5705364b113a76c26432ad7bc22 Mon Sep 17 00:00:00 2001 From: Rugute Date: Tue, 16 Jul 2024 15:24:12 +0300 Subject: [PATCH 2/3] added HIE CR patient verification --- .../src/registers/Jua_Mtoto_Wako/Jua_Mtoto_Wako.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/esm-report-app/src/registers/Jua_Mtoto_Wako/Jua_Mtoto_Wako.scss b/packages/esm-report-app/src/registers/Jua_Mtoto_Wako/Jua_Mtoto_Wako.scss index e7f1bd72..2236bb2c 100644 --- a/packages/esm-report-app/src/registers/Jua_Mtoto_Wako/Jua_Mtoto_Wako.scss +++ b/packages/esm-report-app/src/registers/Jua_Mtoto_Wako/Jua_Mtoto_Wako.scss @@ -159,7 +159,6 @@ p { th:nth-child(9), th:nth-child(10), th:nth-child(11), - td:nth-child(9), td:nth-child(10), td:nth-child(11), From 261aa2a9384fa75b65800aa2cca76be063f4823e Mon Sep 17 00:00:00 2001 From: Rugute Date: Tue, 16 Jul 2024 15:27:51 +0300 Subject: [PATCH 3/3] added HIE CR patient verification --- .../src/care-programs/care-programs.test.tsx | 150 ------------------ .../patient-summary.component.test.tsx | 120 -------------- .../program-summary/program-summary.test.tsx | 101 ------------ .../regimen-history.component.test.tsx | 43 ----- 4 files changed, 414 deletions(-) delete mode 100644 packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx delete mode 100644 packages/esm-care-panel-app/src/patient-summary/patient-summary.component.test.tsx delete mode 100644 packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx delete mode 100644 packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx diff --git a/packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx b/packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx deleted file mode 100644 index 304a4929..00000000 --- a/packages/esm-care-panel-app/src/care-programs/care-programs.test.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import React from 'react'; -import { screen, render } from '@testing-library/react'; -import CarePrograms from './care-programs.component'; -import * as careProgramsHook from '../hooks/useCarePrograms'; -import { launchPatientWorkspace, launchStartVisitPrompt } from '@openmrs/esm-patient-common-lib'; -import { useVisit, launchWorkspace } from '@openmrs/esm-framework'; -import { type PatientCarePrograms } from '../hooks/useCarePrograms'; -import userEvent from '@testing-library/user-event'; - -jest.mock('../hooks/useCarePrograms'); - -const mockUseVisit = useVisit as jest.Mock; - -const testProps = { - isLoading: true, - isValidating: false, - error: null, - carePrograms: [], - mutate: jest.fn(), -}; - -const mockAPIResponse: Array = [ - { - uuid: 'dfdc6d40-2f2f-463d-ba90-cc97350441a8', - display: 'HIV', - enrollmentFormUuid: 'e4b506c1-7379-42b6-a374-284469cba8da', - discontinuationFormUuid: 'e3237ede-fa70-451f-9e6c-0908bc39f8b9', - enrollmentStatus: 'active', - enrollmentDetails: { - uuid: '561f0766-6496-4f59-abc2-a4030788b3cc', - dateEnrolled: '2023-10-25 03:27:15.0', - dateCompleted: '', - location: 'Moi Teaching Refferal Hospital', - }, - }, - { - uuid: '9f144a34-3a4a-44a9-8486-6b7af6cc64f6', - display: 'TB', - enrollmentFormUuid: '89994550-9939-40f3-afa6-173bce445c79', - discontinuationFormUuid: '4b296dd0-f6be-4007-9eb8-d0fd4e94fb3a', - enrollmentStatus: 'eligible', - }, -]; - -jest.mock('@openmrs/esm-framework', () => ({ - ...jest.requireActual('@openmrs/esm-framework'), - useVisit: jest.fn().mockReturnValue({ currentVisit: { uuid: 'some-visitUuid' } }), - useLayoutType: jest.fn().mockReturnValue('tablet'), - launchWorkspace: jest.fn(), -})); - -jest.mock('@openmrs/esm-patient-common-lib', () => ({ - ...jest.requireActual('@openmrs/esm-patient-common-lib'), - launchStartVisitPrompt: jest.fn(), - launchPatientWorkspace: jest.fn(), -})); - -describe('CarePrograms', () => { - xtest('should render loading spinner while fetching care programs', () => { - jest.spyOn(careProgramsHook, 'useCarePrograms').mockReturnValueOnce({ ...testProps }); - renderCarePrograms(); - const loadingSpinner = screen.getByText('Loading data...'); - expect(loadingSpinner).toBeInTheDocument(); - }); - - xtest('should render error state message in API has error', () => { - jest - .spyOn(careProgramsHook, 'useCarePrograms') - .mockReturnValueOnce({ ...testProps, isLoading: false, error: new Error('Internal error 500') }); - renderCarePrograms(); - const errorMessage = screen.getByText( - 'Sorry, there was a problem displaying this information. You can try to reload this page, or contact the site administrator and quote the error code above.', - ); - expect(errorMessage).toBeInTheDocument(); - }); - - xtest('should display empty state if the patient is not eligible to any program', () => { - jest - .spyOn(careProgramsHook, 'useCarePrograms') - .mockReturnValueOnce({ ...testProps, isLoading: false, carePrograms: [] }); - - renderCarePrograms(); - const emptyStateMessage = screen.getByText('There are no {{displayText}} to display for this patient'); - const displayTitle = screen.getByRole('heading', { name: 'Care program' }); - expect(emptyStateMessage).toBeInTheDocument(); - expect(displayTitle).toBeInTheDocument(); - }); - - test('should display patient eligible programs and launch enrollment or discontinuation form', async () => { - const user = userEvent.setup(); - jest - .spyOn(careProgramsHook, 'useCarePrograms') - .mockReturnValueOnce({ ...testProps, isLoading: false, carePrograms: mockAPIResponse }); - mockUseVisit.mockReturnValue({ currentVisit: { uuid: 'some-visitUuid' } }); - renderCarePrograms(); - - const tableHeaders = ['Program name', 'Status']; - tableHeaders.forEach((tableHeader) => expect(screen.getByText(tableHeader)).toBeInTheDocument()); - const cardTitle = screen.getByRole('heading', { name: 'Care Programs' }); - expect(cardTitle).toBeInTheDocument(); - - const enrollButton = screen.getByRole('button', { name: /Enroll/ }); - const discontinueButton = screen.getByRole('button', { name: /Discontinue/ }); - await user.click(enrollButton); - expect(launchPatientWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', { - formInfo: { - additionalProps: { enrollmenrDetails: undefined }, - encounterUuid: '', - formUuid: '89994550-9939-40f3-afa6-173bce445c79', - }, - mutateForm: expect.anything(), - workspaceTitle: 'TB Enrollment form', - }); - - await user.click(discontinueButton); - expect(launchPatientWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', { - formInfo: { - additionalProps: { - enrollmenrDetails: { - dateCompleted: '', - dateEnrolled: '2023-10-25 03:27:15.0', - location: 'Moi Teaching Refferal Hospital', - uuid: '561f0766-6496-4f59-abc2-a4030788b3cc', - }, - }, - encounterUuid: '', - formUuid: 'e3237ede-fa70-451f-9e6c-0908bc39f8b9', - }, - mutateForm: expect.anything(), - workspaceTitle: 'HIV Discontinuation form', - }); - }); - - xtest('should prompt user to start Visit before filling any enrollment form', async () => { - const user = userEvent.setup(); - mockUseVisit.mockReturnValue({ currentVisit: null }); - jest - .spyOn(careProgramsHook, 'useCarePrograms') - .mockReturnValueOnce({ ...testProps, isLoading: false, carePrograms: mockAPIResponse }); - renderCarePrograms(); - - const enrollButton = screen.getByRole('button', { name: /Enroll/ }); - await user.click(enrollButton); - expect(launchStartVisitPrompt).toHaveBeenCalled(); - }); -}); - -const renderCarePrograms = () => { - render(); -}; 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 100644 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/program-summary/program-summary.test.tsx b/packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx deleted file mode 100644 index 40ed5605..00000000 --- a/packages/esm-care-panel-app/src/program-summary/program-summary.test.tsx +++ /dev/null @@ -1,101 +0,0 @@ -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/regimen-history.component.test.tsx b/packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx deleted file mode 100644 index 933c2af7..00000000 --- a/packages/esm-care-panel-app/src/regimen/regimen-history.component.test.tsx +++ /dev/null @@ -1,43 +0,0 @@ -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(); - }); -});