@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.
- package/dist/271.js +15 -0
- package/dist/271.js.map +1 -0
- package/dist/304.js +1 -0
- package/dist/304.js.map +1 -0
- package/dist/395.js +1 -1
- package/dist/395.js.map +1 -1
- package/dist/594.js +1 -0
- package/dist/594.js.map +1 -0
- package/dist/640.js +1 -1
- package/dist/640.js.map +1 -1
- package/dist/digihmis-esm-outpatient-app.js +1 -1
- package/dist/digihmis-esm-outpatient-app.js.buildmanifest.json +83 -83
- package/dist/digihmis-esm-outpatient-app.js.map +1 -1
- package/dist/main.js +5 -5
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +4 -2
- package/src/icon-wrapper/icon-wrapper.extension.tsx +2 -17
- package/src/left-panel/left-panel.component.tsx +6 -100
- package/src/page-header/page-header.component.tsx +4 -25
- package/src/patient-management/clinical-encounters.component.tsx +243 -0
- package/src/patient-management/clinical-encounters.resource.ts +90 -0
- package/src/patient-management/patient-management.component.tsx +147 -75
- package/src/patient-management/patient-management.scss +181 -20
- package/src/routes.json +3 -3
- package/dist/759.js +0 -15
- package/dist/759.js.map +0 -1
- package/dist/797.js +0 -1
- package/dist/797.js.map +0 -1
- package/dist/974.js +0 -1
- package/dist/974.js.map +0 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
11
|
+
Tabs,
|
|
12
|
+
Tag,
|
|
13
|
+
Tile,
|
|
21
14
|
} from '@carbon/react';
|
|
22
|
-
import {
|
|
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
|
|
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
|
|
30
|
-
const
|
|
47
|
+
const { patient, isLoading } = usePatient(patientUuid);
|
|
48
|
+
const { activeVisit } = useVisit(patientUuid);
|
|
31
49
|
|
|
32
|
-
const
|
|
33
|
-
launchWorkspace('
|
|
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
|
-
<
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
<
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
.
|
|
6
|
-
|
|
4
|
+
.container {
|
|
5
|
+
padding: layout.$spacing-05;
|
|
6
|
+
margin: 0 auto;
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
-
:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
padding-bottom: 1rem;
|
|
158
|
+
|
|
159
|
+
.obsRow {
|
|
160
|
+
display: contents;
|
|
25
161
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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": "
|
|
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
|
-
|
|
50
|
+
"slot": "outpatient-file-dashboard-slot",
|
|
51
51
|
"component": "carePanel"
|
|
52
52
|
}
|
|
53
53
|
],
|
|
54
54
|
"workspaces": []
|
|
55
|
-
}
|
|
55
|
+
}
|