@digihmis/esm-outpatient-app 1.0.0 → 1.0.2

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.
@@ -1,97 +1,169 @@
1
1
  import React from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
- import styles from './patient-management.scss';
4
- import { getPatientUuidFromUrl } from './utils';
5
- import { ExtensionSlot, launchWorkspace, launchWorkspace2, usePatient } from '@openmrs/esm-framework';
6
3
  import {
7
- Tabs,
8
- TabsVertical,
9
- TabListVertical,
10
- TextInput,
11
- Stack,
4
+ Button,
12
5
  Layer,
6
+ SkeletonText,
13
7
  Tab,
14
- RadioButtonGroup,
15
- RadioButton,
16
- Checkbox,
17
8
  TabList,
18
9
  TabPanel,
19
10
  TabPanels,
20
- Button,
11
+ Tabs,
12
+ Tag,
13
+ Tile,
21
14
  } from '@carbon/react';
22
- import { AssetMovement } from '@carbon/react/icons';
15
+ import { Calendar, ChartLineData, Medication, Microscope, Stethoscope, type CarbonIconType } from '@carbon/react/icons';
16
+ import {
17
+ ExtensionSlot,
18
+ formatDatetime,
19
+ launchWorkspace,
20
+ parseDate,
21
+ usePatient,
22
+ useVisit,
23
+ } from '@openmrs/esm-framework';
23
24
  import PatientBanner from './patient-banner/patient-banner.component';
24
- import { CardHeader } from '@openmrs/esm-patient-common-lib/src';
25
+ import ClinicalEncountersList from './clinical-encounters.component';
26
+ import { getPatientUuidFromUrl } from './utils';
27
+ import styles from './patient-management.scss';
25
28
 
