@kenyaemr/esm-care-panel-app 5.4.2-pre.2203 → 5.4.2-pre.2223

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/.turbo/turbo-build.log +30 -192
  2. package/dist/144.js +2 -0
  3. package/dist/144.js.map +1 -0
  4. package/dist/174.js +1 -0
  5. package/dist/174.js.map +1 -0
  6. package/dist/216.js +2 -0
  7. package/dist/216.js.map +1 -0
  8. package/dist/300.js +1 -0
  9. package/dist/41.js +2 -0
  10. package/dist/41.js.map +1 -0
  11. package/dist/470.js +1 -0
  12. package/dist/470.js.map +1 -0
  13. package/dist/474.js +2 -0
  14. package/dist/474.js.map +1 -0
  15. package/dist/495.js +1 -0
  16. package/dist/{372.js.map → 495.js.map} +1 -1
  17. package/dist/537.js +1 -0
  18. package/dist/537.js.map +1 -0
  19. package/dist/58.js +1 -0
  20. package/dist/58.js.map +1 -0
  21. package/dist/613.js +2 -0
  22. package/dist/613.js.map +1 -0
  23. package/dist/684.js +1 -0
  24. package/dist/684.js.map +1 -0
  25. package/dist/837.js +2 -0
  26. package/dist/837.js.LICENSE.txt +29 -0
  27. package/dist/837.js.map +1 -0
  28. package/dist/838.js +1 -0
  29. package/dist/838.js.map +1 -0
  30. package/dist/858.js +2 -0
  31. package/dist/858.js.map +1 -0
  32. package/dist/876.js +1 -0
  33. package/dist/876.js.map +1 -0
  34. package/dist/89.js +1 -0
  35. package/dist/89.js.map +1 -0
  36. package/dist/907.js +1 -0
  37. package/dist/907.js.map +1 -0
  38. package/dist/913.js +2 -0
  39. package/dist/{591.js.map → 913.js.map} +1 -1
  40. package/dist/kenyaemr-esm-care-panel-app.js +1 -1
  41. package/dist/kenyaemr-esm-care-panel-app.js.buildmanifest.json +289 -97
  42. package/dist/kenyaemr-esm-care-panel-app.js.map +1 -1
  43. package/dist/main.js +1 -1
  44. package/dist/main.js.map +1 -1
  45. package/dist/routes.json +1 -1
  46. package/package.json +4 -1
  47. package/src/care-panel/care-panel.component.test.tsx +3 -3
  48. package/src/care-panel/care-panel.component.tsx +24 -33
  49. package/src/care-panel/care-panel.scss +3 -2
  50. package/src/care-panel-dashboard/care-panel-dashboard.component.tsx +4 -11
  51. package/src/care-panel-dashboard/care-panel-dashboard.scss +1 -1
  52. package/src/care-programs/care-programs.component.tsx +3 -9
  53. package/src/care-programs/care-programs.test.tsx +5 -5
  54. package/src/declarations.d.ts +0 -13
  55. package/src/index.ts +9 -1
  56. package/src/patient-discharge/discharge-workspace-siderail.component.tsx +23 -0
  57. package/src/patient-discharge/patient-discharge.resource.tsx +123 -0
  58. package/src/patient-discharge/patient-discharge.workspace.test.tsx +160 -0
  59. package/src/patient-discharge/patient-discharge.workspace.tsx +93 -0
  60. package/src/patient-summary/patient-summary.component.tsx +1 -1
  61. package/src/patient-summary/patient-summary.scss +1 -1
  62. package/src/program-enrollment/program-enrollment.component.tsx +15 -14
  63. package/src/program-enrollment/program-enrollment.scss +3 -3
  64. package/src/program-summary/program-summary.component.tsx +1 -1
  65. package/src/program-summary/program-summary.scss +1 -1
  66. package/src/regimen/regimen-history.component.tsx +1 -1
  67. package/src/regimen/regimen-history.scss +4 -4
  68. package/src/regimen-editor/regimen-form.component.tsx +6 -6
  69. package/src/regimen-editor/standard-regimen.scss +2 -2
  70. package/src/routes.json +15 -0
  71. package/src/types/index.ts +32 -0
  72. package/translations/en.json +1 -0
  73. package/dist/130.js +0 -2
  74. package/dist/130.js.LICENSE.txt +0 -7
  75. package/dist/130.js.map +0 -1
  76. package/dist/2.js +0 -1
  77. package/dist/2.js.map +0 -1
  78. package/dist/292.js +0 -2
  79. package/dist/292.js.map +0 -1
  80. package/dist/316.js +0 -2
  81. package/dist/316.js.map +0 -1
  82. package/dist/346.js +0 -1
  83. package/dist/346.js.map +0 -1
  84. package/dist/372.js +0 -1
  85. package/dist/373.js +0 -2
  86. package/dist/373.js.map +0 -1
  87. package/dist/446.js +0 -2
  88. package/dist/446.js.map +0 -1
  89. package/dist/574.js +0 -1
  90. package/dist/589.js +0 -1
  91. package/dist/589.js.map +0 -1
  92. package/dist/591.js +0 -2
  93. package/dist/698.js +0 -2
  94. package/dist/698.js.map +0 -1
  95. package/dist/784.js +0 -2
  96. package/dist/784.js.map +0 -1
  97. /package/dist/{316.js.LICENSE.txt → 144.js.LICENSE.txt} +0 -0
  98. /package/dist/{446.js.LICENSE.txt → 216.js.LICENSE.txt} +0 -0
  99. /package/dist/{292.js.LICENSE.txt → 41.js.LICENSE.txt} +0 -0
  100. /package/dist/{784.js.LICENSE.txt → 474.js.LICENSE.txt} +0 -0
  101. /package/dist/{373.js.LICENSE.txt → 613.js.LICENSE.txt} +0 -0
  102. /package/dist/{698.js.LICENSE.txt → 858.js.LICENSE.txt} +0 -0
  103. /package/dist/{591.js.LICENSE.txt → 913.js.LICENSE.txt} +0 -0
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.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},{"name":"dispensing-patient-vitals","component":"dispensingPaentientVitals","slot":"dispensing-condition-and-diagnoses","order":1,"online":true,"offline":true}],"workspaces":[{"name":"patient-regimen-workspace","title":"Patient Regimen","component":"regimenFormWorkspace","type":"form","canMaximize":true,"canHide":true,"width":"wider"}],"version":"5.4.2-pre.2203"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"kenyaemr":"^19.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},{"name":"dispensing-patient-vitals","component":"dispensingPaentientVitals","slot":"dispensing-condition-and-diagnoses","order":1,"online":true,"offline":true},{"name":"patient-discharge-side-rail-icon","component":"patientDischargeSideRailIcon","slot":"action-menu-ward-patient-items-slot"}],"workspaces":[{"name":"patient-regimen-workspace","title":"Patient Regimen","component":"regimenFormWorkspace","type":"form","canMaximize":true,"canHide":true,"width":"wider"},{"name":"patient-care-discharge-workspace","title":"Patient Discharge","component":"patientDischargeWorkspace","type":"workspace","canMaximize":true,"canHide":true,"width":"extra-wide","groups":["ward-patient"]}],"version":"5.4.2-pre.2223"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-care-panel-app",
3
- "version": "5.4.2-pre.2203",
3
+ "version": "5.4.2-pre.2223",
4
4
  "description": "Patient care panels microfrontend for the OpenMRS SPA",
