@pih/esm-commons-app 2.1.1-pre.15 → 2.1.1-pre.154
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/.turbo/turbo-build.log +5 -35
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/125.js +1 -0
- package/dist/125.js.map +1 -0
- package/dist/163.js +1 -0
- package/dist/163.js.map +1 -0
- package/dist/240.js +1 -0
- package/dist/240.js.map +1 -0
- package/dist/294.js +1 -0
- package/dist/294.js.map +1 -0
- package/dist/347.js +1 -0
- package/dist/347.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/395.js +33 -0
- package/dist/395.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/499.js +1 -0
- package/dist/499.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/646.js +17 -0
- package/dist/646.js.map +1 -0
- package/dist/664.js +1 -0
- package/dist/664.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/701.js +1 -0
- package/dist/701.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/72.js +1 -0
- package/dist/72.js.map +1 -0
- package/dist/743.js +1 -0
- package/dist/743.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/830.js +1 -0
- package/dist/830.js.map +1 -0
- package/dist/846.js +11 -0
- package/dist/846.js.map +1 -0
- package/dist/852.js +1 -0
- package/dist/852.js.map +1 -0
- package/dist/855.js +1 -0
- package/dist/855.js.map +1 -0
- package/dist/859.js +1 -0
- package/dist/859.js.map +1 -0
- package/dist/867.js +1 -0
- package/dist/867.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/959.js +1 -0
- package/dist/959.js.map +1 -0
- package/dist/985.js +1 -0
- package/dist/985.js.map +1 -0
- package/dist/989.js +1 -0
- package/dist/989.js.map +1 -0
- package/dist/main.js +3 -1
- package/dist/main.js.map +1 -1
- package/dist/pih-esm-commons-app.js +3 -1
- package/dist/pih-esm-commons-app.js.buildmanifest.json +642 -107
- package/dist/pih-esm-commons-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +7 -10
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +85 -0
- package/src/declarations.d.ts +2 -0
- package/src/dispensing/prescription-label-print-action.extension.test.tsx +47 -0
- package/src/dispensing/prescription-label-print-action.extension.tsx +45 -0
- package/src/dispensing/prescription-label-print.modal.test.tsx +125 -0
- package/src/dispensing/prescription-label-print.modal.tsx +139 -0
- package/src/dispensing/prescription-label-print.types.ts +16 -0
- package/src/dispensing/zebra-label-print-action.extension.tsx +66 -0
- package/src/dispensing/zebra-label-print.resource.ts +70 -0
- package/src/dispensing/zebra-pdf-print.ts +108 -0
- package/src/hooks/useConceptReferenceRanges.ts +128 -0
- package/src/hooks/useEncounters.ts +22 -0
- package/src/hooks/useVisitAttribute.ts +37 -0
- package/src/index.ts +49 -12
- package/src/routes.json +76 -11
- package/src/service-queues-app/maternal-triage-form.workspace.tsx +111 -0
- package/src/service-queues-app/maternal-triage-queue-actions.extension.tsx +132 -0
- package/src/service-queues-app/maternal-triage-queue-actions.resource.ts +84 -0
- package/src/service-queues-app/maternal-trial-queue-actions.scss +14 -0
- package/src/service-queues-app/types.ts +58 -0
- package/src/ward-app/VitalsTable.tsx +361 -0
- package/src/ward-app/inborn-outborn-tag-row.extension.tsx +79 -0
- package/src/ward-app/inborn-outborn-tag-row.scss +57 -0
- package/src/ward-app/o2-iframe.component.tsx +13 -4
- package/src/ward-app/o2-pregnancy-infant-dashboard.extension.tsx +29 -10
- package/src/ward-app/o2-pregnancy-infant-dashboard.scss +5 -0
- package/src/ward-app/o2-visit-summary-action-button.component.tsx +17 -0
- package/src/ward-app/o2-visit-summary-workspace.component.tsx +104 -26
- package/src/ward-app/o2-vital-signs-action-button.component.tsx +17 -0
- package/src/ward-app/o2-vital-signs-workspace.component.tsx +65 -0
- package/src/ward-app/types.ts +7 -4
- package/translations/en.json +32 -3
- package/translations/es.json +15 -1
- package/translations/fr.json +15 -1
- package/dist/138.js +0 -1
- package/dist/138.js.map +0 -1
- package/dist/291.js +0 -1
- package/dist/291.js.map +0 -1
- package/dist/300.js +0 -1
- package/dist/372.js +0 -1
- package/dist/372.js.map +0 -1
- package/dist/41.js +0 -2
- package/dist/41.js.LICENSE.txt +0 -9
- package/dist/41.js.map +0 -1
- package/dist/411.js +0 -2
- package/dist/411.js.LICENSE.txt +0 -5
- package/dist/411.js.map +0 -1
- package/dist/449.js +0 -2
- package/dist/449.js.LICENSE.txt +0 -9
- package/dist/449.js.map +0 -1
- package/dist/55.js +0 -1
- package/dist/641.js +0 -1
- package/dist/641.js.map +0 -1
- package/dist/660.js +0 -1
- package/dist/660.js.map +0 -1
- package/dist/831.js +0 -2
- package/dist/831.js.LICENSE.txt +0 -5
- package/dist/831.js.map +0 -1
- package/dist/90.js +0 -1
- package/dist/90.js.map +0 -1
- package/dist/99.js +0 -1
- package/src/ward-app/o2-visit-summary-action-button.extension.tsx +0 -18
- package/webpack.config.js +0 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import {
|
|
4
|
+
DataTable,
|
|
5
|
+
Table,
|
|
6
|
+
TableBody,
|
|
7
|
+
TableCell,
|
|
8
|
+
TableContainer,
|
|
9
|
+
TableHead,
|
|
10
|
+
TableHeader,
|
|
11
|
+
TableRow,
|
|
12
|
+
Button,
|
|
13
|
+
} from '@carbon/react';
|
|
14
|
+
import { restBaseUrl, useConfig, useOpenmrsFetchAll, usePagination, usePatient } from '@openmrs/esm-framework';
|
|
15
|
+
import { Pagination, NumericObservation } from '@openmrs/esm-styleguide';
|
|
16
|
+
import {
|
|
17
|
+
useConceptReferenceRanges,
|
|
18
|
+
calculateInterpretation,
|
|
19
|
+
type ObservationInterpretation,
|
|
20
|
+
} from '../hooks/useConceptReferenceRanges';
|
|
21
|
+
import dayjs from 'dayjs';
|
|
22
|
+
|
|
23
|
+
interface VitalsTableProps {
|
|
24
|
+
patientUuid: string;
|
|
25
|
+
visitUuid: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface Observation {
|
|
29
|
+
uuid: string;
|
|
30
|
+
concept: {
|
|
31
|
+
uuid: string;
|
|
32
|
+
name: {
|
|
33
|
+
display: string;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
value: number | string | object | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface EncounterProvider {
|
|
40
|
+
provider: {
|
|
41
|
+
person: {
|
|
42
|
+
display: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface Encounter {
|
|
48
|
+
uuid: string;
|
|
49
|
+
display: string;
|
|
50
|
+
encounterDatetime: string;
|
|
51
|
+
encounterProviders?: EncounterProvider[];
|
|
52
|
+
obs: Observation[];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface EncounterResponse {
|
|
56
|
+
results: Encounter[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const customRepresentation =
|
|
60
|
+
'custom:(uuid,display,encounterDatetime,encounterProviders:(provider:(person:(display))),obs:(uuid,concept:(uuid,name:(display)),value))';
|
|
61
|
+
|
|
62
|
+
const VitalsTable: React.FC<VitalsTableProps> = ({ patientUuid, visitUuid }) => {
|
|
63
|
+
const { concepts: externalConcepts } = useConfig<{ concepts: Record<string, string> }>({
|
|
64
|
+
externalModuleName: '@openmrs/esm-patient-vitals-app',
|
|
65
|
+
});
|
|
66
|
+
const { concepts: localConcepts } = useConfig<{
|
|
67
|
+
concepts: { hemoglobinUuid: string; glucoseUuid: string; fhrUuid: string; secondFhrUuid: string };
|
|
68
|
+
}>();
|
|
69
|
+
const vitalsConcepts = {
|
|
70
|
+
systolic: externalConcepts.systolicBloodPressureUuid,
|
|
71
|
+
diastolic: externalConcepts.diastolicBloodPressureUuid,
|
|
72
|
+
pulse: externalConcepts.pulseUuid,
|
|
73
|
+
temperature: externalConcepts.temperatureUuid,
|
|
74
|
+
oxygenSaturation: externalConcepts.oxygenSaturationUuid,
|
|
75
|
+
respiratoryRate: externalConcepts.respiratoryRateUuid,
|
|
76
|
+
hemoglobin: localConcepts.hemoglobinUuid,
|
|
77
|
+
glucose: localConcepts.glucoseUuid,
|
|
78
|
+
fhr: localConcepts.fhrUuid,
|
|
79
|
+
secondFhr: localConcepts.secondFhrUuid,
|
|
80
|
+
};
|
|
81
|
+
const { referenceRangeMap } = useConceptReferenceRanges(patientUuid, Object.values(vitalsConcepts));
|
|
82
|
+
const { t } = useTranslation();
|
|
83
|
+
const [showAll, setShowAll] = useState(false);
|
|
84
|
+
const { patient } = usePatient(patientUuid);
|
|
85
|
+
|
|
86
|
+
// Check if patient is less than 30 days old
|
|
87
|
+
const isNewborn = useMemo(() => {
|
|
88
|
+
if (!patient?.birthDate) return false;
|
|
89
|
+
const birthDate = dayjs(patient.birthDate);
|
|
90
|
+
const ageInDays = dayjs().diff(birthDate, 'day');
|
|
91
|
+
return ageInDays < 30;
|
|
92
|
+
}, [patient?.birthDate]);
|
|
93
|
+
|
|
94
|
+
const interpretationLabels: Record<ObservationInterpretation, string> = {
|
|
95
|
+
normal: t('normal', 'Normal'),
|
|
96
|
+
high: t('high', 'High'),
|
|
97
|
+
critically_high: t('criticallyHigh', 'Critically high'),
|
|
98
|
+
off_scale_high: t('offScaleHigh', 'Off scale high'),
|
|
99
|
+
low: t('low', 'Low'),
|
|
100
|
+
critically_low: t('criticallyLow', 'Critically low'),
|
|
101
|
+
off_scale_low: t('offScaleLow', 'Off scale low'),
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
interface VitalCellData {
|
|
105
|
+
value: number | string | object | null;
|
|
106
|
+
interpretation?: ObservationInterpretation;
|
|
107
|
+
conceptUuid?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
interface VitalRowData {
|
|
111
|
+
id: string;
|
|
112
|
+
date: string;
|
|
113
|
+
bp: { systolic?: VitalCellData; diastolic?: VitalCellData };
|
|
114
|
+
pulse?: VitalCellData;
|
|
115
|
+
temperature?: VitalCellData;
|
|
116
|
+
oxygenSaturation?: VitalCellData;
|
|
117
|
+
respiratoryRate?: VitalCellData;
|
|
118
|
+
hemoglobin?: VitalCellData;
|
|
119
|
+
glucose?: VitalCellData;
|
|
120
|
+
fhr?: VitalCellData;
|
|
121
|
+
secondFhr?: VitalCellData;
|
|
122
|
+
provider?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
type VitalColumnKey = Exclude<keyof VitalRowData, 'id' | 'date' | 'provider'>;
|
|
126
|
+
|
|
127
|
+
const createVitalCellData = (value: number | string | object | null, conceptUuid?: string): VitalCellData => {
|
|
128
|
+
if (value === null || value === undefined) {
|
|
129
|
+
return { value: null };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const numericValue = typeof value === 'object' ? undefined : value;
|
|
133
|
+
const interpretation = conceptUuid
|
|
134
|
+
? calculateInterpretation(numericValue, referenceRangeMap[conceptUuid])
|
|
135
|
+
: undefined;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
value,
|
|
139
|
+
interpretation,
|
|
140
|
+
conceptUuid,
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const vitalColumns: Array<{ key: VitalColumnKey; label: string; conceptUuid?: string }> = useMemo(() => {
|
|
145
|
+
let columns = [
|
|
146
|
+
{ key: 'bp', label: t('bloodPressure', 'BP') },
|
|
147
|
+
{ key: 'pulse', label: t('pulse', 'HR'), conceptUuid: vitalsConcepts.pulse },
|
|
148
|
+
{ key: 'temperature', label: t('temperature', 'Temp'), conceptUuid: vitalsConcepts.temperature },
|
|
149
|
+
{ key: 'respiratoryRate', label: t('respiratoryRate', 'RR'), conceptUuid: vitalsConcepts.respiratoryRate },
|
|
150
|
+
{ key: 'oxygenSaturation', label: t('oxygenSaturation', 'O2 Sat'), conceptUuid: vitalsConcepts.oxygenSaturation },
|
|
151
|
+
{ key: 'glucose', label: t('glucose', 'Glucose'), conceptUuid: vitalsConcepts.glucose },
|
|
152
|
+
] as Array<{ key: VitalColumnKey; label: string; conceptUuid?: string }>;
|
|
153
|
+
|
|
154
|
+
// Only include hemoglobin, fhr, and secondFhr for patients 30 days or older
|
|
155
|
+
if (!isNewborn) {
|
|
156
|
+
columns.push(
|
|
157
|
+
{ key: 'hemoglobin', label: t('hemoglobin', 'Hb'), conceptUuid: vitalsConcepts.hemoglobin },
|
|
158
|
+
{ key: 'fhr', label: t('fetalHeartRate', 'FHR'), conceptUuid: vitalsConcepts.fhr },
|
|
159
|
+
{ key: 'secondFhr', label: t('secondFetalHeartRate', '2nd FHR'), conceptUuid: vitalsConcepts.secondFhr },
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return columns;
|
|
164
|
+
}, [isNewborn, vitalsConcepts, t]);
|
|
165
|
+
|
|
166
|
+
const renderVitalCell = (
|
|
167
|
+
cellData: VitalCellData | { systolic?: VitalCellData; diastolic?: VitalCellData } | undefined,
|
|
168
|
+
) => {
|
|
169
|
+
if (!cellData) {
|
|
170
|
+
return <span>—</span>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Handle BP which is an object with systolic and diastolic
|
|
174
|
+
if ('systolic' in cellData) {
|
|
175
|
+
const bpData = cellData as { systolic?: VitalCellData; diastolic?: VitalCellData };
|
|
176
|
+
return (
|
|
177
|
+
<div style={{ display: 'flex', gap: '0', alignItems: 'center' }}>
|
|
178
|
+
{bpData.systolic && (
|
|
179
|
+
<NumericObservation
|
|
180
|
+
value={
|
|
181
|
+
bpData.systolic.value !== null && bpData.systolic.value !== undefined
|
|
182
|
+
? String(bpData.systolic.value)
|
|
183
|
+
: ''
|
|
184
|
+
}
|
|
185
|
+
interpretation={bpData.systolic.interpretation}
|
|
186
|
+
conceptUuid={bpData.systolic.conceptUuid}
|
|
187
|
+
variant="cell"
|
|
188
|
+
patientUuid={patientUuid}
|
|
189
|
+
/>
|
|
190
|
+
)}
|
|
191
|
+
<span style={{ margin: '0 -0.25rem' }}>/</span>
|
|
192
|
+
{bpData.diastolic && (
|
|
193
|
+
<NumericObservation
|
|
194
|
+
value={
|
|
195
|
+
bpData.diastolic.value !== null && bpData.diastolic.value !== undefined
|
|
196
|
+
? String(bpData.diastolic.value)
|
|
197
|
+
: ''
|
|
198
|
+
}
|
|
199
|
+
interpretation={bpData.diastolic.interpretation}
|
|
200
|
+
conceptUuid={bpData.diastolic.conceptUuid}
|
|
201
|
+
variant="cell"
|
|
202
|
+
patientUuid={patientUuid}
|
|
203
|
+
/>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Handle regular vitals
|
|
210
|
+
const vitalData = cellData as VitalCellData;
|
|
211
|
+
if (vitalData.value === null || vitalData.value === undefined) {
|
|
212
|
+
return <span>—</span>;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<NumericObservation
|
|
217
|
+
value={String(vitalData.value)}
|
|
218
|
+
interpretation={vitalData.interpretation}
|
|
219
|
+
conceptUuid={vitalData.conceptUuid}
|
|
220
|
+
variant="cell"
|
|
221
|
+
patientUuid={patientUuid}
|
|
222
|
+
/>
|
|
223
|
+
);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
const createRowData = (encounter: Encounter): VitalRowData => {
|
|
227
|
+
const vitalsMap = encounter.obs.reduce(
|
|
228
|
+
(acc, obs) => {
|
|
229
|
+
acc[obs.concept.uuid] = obs.value;
|
|
230
|
+
return acc;
|
|
231
|
+
},
|
|
232
|
+
{} as Record<string, number | string | object | null>,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
const providerName = encounter.encounterProviders?.[0]?.provider?.person?.display || '—';
|
|
236
|
+
|
|
237
|
+
return {
|
|
238
|
+
id: encounter.uuid,
|
|
239
|
+
date: dayjs(encounter.encounterDatetime).format('DD/MM/YYYY HH:mm'),
|
|
240
|
+
bp: {
|
|
241
|
+
systolic: createVitalCellData(vitalsMap[vitalsConcepts.systolic]),
|
|
242
|
+
diastolic: createVitalCellData(vitalsMap[vitalsConcepts.diastolic]),
|
|
243
|
+
},
|
|
244
|
+
pulse: createVitalCellData(vitalsMap[vitalsConcepts.pulse]),
|
|
245
|
+
temperature: createVitalCellData(vitalsMap[vitalsConcepts.temperature]),
|
|
246
|
+
oxygenSaturation: createVitalCellData(
|
|
247
|
+
vitalsMap[vitalsConcepts.oxygenSaturation],
|
|
248
|
+
vitalsConcepts.oxygenSaturation,
|
|
249
|
+
),
|
|
250
|
+
respiratoryRate: createVitalCellData(vitalsMap[vitalsConcepts.respiratoryRate]),
|
|
251
|
+
hemoglobin: createVitalCellData(vitalsMap[vitalsConcepts.hemoglobin]),
|
|
252
|
+
glucose: createVitalCellData(vitalsMap[vitalsConcepts.glucose]),
|
|
253
|
+
fhr: createVitalCellData(vitalsMap[vitalsConcepts.fhr]),
|
|
254
|
+
secondFhr: createVitalCellData(vitalsMap[vitalsConcepts.secondFhr]),
|
|
255
|
+
provider: providerName,
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
const { data: vitalsData, error } = useOpenmrsFetchAll<Encounter>(
|
|
260
|
+
`${restBaseUrl}/encounter?patient=${patientUuid}&visit=${visitUuid}&order=desc&v=${customRepresentation}`,
|
|
261
|
+
);
|
|
262
|
+
|
|
263
|
+
const encounters: Encounter[] = vitalsData || [];
|
|
264
|
+
|
|
265
|
+
// Filter encounters to only include those with vitals observations
|
|
266
|
+
const encountersWithVitals = encounters.filter((encounter) => {
|
|
267
|
+
return encounter.obs.some((obs) => Object.values(vitalsConcepts).includes(obs.concept.uuid));
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// Process each encounter to extract vital signs
|
|
271
|
+
const tableRows = useMemo(
|
|
272
|
+
() => encountersWithVitals.map(createRowData),
|
|
273
|
+
[encountersWithVitals, vitalsConcepts, referenceRangeMap],
|
|
274
|
+
);
|
|
275
|
+
|
|
276
|
+
const pageSize = 5;
|
|
277
|
+
const { results: paginatedVitals, currentPage, goTo } = usePagination(tableRows, pageSize);
|
|
278
|
+
|
|
279
|
+
const displayedRows = showAll ? tableRows : paginatedVitals;
|
|
280
|
+
|
|
281
|
+
if (error) {
|
|
282
|
+
return <div>{t('errorLoadingVitals', 'Error loading vitals')}</div>;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!vitalsData) {
|
|
286
|
+
return <div>{t('loadingVitals', 'Loading vitals...')}</div>;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const tableHeaders = [
|
|
290
|
+
{ key: 'date', header: t('date', 'Date') },
|
|
291
|
+
...vitalColumns.map((col) => ({ key: col.key, header: col.label })),
|
|
292
|
+
{ key: 'provider', header: t('provider', 'Provider') },
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
return (
|
|
296
|
+
<>
|
|
297
|
+
<DataTable headers={tableHeaders} rows={displayedRows} size="sm" useZebraStyles>
|
|
298
|
+
{({ rows, headers, getTableProps, getHeaderProps, getRowProps }) => (
|
|
299
|
+
<TableContainer>
|
|
300
|
+
<Table {...getTableProps()}>
|
|
301
|
+
<TableHead>
|
|
302
|
+
<TableRow>
|
|
303
|
+
{headers.map((header) => (
|
|
304
|
+
<TableHeader {...getHeaderProps({ header })} key={header.key}>
|
|
305
|
+
{header.header}
|
|
306
|
+
</TableHeader>
|
|
307
|
+
))}
|
|
308
|
+
</TableRow>
|
|
309
|
+
</TableHead>
|
|
310
|
+
<TableBody>
|
|
311
|
+
{rows.map((row) => {
|
|
312
|
+
const rowData = displayedRows.find((r) => r.id === row.id) as VitalRowData | undefined;
|
|
313
|
+
|
|
314
|
+
return (
|
|
315
|
+
<TableRow {...getRowProps({ row })} key={row.id}>
|
|
316
|
+
{row.cells.map((cell) => {
|
|
317
|
+
if (cell.info.header === 'date' || cell.info.header === 'provider') {
|
|
318
|
+
return <TableCell key={cell.id}>{cell.value}</TableCell>;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const columnKey = cell.info.header as VitalColumnKey;
|
|
322
|
+
const cellValue = rowData?.[columnKey];
|
|
323
|
+
|
|
324
|
+
return (
|
|
325
|
+
<TableCell key={cell.id} style={{ padding: 0 }}>
|
|
326
|
+
{renderVitalCell(cellValue)}
|
|
327
|
+
</TableCell>
|
|
328
|
+
);
|
|
329
|
+
})}
|
|
330
|
+
</TableRow>
|
|
331
|
+
);
|
|
332
|
+
})}
|
|
333
|
+
</TableBody>
|
|
334
|
+
</Table>
|
|
335
|
+
</TableContainer>
|
|
336
|
+
)}
|
|
337
|
+
</DataTable>
|
|
338
|
+
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginTop: '1rem' }}>
|
|
339
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', marginLeft: '1rem' }}>
|
|
340
|
+
<span style={{ fontSize: '0.875rem', color: '#6f6f6f' }}>
|
|
341
|
+
{displayedRows.length} / {tableRows.length} {t('items', 'items')}
|
|
342
|
+
</span>
|
|
343
|
+
<Button kind="ghost" size="sm" onClick={() => setShowAll(!showAll)}>
|
|
344
|
+
{showAll ? t('showPaginated', 'Show Paginated') : t('showAll', 'Show All')}
|
|
345
|
+
</Button>
|
|
346
|
+
</div>
|
|
347
|
+
{!showAll && (
|
|
348
|
+
<Pagination
|
|
349
|
+
pageNumber={currentPage}
|
|
350
|
+
totalItems={tableRows.length}
|
|
351
|
+
currentItems={paginatedVitals.length}
|
|
352
|
+
pageSize={pageSize}
|
|
353
|
+
onPageNumberChange={({ page }) => goTo(page)}
|
|
354
|
+
/>
|
|
355
|
+
)}
|
|
356
|
+
</div>
|
|
357
|
+
</>
|
|
358
|
+
);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
export default VitalsTable;
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useParams } from 'react-router-dom';
|
|
4
|
+
import { type Visit, useSession } from '@openmrs/esm-framework';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { useVisitAttribute } from '../hooks/useVisitAttribute';
|
|
7
|
+
import styles from './inborn-outborn-tag-row.scss';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Stable, non-module class on the badge so card headers can reserve space for it via a `:has()`
|
|
11
|
+
* sibling rule in openmrs-esm-patient-management (see ward-patient-card.scss /
|
|
12
|
+
* admission-request-card.scss). This class is the cross-package contract between the extension slot
|
|
13
|
+
* and the host card layout — keep it in sync with the selectors there.
|
|
14
|
+
*/
|
|
15
|
+
const BADGE_MARKER_CLASS = 'ward-inborn-outborn-badge';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* UUID of the visit attribute type that records whether the patient was born in this
|
|
19
|
+
* facility (Inborn) or referred from elsewhere (Outborn).
|
|
20
|
+
*/
|
|
21
|
+
const INBORN_OUTBORN_ATTRIBUTE_TYPE_UUID = '86f716fc-5e26-4eb1-9484-46370cff28f0';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* UUID of the NICU ward location. The Inborn/Outborn badge is only relevant on the NICU ward, so the
|
|
25
|
+
* extension renders nothing on any other ward.
|
|
26
|
+
*/
|
|
27
|
+
const NICU_LOCATION_UUID = '0ce2f6fb-6850-11ee-ab8d-0242ac120002';
|
|
28
|
+
|
|
29
|
+
interface InbornOutbornTagRowProps {
|
|
30
|
+
/** The active visit, supplied by the extension slot's state. */
|
|
31
|
+
visit: Visit;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* InbornOutbornTagRow displays a tag indicating the patient's place of birth relative to this
|
|
36
|
+
* facility, based on a boolean visit attribute on the active visit. The patient is considered
|
|
37
|
+
* "Inborn" (born here) only when the attribute's value is `true`; in every other case (the value is
|
|
38
|
+
* false, or the visit has no such attribute) they default to "Outborn" (referred in).
|
|
39
|
+
*
|
|
40
|
+
* This is a PIH-specific extension slotted into the maternal ward cards of
|
|
41
|
+
* openmrs-esm-patient-management via `maternal-ward-patient-card-header-slot`. The default visit
|
|
42
|
+
* representation used by the ward hooks does not include attribute values, so the value is fetched
|
|
43
|
+
* on demand via {@link useVisitAttribute}. Following the NICU patient card design, the badge renders
|
|
44
|
+
* as a rounded pill pinned to the top-right corner of the card: blue for Inborn, purple for Outborn.
|
|
45
|
+
* The badge is scoped to the NICU ward only (see {@link NICU_LOCATION_UUID}).
|
|
46
|
+
*/
|
|
47
|
+
const InbornOutbornTagRow: React.FC<InbornOutbornTagRowProps> = ({ visit }) => {
|
|
48
|
+
const { t } = useTranslation();
|
|
49
|
+
const { value, isLoading } = useVisitAttribute(visit?.uuid, INBORN_OUTBORN_ATTRIBUTE_TYPE_UUID);
|
|
50
|
+
// Resolve the ward being viewed the same way esm-ward-app does: the location UUID comes from the
|
|
51
|
+
// ward route (`.../ward/:locationUuid`), falling back to the session location when unset.
|
|
52
|
+
const { locationUuid } = useParams();
|
|
53
|
+
const { sessionLocation } = useSession();
|
|
54
|
+
const wardLocationUuid = locationUuid ?? sessionLocation?.uuid;
|
|
55
|
+
|
|
56
|
+
// Only show the badge on the NICU ward.
|
|
57
|
+
if (wardLocationUuid !== NICU_LOCATION_UUID) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Avoid flashing a (default) Outborn badge before the attribute value has loaded.
|
|
62
|
+
if (isLoading) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const isInborn = value === true;
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className={classNames(styles.inbornOutbornBadge, BADGE_MARKER_CLASS)}>
|
|
70
|
+
{isInborn ? (
|
|
71
|
+
<span className={classNames(styles.badge, styles.inborn)}>{t('inborn', 'Inborn')}</span>
|
|
72
|
+
) : (
|
|
73
|
+
<span className={classNames(styles.badge, styles.outborn)}>{t('outborn', 'Outborn')}</span>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export default InbornOutbornTagRow;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The extension slot wrapper and its inner `Extension` wrapper are both hardcoded to
|
|
5
|
+
* `position: relative` by @openmrs/esm-framework, which would otherwise trap the absolutely
|
|
6
|
+
* positioned badge inside that small in-flow wrapper box instead of the card. `display: contents`
|
|
7
|
+
* removes those wrapper boxes from layout so the badge anchors to the `position: relative` card and
|
|
8
|
+
* pins to its top-right corner. (Sibling/`:has()` selectors still match — `display: contents` only
|
|
9
|
+
* affects box generation, not the DOM tree.)
|
|
10
|
+
*/
|
|
11
|
+
:global {
|
|
12
|
+
[data-extension-slot-name='maternal-ward-patient-card-header-slot'],
|
|
13
|
+
[data-extension-slot-name='maternal-ward-patient-card-header-slot'] [data-extension-id] {
|
|
14
|
+
display: contents;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Inborn/Outborn origin badge, rendered as a rounded pill pinned to the top-right corner of the
|
|
20
|
+
* patient card (per the NICU patient card design). The card container is `position: relative`, so
|
|
21
|
+
* the badge positions against the whole card regardless of where it sits in the DOM (including when
|
|
22
|
+
* it is injected through an extension slot).
|
|
23
|
+
*/
|
|
24
|
+
.inbornOutbornBadge {
|
|
25
|
+
position: absolute;
|
|
26
|
+
top: 0.875rem; // 14px, matches the design
|
|
27
|
+
right: 0.875rem;
|
|
28
|
+
z-index: 2; // sit above the card's clickable overlay (::before, z-index 1)
|
|
29
|
+
margin: 0 !important; // neutralize blanket child margins applied by some card containers
|
|
30
|
+
pointer-events: none; // let clicks fall through to the underlying card button
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// The origin badge, rendered as the design's fully-rounded pill.
|
|
34
|
+
.badge {
|
|
35
|
+
display: inline-flex;
|
|
36
|
+
align-items: center;
|
|
37
|
+
height: 1.5rem; // 24px
|
|
38
|
+
min-height: 1.5rem;
|
|
39
|
+
padding: 0 0.75rem; // 12px
|
|
40
|
+
border-radius: 62.5rem;
|
|
41
|
+
font-size: 0.75rem; // 12px, matches Carbon tag
|
|
42
|
+
font-weight: 600;
|
|
43
|
+
line-height: 1;
|
|
44
|
+
white-space: nowrap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Inborn: blue (matches the former Carbon `blue` tag colors).
|
|
48
|
+
.inborn {
|
|
49
|
+
background-color: #d0e2ff;
|
|
50
|
+
color: #0043ce;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Outborn: purple (matches the former Carbon `purple` tag colors).
|
|
54
|
+
.outborn {
|
|
55
|
+
background-color: #e8daff;
|
|
56
|
+
color: #6929c4;
|
|
57
|
+
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { ArrowLeft } from '@carbon/react/icons';
|
|
2
2
|
|
|
3
|
-
import React, { useCallback, useMemo, useRef, useState } from 'react';
|
|
4
|
-
import styles from './o2-iframe.scss';
|
|
5
3
|
import { IconButton, InlineLoading } from '@carbon/react';
|
|
4
|
+
import React, { useMemo, useRef, useState } from 'react';
|
|
6
5
|
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import styles from './o2-iframe.scss';
|
|
7
7
|
|
|
8
8
|
interface O2IFrame {
|
|
9
9
|
src: string;
|
|
@@ -16,9 +16,14 @@ interface O2IFrame {
|
|
|
16
16
|
* a list of css selectors defining elements to be made un-clickable within the iframe
|
|
17
17
|
*/
|
|
18
18
|
elementsToDisable?: string[];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* JavaScript code to inject into the iframe to run
|
|
22
|
+
*/
|
|
23
|
+
customJavaScript?: string;
|
|
19
24
|
}
|
|
20
25
|
|
|
21
|
-
const O2IFrame: React.FC<O2IFrame> = ({ src, elementsToHide, elementsToDisable }) => {
|
|
26
|
+
const O2IFrame: React.FC<O2IFrame> = ({ src, elementsToHide, elementsToDisable, customJavaScript = '' }) => {
|
|
22
27
|
const iframeRef = useRef<HTMLIFrameElement>();
|
|
23
28
|
const [isIframeLoading, setIsIframeLoading] = useState(true);
|
|
24
29
|
const [isGoingBack, setIsGoingBack] = useState(false);
|
|
@@ -32,7 +37,7 @@ const O2IFrame: React.FC<O2IFrame> = ({ src, elementsToHide, elementsToDisable }
|
|
|
32
37
|
|
|
33
38
|
return `@media screen {
|
|
34
39
|
${toHide}
|
|
35
|
-
${toDisable}
|
|
40
|
+
${toDisable}
|
|
36
41
|
}`;
|
|
37
42
|
}, [elementsToHide, elementsToDisable]);
|
|
38
43
|
|
|
@@ -48,6 +53,10 @@ const O2IFrame: React.FC<O2IFrame> = ({ src, elementsToHide, elementsToDisable }
|
|
|
48
53
|
const styleTag = contentDocument.createElement('style');
|
|
49
54
|
styleTag.innerHTML = customCss;
|
|
50
55
|
contentDocument.head.appendChild(styleTag);
|
|
56
|
+
|
|
57
|
+
const scriptTag = contentDocument.createElement('script');
|
|
58
|
+
scriptTag.innerHTML = customJavaScript;
|
|
59
|
+
contentDocument.body.appendChild(scriptTag);
|
|
51
60
|
};
|
|
52
61
|
|
|
53
62
|
const goBack = () => {
|
|
@@ -3,6 +3,7 @@ import React from 'react';
|
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
4
|
import { useActivePatientEnrollment } from '../hooks/useProgramEnrollment';
|
|
5
5
|
import O2IFrame from './o2-iframe.component';
|
|
6
|
+
import styles from './o2-pregnancy-infant-dashboard.scss';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Extension to display either the O2 pregnancy program or infant program dashboard,
|
|
@@ -30,6 +31,9 @@ const O2PregnancyInfantProgramDashboard: React.FC<{ patientUuid: string }> = ({
|
|
|
30
31
|
'.pregnancy\\.dashboard\\.ancFollowupEncounters',
|
|
31
32
|
'.pregnancy\\.dashboard\\.riskFactors a.right',
|
|
32
33
|
'.pregnancy\\.dashboard\\.temperature a.right',
|
|
34
|
+
'.pregnancy\\.dashboard\\.postpartumDailyProgressEncounters',
|
|
35
|
+
'.pregnancy\\.dashboard\\.laborAndDeliverySummaryEncounters',
|
|
36
|
+
'.pregnancy\\.dashboard\\.maternalAdmissionEncounters',
|
|
33
37
|
'.patient-location',
|
|
34
38
|
'.pregnancy\\.dashboard\\.currentEnrollment',
|
|
35
39
|
'.infant\\.dashboard\\.newbornAssesmentEncounters',
|
|
@@ -48,19 +52,34 @@ const O2PregnancyInfantProgramDashboard: React.FC<{ patientUuid: string }> = ({
|
|
|
48
52
|
if (isLoading) {
|
|
49
53
|
return <InlineLoading />;
|
|
50
54
|
} else if (inInfantProgram) {
|
|
51
|
-
const
|
|
52
|
-
return
|
|
55
|
+
const infantProgramUrl = `${window.openmrsBase}/coreapps/clinicianfacing/patient.page?patientId=${patientUuid}&dashboard=${infantProgramUuid}`;
|
|
56
|
+
return (
|
|
57
|
+
<>
|
|
58
|
+
<div className={styles.patientChartLinkContainer}>
|
|
59
|
+
<a href={infantProgramUrl}>{t('fullInfantDashboard', 'Full Infant Dashboard')}</a>
|
|
60
|
+
</div>
|
|
61
|
+
<O2IFrame key={patientUuid} {...{ src: infantProgramUrl, elementsToDisable, elementsToHide }} />
|
|
62
|
+
</>
|
|
63
|
+
);
|
|
53
64
|
} else if (inPregnancyProgram) {
|
|
54
|
-
const
|
|
55
|
-
return
|
|
65
|
+
const pregnancyProgramUrl = `${window.openmrsBase}/coreapps/clinicianfacing/patient.page?patientId=${patientUuid}&dashboard=${pregnancyProgramUuid}`;
|
|
66
|
+
return (
|
|
67
|
+
<>
|
|
68
|
+
<div className={styles.patientChartLinkContainer}>
|
|
69
|
+
<a href={pregnancyProgramUrl}>{t('fullPregnancyDashboard', 'Full Pregnancy Dashboard')}</a>
|
|
70
|
+
</div>
|
|
71
|
+
<O2IFrame key={patientUuid} {...{ src: pregnancyProgramUrl, elementsToDisable, elementsToHide }} />
|
|
72
|
+
</>
|
|
73
|
+
);
|
|
56
74
|
} else {
|
|
75
|
+
const patientChartUrl = `${window.openmrsBase}/coreapps/clinicianfacing/patient.page?patientId=${patientUuid}`;
|
|
57
76
|
return (
|
|
58
|
-
|
|
59
|
-
{
|
|
60
|
-
'
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
77
|
+
<>
|
|
78
|
+
<div className={styles.patientChartLinkContainer}>
|
|
79
|
+
<a href={patientChartUrl}>{t('fullPatientChart', 'Full Patient Chart')}</a>
|
|
80
|
+
</div>
|
|
81
|
+
<O2IFrame key={patientUuid} {...{ src: patientChartUrl, elementsToDisable, elementsToHide }} />
|
|
82
|
+
</>
|
|
64
83
|
);
|
|
65
84
|
}
|
|
66
85
|
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { ActionMenuButton2, CalendarHeatMapIcon } from '@openmrs/esm-framework';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
export default function WardPatientActionButton() {
|
|
6
|
+
const { t } = useTranslation();
|
|
7
|
+
|
|
8
|
+
return (
|
|
9
|
+
<ActionMenuButton2
|
|
10
|
+
icon={(props) => <CalendarHeatMapIcon {...props} />}
|
|
11
|
+
label={t('visitSummary', 'Visit summary')}
|
|
12
|
+
workspaceToLaunch={{
|
|
13
|
+
workspaceName: 'o2-visit-summary-workspace',
|
|
14
|
+
}}
|
|
15
|
+
/>
|
|
16
|
+
);
|
|
17
|
+
}
|