29
+ interface CarePanelTab {
30
+ key: string;
31
+ label: string;
32
+ icon: CarbonIconType;
33
+ slot: string;
34
+ emptyTitle: string;
35
+ emptyBody: string;
36
+ action?: { label: string; onClick: () => void };
37
+ }
38
+
39
+ /**
40
+ * The outpatient patient file: a Carbon care panel. A header with the patient banner, active-visit
41
+ * status and the Start Consultation action; the vitals header; and a set of contained tabs whose
42
+ * content comes from extension slots, each with a Carbon empty state until a module fills it.
43
+ */
26
44
  const CarePanel: React.FC = () => {
27
45
  const { t } = useTranslation();
28
46
  const patientUuid = getPatientUuidFromUrl();
29
- const { patient, isLoading, error } = usePatient(patientUuid);
30
- const [selectedTabIndex, setSelectedTabIndex] = React.useState(0);
47
+ const { patient, isLoading } = usePatient(patientUuid);
48
+ const { activeVisit } = useVisit(patientUuid);
31
49
 
32
- const handleConsultationStart = (patientUuid: string) => {
33
- launchWorkspace('patient-consultation', { patientUuid });
50
+ const startConsultation = () => {
51
+ launchWorkspace('form-renderer-workspace', {
52
+ workspaceTitle: t('clinicalEncounter', 'Clinical encounter'),
53
+ formId: 'clinical-encounter',
54
+ patientUuid,
55
+ visitUuid: activeVisit?.uuid,
56
+ });
34
57
  };
35
58
 
59
+ const tabs: Array<CarePanelTab> = [
60
+ {
61
+ key: 'encounter',
62
+ label: t('clinicalEncounter', 'Clinical encounter'),
63
+ icon: Stethoscope,
64
+ slot: 'outpatient-clinical-encounter-slot',
65
+ emptyTitle: t('noConsultationsTitle', 'No consultations yet'),
66
+ emptyBody: t('noConsultationsBody', 'Start a consultation to record findings for this visit.'),
67
+ action: { label: t('startConsultation', 'Start consultation'), onClick: startConsultation },
68
+ },
69
+ {
70
+ key: 'labs',
71
+ label: t('labResults', 'Lab results'),
72
+ icon: Microscope,
73
+ slot: 'outpatient-lab-results-slot',
74
+ emptyTitle: t('noLabResultsTitle', 'No lab results'),
75
+ emptyBody: t('noLabResultsBody', 'Laboratory results for this patient will appear here.'),
76
+ },
77
+ {
78
+ key: 'imaging',
79
+ label: t('imaging', 'Imaging'),
80
+ icon: ChartLineData,
81
+ slot: 'outpatient-imaging-slot',
82
+ emptyTitle: t('noImagingTitle', 'No imaging results'),
83
+ emptyBody: t('noImagingBody', 'Radiology and imaging results will appear here.'),
84
+ },
85
+ {
86
+ key: 'prescriptions',
87
+ label: t('prescriptions', 'Prescriptions'),
88
+ icon: Medication,
89
+ slot: 'outpatient-prescriptions-slot',
90
+ emptyTitle: t('noPrescriptionsTitle', 'No prescriptions'),
91
+ emptyBody: t('noPrescriptionsBody', 'Medications prescribed during this visit will appear here.'),
92
+ },
93
+ {
94
+ key: 'appointments',
95
+ label: t('appointments', 'Appointments'),
96
+ icon: Calendar,
97
+ slot: 'outpatient-appointments-slot',
98
+ emptyTitle: t('noAppointmentsTitle', 'No appointments'),
99
+ emptyBody: t('noAppointmentsBody', "The patient's upcoming appointments will appear here."),
100
+ },
101
+ ];
102
+
36
103
  return (
37
- <>
38
- <div className={styles.header}>
39
- {patient && <PatientBanner patientUuid={patientUuid} patient={patient} hideActionsOverflow={true} />}
40
- <ExtensionSlot name="patient-vitals-header-slot" state={{ patientUuid }} />
41
- <Button
42
- size="sm"
43
- kind="primary"
44
- renderIcon={AssetMovement}
45
- className={styles.buttonStartConsultation}
46
- onClick={() => handleConsultationStart(patientUuid!)}>
47
- {t('startConsultation', 'Start Consultation')}
48
- </Button>
49
- </div>
50
- {/* <TabsVertical height="3">
51
- <TabListVertical>
52
- <Tab>{t('clinicalEncounter', 'Clinical Encounter')}</Tab>
53
- <Tab>{t('labResults', 'Laboratory Results')}</Tab>
54
- <Tab>{t('procedureResults', 'Procedure Results')}</Tab>
55
- <Tab>{t('imagingResults', 'Imaging Results')}</Tab>
56
- <Tab>{t('prescriptions', 'Prescriptions')}</Tab>
57
- <Tab>{t('appointments', 'Appointments')}</Tab>
58
- </TabListVertical>
59
- <TabPanels>
60
- <TabPanel>Tab Panel 1</TabPanel>
61
- <TabPanel>
62
- <Layer>
63
- <form
64
- style={{
65
- margin: '2em',
66
- }}>
67
- <Stack gap={7}>
68
- <TextInput id="one" labelText="First Name" />
69
- <TextInput id="three" labelText="Middle Initial" />
70
- <TextInput id="two" labelText="Last Name" />
71
- <RadioButtonGroup
72
- defaultSelected="radio-1"
73
- legendText="Radio button heading"
74
- name="formgroup-default-radio-button-group">
75
- <RadioButton id="radio-1" labelText="Option 1" value="radio-1" />
76
- <RadioButton id="radio-2" labelText="Option 2" value="radio-2" />
77
- <RadioButton id="radio-3" labelText="Option 3" value="radio-3" />
78
- </RadioButtonGroup>
79
- <Checkbox id="checkbox-label-1" labelText="Checkbox one" />
80
- <Checkbox id="checkbox-label-2" labelText="Checkbox two" />
81
- <Button>Submit</Button>
82
- </Stack>
83
- </form>
84
- </Layer>
85
- </TabPanel>
86
- <TabPanel>Tab Panel 3</TabPanel>
87
- <TabPanel>Tab Panel 4</TabPanel>
88
- <TabPanel>Tab Panel 5</TabPanel>
89
- <TabPanel>Tab Panel 6</TabPanel>
90
- <TabPanel>Tab Panel 7</TabPanel>
91
- </TabPanels>
92
- </TabsVertical> */}
93
- </>
104
+ <div className={styles.container}>
105
+ <Layer>
106
+ <Tile className={styles.headerCard}>
107
+ {isLoading ? (
108
+ <SkeletonText heading width="50%" />
109
+ ) : (
110
+ patient && <PatientBanner patientUuid={patientUuid} patient={patient} hideActionsOverflow />
111
+ )}
112
+
113
+ </Tile>
114
+ </Layer>
115
+
116
+ <ExtensionSlot name="patient-vitals-header-slot" state={{ patientUuid }} className={styles.vitals} />
117
+
118
+ <Layer className={styles.tabsCard}>
119
+ <Tabs>
120
+ <TabList aria-label={t('carePanel', 'Care panel')} contained>
121
+ {tabs.map((tab) => (
122
+ <Tab key={tab.key} renderIcon={tab.icon}>
123
+ {tab.label}
124
+ </Tab>
125
+ ))}
126
+ </TabList>
127
+ <TabPanels>
128
+ {tabs.map((tab) => (
129
+ <TabPanel key={tab.key} className={styles.tabPanel}>
130
+ {tab.key === 'encounter' ? (
131
+ <ClinicalEncountersList patientUuid={patientUuid} onStartConsultation={startConsultation} />
132
+ ) : (
133
+ <>
134
+ <ExtensionSlot name={tab.slot} state={{ patientUuid, visitUuid: activeVisit?.uuid }} />
135
+ <EmptyState icon={tab.icon} title={tab.emptyTitle} body={tab.emptyBody} action={tab.action} />
136
+ </>
137
+ )}
138
+ </TabPanel>
139
+ ))}
140
+ </TabPanels>
141
+ </Tabs>
142
+ </Layer>
143
+ </div>
94
144
  );
95
145
  };
96
146
 
147
+ interface EmptyStateProps {
148
+ icon: CarbonIconType;
149
+ title: string;
150
+ body: string;
151
+ action?: { label: string; onClick: () => void };
152
+ }
153
+
154
+ const EmptyState: React.FC<EmptyStateProps> = ({ icon: Icon, title, body, action }) => (
155
+ <Layer>
156
+ <Tile className={styles.emptyTile}>
157
+ <Icon size={32} className={styles.emptyIcon} />
158
+ <p className={styles.emptyTitle}>{title}</p>
159
+ <p className={styles.emptyBody}>{body}</p>
160
+ {action && (
161
+ <Button kind="ghost" size="sm" onClick={action.onClick}>
162
+ {action.label}
163
+ </Button>
164
+ )}
165
+ </Tile>
166
+ </Layer>
167
+ );
168
+
97
169
  export default CarePanel;
@@ -1,30 +1,191 @@
1
1
  @use '@carbon/layout';
2
2
  @use '@carbon/type';
3
- @use '@carbon/colors';
4
3
 
5
- .header {
6
- margin-top: layout.$spacing-04;
4
+ .container {
5
+ padding: layout.$spacing-05;
6
+ margin: 0 auto;
7
7
  }
8
8
 
9
- :global(.cds--css-grid) {
10
- --cds-grid-gutter-start: calc(var(--cds-grid-gutter) / 2);
11
- --cds-grid-gutter-end: calc(var(--cds-grid-gutter) / 2);
12
- --cds-grid-column-hang: calc(var(--cds-grid-gutter) / 2);
13
- grid-template-columns: repeat(var(--cds-grid-columns), minmax(0, 1fr));
14
- width: 100%;
15
- max-width: 150rem;
9
+ /* ---- Header card: banner + visit status + primary action ---- */
10
+ .headerCard {
11
+ padding: layout.$spacing-05;
12
+ margin-bottom: layout.$spacing-05;
13
+ }
14
+
15
+ .actionBar {
16
+ display: flex;
17
+ align-items: center;
18
+ justify-content: space-between;
19
+ flex-wrap: wrap;
20
+ gap: layout.$spacing-04;
21
+ margin-top: layout.$spacing-05;
22
+ padding-top: layout.$spacing-05;
23
+ border-top: 1px solid var(--cds-border-subtle-01);
24
+ }
25
+
26
+ .visitTag {
27
+ margin: 0;
28
+ }
29
+
30
+ .vitals {
31
+ display: block;
32
+ margin-bottom: layout.$spacing-05;
33
+ }
34
+
35
+ /* ---- Tabs card ---- */
36
+ .tabsCard {
37
+ border: 1px solid var(--cds-border-subtle);
38
+ }
39
+
40
+ .tabPanel {
16
41
  padding: 0;
17
- margin-left: 0;
18
- margin-right: 0;
42
+ }
43
+
44
+ /* ---- Empty state inside each tab ---- */
45
+ .emptyTile {
46
+ display: flex;
47
+ flex-direction: column;
48
+ align-items: center;
49
+ justify-content: center;
50
+ text-align: center;
51
+ min-height: 14rem;
52
+ padding: layout.$spacing-10 layout.$spacing-05;
53
+ }
54
+
55
+ .emptyIcon {
56
+ color: var(--cds-icon-secondary);
57
+ margin-bottom: layout.$spacing-04;
58
+ }
59
+
60
+ .emptyTitle {
61
+ @include type.type-style('heading-compact-01');
62
+ color: var(--cds-text-primary);
63
+ margin-bottom: layout.$spacing-02;
64
+ }
65
+
66
+ .emptyBody {
67
+ @include type.type-style('body-compact-01');
68
+ color: var(--cds-text-secondary);
69
+ max-width: 24rem;
70
+ margin-bottom: layout.$spacing-05;
71
+ }
72
+
73
+ /* Responsive: stack the action bar on narrow / tablet layouts (Carbon md breakpoint = 42rem) */
74
+ @media (max-width: 42rem) {
75
+ .actionBar {
76
+ align-items: stretch;
77
+ flex-direction: column;
78
+ }
79
+
80
+ .actionBar :global(.cds--btn) {
81
+ width: 100%;
82
+ max-inline-size: none;
83
+ justify-content: space-between;
84
+ }
85
+ }
86
+
87
+ /* ---------- Clinical encounters list ---------- */
88
+
89
+ .encounterList {
90
+ display: flex;
91
+ flex-direction: column;
92
+ }
93
+
94
+ .encounterListHeader {
95
+ display: flex;
96
+ align-items: center;
97
+ justify-content: space-between;
98
+ gap: layout.$spacing-05;
99
+ margin-bottom: layout.$spacing-03;
100
+ }
101
+
102
+ .encounterCount {
103
+ @include type.type-style('label-01');
104
+ color: var(--cds-text-secondary);
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.02em;
107
+ }
108
+
109
+ .encounterTitle {
110
+ display: flex;
111
+ flex-direction: column;
112
+ gap: layout.$spacing-02;
113
+ }
114
+
115
+ .encounterDate {
116
+ @include type.type-style('heading-compact-01');
117
+ color: var(--cds-text-primary);
118
+ }
119
+
120
+ .encounterProvider {
121
+ @include type.type-style('label-01');
122
+ color: var(--cds-text-secondary);
123
+ }
124
+
125
+ .detail {
126
+ display: flex;
127
+ flex-direction: column;
128
+ gap: layout.$spacing-05;
129
+ padding: layout.$spacing-03 0;
130
+ }
131
+
132
+ .detailSection {
133
+ display: flex;
134
+ flex-direction: column;
135
+ gap: layout.$spacing-03;
136
+ }
137
+
138
+ .detailLabel {
139
+ @include type.type-style('label-01');
140
+ color: var(--cds-text-secondary);
141
+ text-transform: uppercase;
142
+ letter-spacing: 0.02em;
143
+ margin: 0;
144
+ }
145
+
146
+ .tagRow {
147
+ display: flex;
148
+ flex-wrap: wrap;
149
+ gap: layout.$spacing-03;
150
+ }
151
+
152
+ .obsGrid {
19
153
  display: grid;
154
+ grid-template-columns: minmax(10rem, 18rem) 1fr;
155
+ gap: layout.$spacing-03 layout.$spacing-05;
156
+ margin: 0;
20
157
  }
21
- :global(.-esm-outpatient__card-header__desktopHeader___Qe2fu),
22
- :global(.-esm-outpatient__card-header__tabletHeader___gFxMA) {
23
- // background-color: #f4f4f4 !important; // Your desired color
24
- padding-bottom: 1rem;
158
+
159
+ .obsRow {
160
+ display: contents;
25
161
  }
26
- :global(.cds--tabs.cds--tabs--vertical .cds--tabs__nav-item.cds--tabs__nav-item--selected) {
27
- box-shadow: inset 3px 0 0 0 var(--cds-border-interactive, #4a90b8);
28
- border-left: none;
29
- border-right: none;
162
+
163
+ .obsName {
164
+ @include type.type-style('body-compact-01');
165
+ color: var(--cds-text-secondary);
166
+ }
167
+
168
+ .obsValue {
169
+ @include type.type-style('body-compact-01');
170
+ color: var(--cds-text-primary);
171
+ margin: 0;
172
+ overflow-wrap: anywhere;
173
+ }
174
+
175
+ .orderList {
176
+ margin: 0;
177
+ padding-left: layout.$spacing-05;
178
+ @include type.type-style('body-compact-01');
179
+ color: var(--cds-text-primary);
180
+ }
181
+
182
+ .encounterActions {
183
+ display: flex;
184
+ align-items: center;
185
+ gap: layout.$spacing-03;
186
+ }
187
+
188
+ .rowActionCell {
189
+ text-align: right;
190
+ width: 3rem;
30
191
  }
package/src/routes.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.openmrs.org/routes.schema.json",
3
3
  "backendDependencies": {
4
4
  "fhir2": ">=1.2",
5
- "webservices.rest": "^2.24.0"
5
+ "webservices.rest": ">=2.24.0"
6
6
  },
7
7
  "extensions": [
8
8
  {
@@ -47,9 +47,9 @@
47
47
  },
48
48
  {
49
49
  "name": "care-panel-history",
50
- "slot": "outpatient-file-dashboard-slot",
50
+ "slot": "outpatient-file-dashboard-slot",
51
51
  "component": "carePanel"
52
52
  }
53
53
  ],
54
54
  "workspaces": []
55
- }
55
+ }