5
5
  "browser": "dist/kenyaemr-esm-care-panel-app.js",
6
6
  "main": "src/index.ts",
@@ -48,5 +48,8 @@
48
48
  "react-router-dom": "6.x",
49
49
  "swr": "2.x"
50
50
  },
51
+ "devDependencies": {
52
+ "webpack": "^5.99.9"
53
+ },
51
54
  "stableVersion": "5.4.1"
52
55
  }
@@ -29,7 +29,7 @@ describe('CarePanel Component', () => {
29
29
  isLoading: false,
30
30
  isError: false,
31
31
  });
32
- render(<CarePanel patientUuid={mockPatientUuid} formEntrySub={jest.fn()} launchPatientWorkspace={jest.fn()} />);
32
+ render(<CarePanel patientUuid={mockPatientUuid} formEntrySub={jest.fn()} />);
33
33
  expect(screen.getByText('Care Panel')).toBeInTheDocument();
34
34
  });
35
35
 
@@ -40,7 +40,7 @@ describe('CarePanel Component', () => {
40
40
  isError: false,
41
41
  });
42
42
 
43
- render(<CarePanel patientUuid={mockPatientUuid} formEntrySub={jest.fn()} launchPatientWorkspace={jest.fn()} />);
43
+ render(<CarePanel patientUuid={mockPatientUuid} formEntrySub={jest.fn()} />);
44
44
 
45
45
  expect(screen.getByTestId('mocked-structured-list-skeleton')).toBeInTheDocument();
46
46
  expect(screen.queryByTestId('mocked-program-summary')).not.toBeInTheDocument();
@@ -54,7 +54,7 @@ describe('CarePanel Component', () => {
54
54
  isError: true,
55
55
  });
56
56
 
57
- render(<CarePanel patientUuid={mockPatientUuid} formEntrySub={jest.fn()} launchPatientWorkspace={jest.fn()} />);
57
+ render(<CarePanel patientUuid={mockPatientUuid} formEntrySub={jest.fn()} />);
58
58
 
59
59
  expect(screen.getByText('Error loading program enrollments')).toBeInTheDocument();
60
60
  expect(screen.queryByTestId('mocked-structured-list-skeleton')).not.toBeInTheDocument();
@@ -1,6 +1,6 @@
1
1
  import React, { useMemo, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import { StructuredListSkeleton, ContentSwitcher, Switch } from '@carbon/react';
3
+ import { StructuredListSkeleton, ContentSwitcher, Switch, type SwitchEventHandlersParams } from '@carbon/react';
4
4
  import styles from './care-panel.scss';
5
5
  import { useEnrollmentHistory } from '../hooks/useEnrollmentHistory';
6
6
  import ProgramSummary from '../program-summary/program-summary.component';
@@ -11,23 +11,16 @@ import first from 'lodash/first';
11
11
  import sortBy from 'lodash/sortBy';
12
12
  import { ErrorState } from '@openmrs/esm-framework';
13
13
 
14
- interface CarePanelProps {
14
+ type CarePanelProps = {
15
15
  patientUuid: string;
16
16
  formEntrySub: any;
17
- launchPatientWorkspace: Function;
18
- }
19
-
20
- type SwitcherItem = {
21
- index: number;
22
- name?: string;
23
- text?: string;
24
17
  };
25
18
 
26
- const CarePanel: React.FC<CarePanelProps> = ({ patientUuid, formEntrySub, launchPatientWorkspace }) => {
19
+ const CarePanel: React.FC<CarePanelProps> = ({ patientUuid, formEntrySub }) => {
27
20
  const { t } = useTranslation();
28
21
  const { isLoading, error, enrollments, isValidating } = useEnrollmentHistory(patientUuid);
29
22
  const switcherHeaders = sortBy(Object.keys(enrollments || {}));
30
- const [switchItem, setSwitcherItem] = useState<SwitcherItem>({ index: 0 });
23
+ const [switchItem, setSwitcherItem] = useState<SwitchEventHandlersParams>({ index: 0 });
31
24
  const patientEnrollments = useMemo(
32
25
  () => (isLoading ? [] : enrollments[switchItem?.name || first(switcherHeaders)]),
33
26
  [enrollments, isLoading, switchItem?.name, switcherHeaders],
@@ -36,7 +29,7 @@ const CarePanel: React.FC<CarePanelProps> = ({ patientUuid, formEntrySub, launch
36
29
  if (isLoading) {
37
30
  return (
38
31
  <div className={styles.widgetCard}>
39
- <StructuredListSkeleton role="progressbar" />
32
+ <StructuredListSkeleton />
40
33
  </div>
41
34
  );
42
35
  }
@@ -54,29 +47,27 @@ const CarePanel: React.FC<CarePanelProps> = ({ patientUuid, formEntrySub, launch
54
47
  }
55
48
 
56
49
  return (
57
- <>
58
- <div className={styles.widgetCard}>
59
- <CardHeader title={t('carePanel', 'Care Panel')}>
60
- <div className={styles.contextSwitcherContainer}>
61
- <ContentSwitcher selectedIndex={switchItem?.index} onChange={setSwitcherItem}>
62
- {switcherHeaders?.map((enrollment) => (
63
- <Switch key={enrollment} name={enrollment} text={enrollment} />
64
- ))}
65
- </ContentSwitcher>
66
- </div>
67
- </CardHeader>
68
- <div style={{ width: '100%', minHeight: '20rem' }}>
69
- <ProgramSummary patientUuid={patientUuid} programName={switcherHeaders[switchItem?.index]} />
70
- <RegimenHistory patientUuid={patientUuid} category={switcherHeaders[switchItem?.index]} />
71
- <ProgramEnrollment
72
- patientUuid={patientUuid}
73
- programName={switcherHeaders[switchItem?.index]}
74
- enrollments={patientEnrollments}
75
- formEntrySub={formEntrySub}
76
- />
50
+ <div className={styles.widgetCard}>
51
+ <CardHeader title={t('carePanel', 'Care Panel')}>
52
+ <div className={styles.contextSwitcherContainer}>
53
+ <ContentSwitcher size="md" selectedIndex={switchItem?.index} onChange={(params) => setSwitcherItem(params)}>
54
+ {switcherHeaders?.map((enrollment) => (
55
+ <Switch key={enrollment} name={enrollment} text={enrollment} />
56
+ ))}
57
+ </ContentSwitcher>
77
58
  </div>
59
+ </CardHeader>
60
+ <div style={{ width: '100%', minHeight: '20rem' }}>
61
+ <ProgramSummary patientUuid={patientUuid} programName={switcherHeaders[switchItem?.index]} />
62
+ <RegimenHistory patientUuid={patientUuid} category={switcherHeaders[switchItem?.index]} />
63
+ <ProgramEnrollment
64
+ patientUuid={patientUuid}
65
+ programName={switcherHeaders[switchItem?.index]}
66
+ enrollments={patientEnrollments}
67
+ formEntrySub={formEntrySub}
68
+ />
78
69
  </div>
79
- </>
70
+ </div>
80
71
  );
81
72
  };
82
73
 
@@ -1,13 +1,14 @@
1
1
  @use '@carbon/styles/scss/type';
2
2
  @use '@carbon/styles/scss/spacing';
3
- @import '~@openmrs/esm-styleguide/src/vars';
3
+ @use '@carbon/colors';
4
+ @use '~@openmrs/esm-styleguide/src/vars' as *;
4
5
 
5
6
  .bodyShort01 {
6
7
  @include type.type-style('body-compact-01');
7
8
  }
8
9
 
9
10
  .widgetCard {
10
- background-color: $ui-background;
11
+ background-color: colors.$white;
11
12
  border: 1px solid $ui-03;
12
13
  position: relative;
13
14
  }
@@ -7,14 +7,11 @@ import CarePrograms from '../care-programs/care-programs.component';
7
7
 
8
8
  import CarePanelMachineLearning from '../machine-learning/machine-learning.component';
9
9
  import styles from './care-panel-dashboard.scss';
10
+ import { DefaultWorkspaceProps, launchWorkspace } from '@openmrs/esm-framework/src';
10
11
 
11
- type CarePanelDashboardProps = { patientUuid: string; formEntrySub: any; launchPatientWorkspace: Function };
12
+ type CarePanelDashboardProps = { patientUuid: string; formEntrySub: any } & DefaultWorkspaceProps;
12
13
 
13
- const CarePanelDashboard: React.FC<CarePanelDashboardProps> = ({
14
- formEntrySub,
15
- patientUuid,
16
- launchPatientWorkspace,
17
- }) => {
14
+ const CarePanelDashboard: React.FC<CarePanelDashboardProps> = ({ formEntrySub, patientUuid }) => {
18
15
  const { t } = useTranslation();
19
16
  return (
20
17
  <Layer className={styles.container}>
@@ -32,11 +29,7 @@ const CarePanelDashboard: React.FC<CarePanelDashboardProps> = ({
32
29
  </TabList>
33
30
  <TabPanels>
34
31
  <TabPanel>
35
- <CarePanel
36
- patientUuid={patientUuid}
37
- formEntrySub={formEntrySub}
38
- launchPatientWorkspace={launchPatientWorkspace}
39
- />
32
+ <CarePanel patientUuid={patientUuid} formEntrySub={formEntrySub} />
40
33
  </TabPanel>
41
34
  <TabPanel>
42
35
  <CarePrograms patientUuid={patientUuid} />
@@ -1,7 +1,7 @@
1
1
  @use '@carbon/layout';
2
2
  @use '@carbon/type';
3
3
  @use '@carbon/colors';
4
- @import '~@openmrs/esm-styleguide/src/vars';
4
+ @use '~@openmrs/esm-styleguide/src/vars' as *;
5
5
 
6
6
  .container {
7
7
  background-color: colors.$white;
@@ -14,16 +14,10 @@ import {
14
14
  DataTableSkeleton,
15
15
  } from '@carbon/react';
16
16
  import { Close, DocumentAdd } from '@carbon/react/icons';
17
- import {
18
- CardHeader,
19
- EmptyState,
20
- launchStartVisitPrompt,
21
- ErrorState,
22
- launchPatientWorkspace,
23
- } from '@openmrs/esm-patient-common-lib';
17
+ import { CardHeader, EmptyState, launchStartVisitPrompt, ErrorState } from '@openmrs/esm-patient-common-lib';
24
18
  import { useTranslation } from 'react-i18next';
25
19
  import { PatientCarePrograms, useCarePrograms } from '../hooks/useCarePrograms';
26
- import { formatDate, restBaseUrl, useLayoutType, useVisit } from '@openmrs/esm-framework';
20
+ import { formatDate, launchWorkspace, restBaseUrl, useLayoutType, useVisit } from '@openmrs/esm-framework';
27
21
  import capitalize from 'lodash/capitalize';
28
22
  import { mutate } from 'swr';
29
23
 
@@ -64,7 +58,7 @@ const CarePrograms: React.FC<CareProgramsProps> = ({ patientUuid }) => {
64
58
  : `${careProgram.display} Enrollment form`;
65
59
 
66
60
  currentVisit
67
- ? launchPatientWorkspace('patient-form-entry-workspace', {
61
+ ? launchWorkspace('patient-form-entry-workspace', {
68
62
  workspaceTitle: workspaceTitle,
69
63
  mutateForm: () => {
70
64
  handleMutations();
@@ -2,8 +2,8 @@ import React from 'react';
2
2
  import { screen, render } from '@testing-library/react';
3
3
  import CarePrograms from './care-programs.component';
4
4
  import * as careProgramsHook from '../hooks/useCarePrograms';
5
- import { launchPatientWorkspace, launchStartVisitPrompt } from '@openmrs/esm-patient-common-lib';
6
- import { useVisit, launchWorkspace } from '@openmrs/esm-framework';
5
+ import { launchStartVisitPrompt } from '@openmrs/esm-patient-common-lib';
6
+ import { launchWorkspace, useVisit } from '@openmrs/esm-framework';
7
7
  import { PatientCarePrograms } from '../hooks/useCarePrograms';
8
8
  import userEvent from '@testing-library/user-event';
9
9
 
@@ -17,6 +17,7 @@ const testProps = {
17
17
  error: null,
18
18
  carePrograms: [],
19
19
  mutate: jest.fn(),
20
+ mutateEligiblePrograms: jest.fn(),
20
21
  };
21
22
 
22
23
  const mockAPIResponse: Array<PatientCarePrograms> = [
@@ -45,7 +46,6 @@ const mockAPIResponse: Array<PatientCarePrograms> = [
45
46
  jest.mock('@openmrs/esm-patient-common-lib', () => ({
46
47
  ...jest.requireActual('@openmrs/esm-patient-common-lib'),
47
48
  launchStartVisitPrompt: jest.fn(),
48
- launchPatientWorkspace: jest.fn(),
49
49
  }));
50
50
 
51
51
  describe('CarePrograms', () => {
@@ -95,7 +95,7 @@ describe('CarePrograms', () => {
95
95
  const enrollButton = screen.getByRole('button', { name: /Enroll/ });
96
96
  const discontinueButton = screen.getByRole('button', { name: /Discontinue/ });
97
97
  await user.click(enrollButton);
98
- expect(launchPatientWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', {
98
+ expect(launchWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', {
99
99
  formInfo: {
100
100
  additionalProps: { enrollmenrDetails: expect.anything() },
101
101
  encounterUuid: '',
@@ -106,7 +106,7 @@ describe('CarePrograms', () => {
106
106
  });
107
107
 
108
108
  await user.click(discontinueButton);
109
- expect(launchPatientWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', {
109
+ expect(launchWorkspace).toHaveBeenCalledWith('patient-form-entry-workspace', {
110
110
  formInfo: {
111
111
  additionalProps: {
112
112
  enrollmenrDetails: {
@@ -1,15 +1,2 @@
1
1
  declare module '*.css';
2
2
  declare module '*.scss';
3
- declare module '@carbon/react';
4
- declare module 'react-aria' {
5
- export const I18nProvider: (...args: any) => JSX.Element;
6
- export { DateValue } from '@react-types/datepicker';
7
- export const mergeProps: any;
8
- export const useLocale: any;
9
- export const useDateField: any;
10
- export const useDatePicker: any;
11
- export const useDateSegment: any;
12
- export const useFocusRing: any;
13
- export const useHover: any;
14
- export const useObjectRef: any;
15
- }
package/src/index.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { defineConfigSchema, getSyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
1
+ import { defineConfigSchema, getAsyncLifecycle, getSyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
2
2
  import { configSchema } from './config-schema';
3
3
  import { dashboardMeta, hivPatientSummaryDashboardMeta } from './dashboard.meta';
4
4
  import { createDashboardLink } from '@openmrs/esm-patient-common-lib';
@@ -9,6 +9,8 @@ import regimenFormComponent from './regimen-editor/regimen-form.component';
9
9
  import CarePanelDashboard from './care-panel-dashboard/care-panel-dashboard.component';
10
10
  import PatientSummary from './patient-summary/patient-summary.component';
11
11
  import DispensingPatientVitals from './dispensing-patient-details/patient-vitals.component';
12
+ import PatientDischargeSideRailIcon from './patient-discharge/discharge-workspace-siderail.component';
13
+ import PatientDischargeWorkspace from './patient-discharge/patient-discharge.workspace';
12
14
 
13
15
  const moduleName = '@kenyaemr/esm-care-panel-app';
14
16
 
@@ -48,3 +50,9 @@ export const hivPatientSummary = getSyncLifecycle(PatientSummary, options);
48
50
  export const regimenFormWorkspace = getSyncLifecycle(regimenFormComponent, options);
49
51
 
50
52
  export const dispensingPaentientVitals = getSyncLifecycle(DispensingPatientVitals, options);
53
+
54
+ export const patientDischargeSideRailIcon = getSyncLifecycle(PatientDischargeSideRailIcon, options);
55
+ export const patientDischargeWorkspace = getAsyncLifecycle(
56
+ () => import('./patient-discharge/patient-discharge.workspace'),
57
+ options,
58
+ );
@@ -0,0 +1,23 @@
1
+ import { ActionMenuButton, launchWorkspace } from '@openmrs/esm-framework';
2
+ import React from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import { Exit } from '@carbon/react/icons';
5
+
6
+ const DischargeIcon = (props) => <Exit {...props} />;
7
+
8
+ export default function PatientDischargeSideRailIcon() {
9
+ const { t } = useTranslation();
10
+ const handler = () => {
11
+ launchWorkspace('patient-care-discharge-workspace');
12
+ };
13
+
14
+ return (
15
+ <ActionMenuButton
16
+ getIcon={DischargeIcon}
17
+ label={t('discharge', 'Discharge')}
18
+ iconDescription={t('discharge', 'Discharge')}
19
+ handler={handler}
20
+ type="ward-patient-discharge"
21
+ />
22
+ );
23
+ }
@@ -0,0 +1,123 @@
1
+ import {
2
+ Encounter,
3
+ openmrsFetch,
4
+ OpenmrsResource,
5
+ restBaseUrl,
6
+ showSnackbar,
7
+ useAppContext,
8
+ useSession,
9
+ Visit,
10
+ } from '@openmrs/esm-framework';
11
+ import { WardPatient } from '../types';
12
+ import { useTranslation } from 'react-i18next';
13
+
14
+ export function removePatientFromBed(bedId: number, patientUuid: string) {
15
+ return openmrsFetch(`${restBaseUrl}/beds/${bedId}?patientUuid=${patientUuid}`, {
16
+ method: 'DELETE',
17
+ });
18
+ }
19
+
20
+ const createDischargeEncounterPayload = (
21
+ patientUuid: string,
22
+ encounterType: OpenmrsResource,
23
+ location: OpenmrsResource,
24
+ currentProvider: OpenmrsResource,
25
+ visitUuid: string,
26
+ clinicianEncounterRole: OpenmrsResource,
27
+ ) => {
28
+ const encounterPayload = {
29
+ patient: patientUuid,
30
+ encounterType,
31
+ location: location?.uuid,
32
+ encounterProviders: [
33
+ {
34
+ provider: currentProvider?.uuid,
35
+ encounterRole: clinicianEncounterRole?.uuid,
36
+ },
37
+ ],
38
+ obs: [],
39
+ visit: visitUuid,
40
+ };
41
+ return encounterPayload;
42
+ };
43
+
44
+ const createDischargeEncounter = (encounterPayload: any) => {
45
+ return openmrsFetch(`${restBaseUrl}/encounter`, {
46
+ method: 'POST',
47
+ body: encounterPayload,
48
+ headers: { 'Content-Type': 'application/json' },
49
+ });
50
+ };
51
+
52
+ /**
53
+ * Custom hook for handling patient discharge operations
54
+ *
55
+ * This hook provides functionality to discharge a patient by:
56
+ * 1. Creating a discharge encounter
57
+ * 2. Removing the patient from their bed (if applicable)
58
+ * 3. Updating the ward patient group details
59
+ *
60
+ * @returns {Object} An object containing the handleDischarge function
61
+ * @property {Function} handleDischarge - Function to handle the patient discharge process
62
+ */
63
+ export const usePatientDischarge = () => {
64
+ const { t } = useTranslation();
65
+ const { wardPatientGroupDetails } =
66
+ useAppContext<{ wardPatientGroupDetails: { mutate: () => void } }>('ward-view-context') ?? {};
67
+ const session = useSession();
68
+
69
+ /**
70
+ * Handles the patient discharge process
71
+ *
72
+ * @param {Encounter} encounter - The current encounter
73
+ * @param {WardPatient} wardPatient - The ward patient information
74
+ * @param {Record<string, unknown>} emrConfiguration - EMR configuration containing encounter types and roles
75
+ * @param {Visit} visit - The current visit
76
+ * @returns {Promise<void>} A promise that resolves when the discharge process is complete
77
+ */
78
+ const handleDischarge = async (
79
+ encounter: Encounter,
80
+ wardPatient: WardPatient,
81
+ emrConfiguration: Record<string, unknown>,
82
+ visit: Visit,
83
+ ) => {
84
+ try {
85
+ const encounterPayload = createDischargeEncounterPayload(
86
+ wardPatient.patient.uuid,
87
+ emrConfiguration.exitFromInpatientEncounterType as OpenmrsResource,
88
+ session?.sessionLocation as OpenmrsResource,
89
+ session?.currentProvider as OpenmrsResource,
90
+ visit.uuid,
91
+ emrConfiguration.clinicianEncounterRole as OpenmrsResource,
92
+ );
93
+
94
+ const dischargeResponse = await createDischargeEncounter(encounterPayload);
95
+
96
+ if (!dischargeResponse?.ok) {
97
+ throw new Error('Failed to create discharge encounter');
98
+ }
99
+
100
+ if (wardPatient?.bed?.id) {
101
+ const bedRemovalResponse = await removePatientFromBed(wardPatient.bed.id, wardPatient?.patient?.uuid);
102
+ if (!bedRemovalResponse?.ok) {
103
+ throw new Error('Failed to remove patient from bed');
104
+ }
105
+ }
106
+
107
+ showSnackbar({
108
+ title: t('patientWasDischarged', 'Patient was discharged'),
109
+ kind: 'success',
110
+ });
111
+ } catch (err) {
112
+ showSnackbar({
113
+ title: t('errorDischargingPatient', 'Error discharging patient'),
114
+ subtitle: err instanceof Error ? err.message : 'Unknown error occurred',
115
+ kind: 'error',
116
+ });
117
+ } finally {
118
+ wardPatientGroupDetails?.mutate();
119
+ }
120
+ };
121
+
122
+ return { handleDischarge };
123
+ };
@@ -0,0 +1,160 @@
1
+ import React from 'react';
2
+ import { render, screen } from '@testing-library/react';
3
+ import { PatientDischargeWorkspace } from './patient-discharge.workspace';
4
+ import { useVisitOrOfflineVisit } from '@openmrs/esm-patient-common-lib';
5
+ import { usePatient, useEmrConfiguration } from '@openmrs/esm-framework';
6
+ import { usePatientDischarge } from './patient-discharge.resource';
7
+
8
+ jest.mock('@openmrs/esm-patient-common-lib', () => ({
9
+ useVisitOrOfflineVisit: jest.fn(),
10
+ }));
11
+
12
+ jest.mock('@openmrs/esm-framework', () => ({
13
+ usePatient: jest.fn(),
14
+ useEmrConfiguration: jest.fn(),
15
+ ExtensionSlot: jest.fn().mockImplementation(({ name }) => <div data-testid={name} />),
16
+ }));
17
+
18
+ jest.mock('./patient-discharge.resource', () => ({
19
+ usePatientDischarge: jest.fn(),
20
+ }));
21
+
22
+ jest.mock('react-i18next', () => ({
23
+ useTranslation: () => ({
24
+ t: (key: string) => key,
25
+ }),
26
+ }));
27
+
28
+ describe('PatientDischargeWorkspace', () => {
29
+ const mockProps = {
30
+ patientUuid: 'test-patient-uuid',
31
+ wardPatient: {
32
+ patient: {
33
+ uuid: 'test-patient-uuid',
34
+ display: 'Test Patient',
35
+ identifiers: [],
36
+ person: {
37
+ uuid: 'test-person-uuid',
38
+ display: 'Test Person',
39
+ gender: 'M',
40
+ birthdate: '1990-01-01',
41
+ dead: false,
42
+ age: 33,
43
+ deathDate: null,
44
+ causeOfDeath: null,
45
+ preferredAddress: null,
46
+ attributes: [],
47
+ },
48
+ },
49
+ visit: {
50
+ uuid: 'test-visit-uuid',
51
+ display: 'Test Visit',
52
+ encounters: [],
53
+ visitType: { uuid: 'test-visit-type-uuid', display: 'Test Visit Type' },
54
+ startDatetime: '2024-01-01',
55
+ stopDatetime: null,
56
+ },
57
+ bed: {
58
+ uuid: 'test-bed-uuid',
59
+ id: 1,
60
+ bedNumber: 'BED-001',
61
+ bedType: { uuid: 'test-bed-type-uuid', display: 'Standard' },
62
+ status: { uuid: 'test-status-uuid', display: 'OCCUPIED' },
63
+ location: { uuid: 'test-location-uuid', display: 'Test Location' },
64
+ row: 1,
65
+ column: 1,
66
+ },
67
+ },
68
+ closeWorkspace: jest.fn(),
69
+ closeWorkspaceWithSavedChanges: jest.fn(),
70
+ promptBeforeClosing: jest.fn(),
71
+ setTitle: jest.fn(),
72
+ };
73
+
74
+ const mockPatient = {
75
+ uuid: 'test-patient-uuid',
76
+ display: 'Test Patient',
77
+ };
78
+
79
+ const mockVisit = {
80
+ uuid: 'test-visit-uuid',
81
+ visitType: { uuid: 'test-visit-type-uuid' },
82
+ encounters: [{ uuid: 'test-encounter-uuid' }],
83
+ };
84
+
85
+ const mockEmrConfiguration = {
86
+ visitTypes: [],
87
+ visitTypeConfig: {},
88
+ };
89
+
90
+ beforeEach(() => {
91
+ jest.clearAllMocks();
92
+ (useVisitOrOfflineVisit as jest.Mock).mockReturnValue({
93
+ isLoading: false,
94
+ currentVisit: mockVisit,
95
+ error: null,
96
+ });
97
+ (usePatient as jest.Mock).mockReturnValue({
98
+ patient: mockPatient,
99
+ isLoading: false,
100
+ error: null,
101
+ });
102
+ (useEmrConfiguration as jest.Mock).mockReturnValue({
103
+ emrConfiguration: mockEmrConfiguration,
104
+ isLoadingEmrConfiguration: false,
105
+ errorFetchingEmrConfiguration: null,
106
+ });
107
+ (usePatientDischarge as jest.Mock).mockReturnValue({
108
+ handleDischarge: jest.fn(),
109
+ });
110
+ });
111
+
112
+ test('renders loading state when data is being fetched', () => {
113
+ (useVisitOrOfflineVisit as jest.Mock).mockReturnValue({
114
+ isLoading: true,
115
+ currentVisit: null,
116
+ error: null,
117
+ });
118
+
119
+ render(<PatientDischargeWorkspace {...mockProps} />);
120
+ const loadingSpinner = screen.getAllByText(/loading/i);
121
+ expect(loadingSpinner).toHaveLength(2);
122
+ });
123
+
124
+ test('renders error state when there is an error', () => {
125
+ (useVisitOrOfflineVisit as jest.Mock).mockReturnValue({
126
+ isLoading: false,
127
+ currentVisit: null,
128
+ error: new Error('Test error'),
129
+ });
130
+
131
+ render(<PatientDischargeWorkspace {...mockProps} />);
132
+ expect(screen.getByText('error')).toBeInTheDocument();
133
+ expect(screen.getByText(/errorLoadingPatientWorkspace/)).toBeInTheDocument();
134
+ });
135
+
136
+ test('renders the component with all required slots when data is loaded', () => {
137
+ render(<PatientDischargeWorkspace {...mockProps} />);
138
+
139
+ expect(screen.getByTestId('visit-context-header-slot')).toBeInTheDocument();
140
+ expect(screen.getByTestId('form-widget-slot')).toBeInTheDocument();
141
+ });
142
+
143
+ test('passes correct state to form-widget-slot', () => {
144
+ render(<PatientDischargeWorkspace {...mockProps} />);
145
+
146
+ const formWidgetSlot = screen.getByTestId('form-widget-slot');
147
+ expect(formWidgetSlot).toBeInTheDocument();
148
+ });
149
+
150
+ test('handles missing visit data gracefully', () => {
151
+ (useVisitOrOfflineVisit as jest.Mock).mockReturnValue({
152
+ isLoading: false,
153
+ currentVisit: null,
154
+ error: null,
155
+ });
156
+
157
+ render(<PatientDischargeWorkspace {...mockProps} />);
158
+ expect(screen.getByTestId('visit-context-header-slot')).toBeInTheDocument();
159
+ });
160
+ });