@kenyaemr/esm-active-visits-app 7.0.2-pre.65
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 +32 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +3 -0
- package/dist/130.js.map +1 -0
- package/dist/255.js +2 -0
- package/dist/255.js.LICENSE.txt +9 -0
- package/dist/255.js.map +1 -0
- package/dist/271.js +1 -0
- package/dist/316.js +2 -0
- package/dist/316.js.LICENSE.txt +19 -0
- package/dist/316.js.map +1 -0
- package/dist/319.js +1 -0
- package/dist/382.js +1 -0
- package/dist/382.js.map +1 -0
- package/dist/443.js +1 -0
- package/dist/443.js.map +1 -0
- package/dist/460.js +1 -0
- package/dist/574.js +1 -0
- package/dist/635.js +1 -0
- package/dist/635.js.map +1 -0
- package/dist/644.js +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/835.js +1 -0
- package/dist/835.js.map +1 -0
- package/dist/875.js +2 -0
- package/dist/875.js.LICENSE.txt +15 -0
- package/dist/875.js.map +1 -0
- package/dist/879.js +1 -0
- package/dist/879.js.map +1 -0
- package/dist/kenyaemr-esm-active-visits-app.js +1 -0
- package/dist/kenyaemr-esm-active-visits-app.js.buildmanifest.json +580 -0
- package/dist/kenyaemr-esm-active-visits-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +25 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +3 -0
- package/package.json +55 -0
- package/src/active-visits-widget/active-visits.component.tsx +311 -0
- package/src/active-visits-widget/active-visits.resource.tsx +148 -0
- package/src/active-visits-widget/active-visits.scss +191 -0
- package/src/active-visits-widget/active-visits.test.tsx +119 -0
- package/src/active-visits-widget/empty-data-illustration.component.tsx +39 -0
- package/src/config-schema.ts +57 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +21 -0
- package/src/root.scss +30 -0
- package/src/routes.json +20 -0
- package/src/types/index.ts +28 -0
- package/src/visits-summary/visit-detail-overview.scss +328 -0
- package/src/visits-summary/visit-detail.component.tsx +77 -0
- package/src/visits-summary/visit-detail.test.tsx +122 -0
- package/src/visits-summary/visit.resource.ts +190 -0
- package/src/visits-summary/visits-components/encounter-list.component.tsx +127 -0
- package/src/visits-summary/visits-components/encounter-observations.component.tsx +43 -0
- package/src/visits-summary/visits-components/encounter-observations.test.tsx +36 -0
- package/src/visits-summary/visits-components/medications-summary.component.tsx +105 -0
- package/src/visits-summary/visits-components/notes-summary.component.tsx +51 -0
- package/src/visits-summary/visits-components/tests-summary.component.tsx +21 -0
- package/src/visits-summary/visits-components/visit-summary.component.tsx +118 -0
- package/translations/am.json +35 -0
- package/translations/ar.json +35 -0
- package/translations/en.json +35 -0
- package/translations/es.json +35 -0
- package/translations/fr.json +35 -0
- package/translations/he.json +35 -0
- package/translations/km.json +35 -0
- package/translations/zh.json +35 -0
- package/translations/zh_CN.json +35 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import React, { useEffect, useState, useMemo, useRef } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import {
|
|
5
|
+
DataTable,
|
|
6
|
+
TableContainer,
|
|
7
|
+
Table,
|
|
8
|
+
TableHead,
|
|
9
|
+
TableExpandHeader,
|
|
10
|
+
TableRow,
|
|
11
|
+
TableHeader,
|
|
12
|
+
TableBody,
|
|
13
|
+
TableExpandRow,
|
|
14
|
+
TableCell,
|
|
15
|
+
TableExpandedRow,
|
|
16
|
+
} from '@carbon/react';
|
|
17
|
+
import { useLayoutType, isDesktop } from '@openmrs/esm-framework';
|
|
18
|
+
import { type Observation } from '../visit.resource';
|
|
19
|
+
import EncounterObservations from './encounter-observations.component';
|
|
20
|
+
import styles from '../visit-detail-overview.scss';
|
|
21
|
+
|
|
22
|
+
interface EncounterListProps {
|
|
23
|
+
encounters: Array<{
|
|
24
|
+
id: any;
|
|
25
|
+
time: any;
|
|
26
|
+
encounterType: string;
|
|
27
|
+
provider: string;
|
|
28
|
+
obs: Array<Observation>;
|
|
29
|
+
}>;
|
|
30
|
+
visitUuid: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const EncounterListDataTable: React.FC<EncounterListProps> = ({ encounters, visitUuid }) => {
|
|
34
|
+
const { t } = useTranslation();
|
|
35
|
+
const layout = useLayoutType();
|
|
36
|
+
const [headerWidth, setHeaderWidth] = useState(0);
|
|
37
|
+
const headerRef = useRef(null);
|
|
38
|
+
|
|
39
|
+
const headerData = useMemo(
|
|
40
|
+
() => [
|
|
41
|
+
{
|
|
42
|
+
id: 1,
|
|
43
|
+
header: t('time', 'Time'),
|
|
44
|
+
key: 'time',
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
id: 2,
|
|
48
|
+
header: t('encounterType', 'Encounter Type'),
|
|
49
|
+
key: 'encounterType',
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 3,
|
|
53
|
+
header: t('provider', 'Provider'),
|
|
54
|
+
key: 'provider',
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
[t],
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
useEffect(() => {
|
|
61
|
+
setHeaderWidth(headerRef?.current?.clientWidth);
|
|
62
|
+
const handler = () => setHeaderWidth(headerRef?.current?.clientWidth);
|
|
63
|
+
window.addEventListener('resize', handler);
|
|
64
|
+
return () => window.removeEventListener('resize', handler);
|
|
65
|
+
}, []);
|
|
66
|
+
|
|
67
|
+
return encounters.length > 0 ? (
|
|
68
|
+
<DataTable rows={encounters} headers={headerData}>
|
|
69
|
+
{({ rows, headers, getHeaderProps, getRowProps, getTableProps }) => {
|
|
70
|
+
return (
|
|
71
|
+
<TableContainer data-testid="encountersTable">
|
|
72
|
+
<Table className={styles.customTable} {...getTableProps()} size={isDesktop(layout) ? 'sm' : 'md'}>
|
|
73
|
+
<TableHead>
|
|
74
|
+
<TableRow>
|
|
75
|
+
<TableExpandHeader />
|
|
76
|
+
{headers.map((header, i) =>
|
|
77
|
+
i === 0 ? (
|
|
78
|
+
<TableHeader id={`header_${visitUuid}_${i}`} ref={headerRef} {...getHeaderProps({ header })}>
|
|
79
|
+
{header.header}
|
|
80
|
+
</TableHeader>
|
|
81
|
+
) : (
|
|
82
|
+
<TableHeader id={`header_${visitUuid}_${i}`} {...getHeaderProps({ header })}>
|
|
83
|
+
{header.header}
|
|
84
|
+
</TableHeader>
|
|
85
|
+
),
|
|
86
|
+
)}
|
|
87
|
+
</TableRow>
|
|
88
|
+
</TableHead>
|
|
89
|
+
<TableBody>
|
|
90
|
+
{rows.map((row, i) => (
|
|
91
|
+
<React.Fragment key={row.id}>
|
|
92
|
+
<TableExpandRow {...getRowProps({ row })}>
|
|
93
|
+
{row.cells.map((cell) => (
|
|
94
|
+
<TableCell key={cell.id} data-testid={cell.id}>
|
|
95
|
+
{cell.value}
|
|
96
|
+
</TableCell>
|
|
97
|
+
))}
|
|
98
|
+
</TableExpandRow>
|
|
99
|
+
{row.isExpanded && (
|
|
100
|
+
<TableExpandedRow
|
|
101
|
+
className={styles.expandedRow}
|
|
102
|
+
style={{ paddingLeft: isDesktop(layout) ? '3rem' : '4rem' }}
|
|
103
|
+
colSpan={headers.length + 2}>
|
|
104
|
+
<div style={{ marginLeft: headerWidth }}>
|
|
105
|
+
<EncounterObservations observations={encounters[i].obs} />
|
|
106
|
+
</div>
|
|
107
|
+
</TableExpandedRow>
|
|
108
|
+
)}
|
|
109
|
+
</React.Fragment>
|
|
110
|
+
))}
|
|
111
|
+
</TableBody>
|
|
112
|
+
</Table>
|
|
113
|
+
</TableContainer>
|
|
114
|
+
);
|
|
115
|
+
}}
|
|
116
|
+
</DataTable>
|
|
117
|
+
) : (
|
|
118
|
+
<div className={styles.encounterEmptyState}>
|
|
119
|
+
<h4 className={styles.productiveHeading02}>{t('noEncountersFound', 'No encounters found')}</h4>
|
|
120
|
+
<p className={classNames(styles.bodyLong01, styles.text02)}>
|
|
121
|
+
{t('thereIsNoInformationToDisplayHere', 'There is no information to display here')}
|
|
122
|
+
</p>
|
|
123
|
+
</div>
|
|
124
|
+
);
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default EncounterListDataTable;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { SkeletonText } from '@carbon/react';
|
|
5
|
+
import { type Observation } from '../visit.resource';
|
|
6
|
+
import styles from '../visit-detail-overview.scss';
|
|
7
|
+
|
|
8
|
+
interface EncounterObservationsProps {
|
|
9
|
+
observations: Array<Observation>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const EncounterObservations: React.FC<EncounterObservationsProps> = ({ observations }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
|
|
15
|
+
const observationsList = useMemo(() => {
|
|
16
|
+
return (
|
|
17
|
+
observations &&
|
|
18
|
+
observations.map((obs: Observation) => {
|
|
19
|
+
const [question, answer] = obs.display.split(':');
|
|
20
|
+
return { question, answer };
|
|
21
|
+
})
|
|
22
|
+
);
|
|
23
|
+
}, [observations]);
|
|
24
|
+
|
|
25
|
+
return observationsList ? (
|
|
26
|
+
observationsList.length > 0 ? (
|
|
27
|
+
<div className={styles.observation}>
|
|
28
|
+
{observationsList.map((obs, ind) => (
|
|
29
|
+
<React.Fragment key={ind}>
|
|
30
|
+
<span className={styles.caption01}>{obs.question}: </span>
|
|
31
|
+
<span className={classNames(styles.bodyShort02, styles.text01)}>{obs.answer}</span>
|
|
32
|
+
</React.Fragment>
|
|
33
|
+
))}
|
|
34
|
+
</div>
|
|
35
|
+
) : (
|
|
36
|
+
<p className={styles.caption01}>{t('noObservationsFound', 'No observations found')}</p>
|
|
37
|
+
)
|
|
38
|
+
) : (
|
|
39
|
+
<SkeletonText />
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default EncounterObservations;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import EncounterObservations from './encounter-observations.component';
|
|
4
|
+
|
|
5
|
+
describe('EncounterObservations', () => {
|
|
6
|
+
test('renders skeleton text while loading', () => {
|
|
7
|
+
render(<EncounterObservations observations={null} />);
|
|
8
|
+
|
|
9
|
+
expect(screen.queryByText('Temperature')).not.toBeInTheDocument();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('renders "No observations found" message when observations list is empty', () => {
|
|
13
|
+
const emptyObservations = [];
|
|
14
|
+
render(<EncounterObservations observations={emptyObservations} />);
|
|
15
|
+
|
|
16
|
+
expect(screen.getByText('No observations found')).toBeInTheDocument();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('renders observations list correctly', () => {
|
|
20
|
+
const observations = [
|
|
21
|
+
{ display: 'Temperature: 98.6°F' },
|
|
22
|
+
{ display: 'Blood Pressure: 120/80 mmHg' },
|
|
23
|
+
{ display: 'Heart Rate: 72 bpm' },
|
|
24
|
+
];
|
|
25
|
+
render(<EncounterObservations observations={observations} />);
|
|
26
|
+
|
|
27
|
+
expect(screen.getByText('Temperature:')).toBeInTheDocument();
|
|
28
|
+
expect(screen.getByText('98.6°F')).toBeInTheDocument();
|
|
29
|
+
|
|
30
|
+
expect(screen.getByText('Blood Pressure:')).toBeInTheDocument();
|
|
31
|
+
expect(screen.getByText('120/80 mmHg')).toBeInTheDocument();
|
|
32
|
+
|
|
33
|
+
expect(screen.getByText('Heart Rate:')).toBeInTheDocument();
|
|
34
|
+
expect(screen.getByText('72 bpm')).toBeInTheDocument();
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import capitalize from 'lodash-es/capitalize';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
import { formatDate, formatTime, parseDate } from '@openmrs/esm-framework';
|
|
6
|
+
import { type OrderItem, getDosage } from '../visit.resource';
|
|
7
|
+
import styles from '../visit-detail-overview.scss';
|
|
8
|
+
|
|
9
|
+
interface MedicationSummaryProps {
|
|
10
|
+
medications: Array<OrderItem>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MedicationSummary: React.FC<MedicationSummaryProps> = ({ medications }) => {
|
|
14
|
+
const { t } = useTranslation();
|
|
15
|
+
|
|
16
|
+
return (
|
|
17
|
+
<React.Fragment>
|
|
18
|
+
{medications.length > 0 ? (
|
|
19
|
+
<div className={styles.medicationRecord}>
|
|
20
|
+
{medications.map(
|
|
21
|
+
(medication, i) =>
|
|
22
|
+
medication.order?.dose &&
|
|
23
|
+
medication.order?.orderType?.display === 'Drug Order' && (
|
|
24
|
+
<React.Fragment key={i}>
|
|
25
|
+
<div className={styles.medicationContainer}>
|
|
26
|
+
<div>
|
|
27
|
+
<p className={styles.bodyLong01}>
|
|
28
|
+
<strong>{capitalize(medication?.order?.drug?.name)}</strong>{' '}
|
|
29
|
+
{medication?.order?.drug?.strength && (
|
|
30
|
+
<>— {medication?.order?.drug?.strength?.toLowerCase()}</>
|
|
31
|
+
)}{' '}
|
|
32
|
+
{medication?.order?.doseUnits?.display && (
|
|
33
|
+
<>— {medication?.order?.doseUnits?.display?.toLowerCase()}</>
|
|
34
|
+
)}{' '}
|
|
35
|
+
</p>
|
|
36
|
+
<p className={styles.bodyLong01}>
|
|
37
|
+
<span className={styles.label01}> {t('dose', 'Dose').toUpperCase()} </span>{' '}
|
|
38
|
+
<span className={styles.dosage}>
|
|
39
|
+
{medication?.order?.dose} {medication?.order?.doseUnits?.display?.toLowerCase()}
|
|
40
|
+
</span>{' '}
|
|
41
|
+
{medication.order?.route?.display && (
|
|
42
|
+
<span>— {medication?.order?.route?.display?.toLowerCase()} — </span>
|
|
43
|
+
)}
|
|
44
|
+
{medication?.order?.frequency?.display?.toLowerCase()} —{' '}
|
|
45
|
+
{!medication?.order?.duration
|
|
46
|
+
? t('orderIndefiniteDuration', 'Indefinite duration')
|
|
47
|
+
: t('orderDurationAndUnit', 'for {{duration}} {{durationUnit}}', {
|
|
48
|
+
duration: medication?.order?.duration,
|
|
49
|
+
durationUnit: medication?.order?.durationUnits?.display?.toLowerCase(),
|
|
50
|
+
})}
|
|
51
|
+
{medication?.order?.numRefills !== 0 && (
|
|
52
|
+
<span>
|
|
53
|
+
<span className={styles.label01}> — {t('refills', 'Refills').toUpperCase()}</span>{' '}
|
|
54
|
+
{medication?.order?.numRefills}
|
|
55
|
+
{''}
|
|
56
|
+
</span>
|
|
57
|
+
)}
|
|
58
|
+
{medication?.order?.dosingInstructions && (
|
|
59
|
+
<span> — {medication?.order?.dosingInstructions?.toLocaleLowerCase()}</span>
|
|
60
|
+
)}
|
|
61
|
+
</p>
|
|
62
|
+
<p className={styles.bodyLong01}>
|
|
63
|
+
{medication?.order?.orderReasonNonCoded ? (
|
|
64
|
+
<span>
|
|
65
|
+
<span className={styles.label01}>{t('indication', 'Indication').toUpperCase()}</span>{' '}
|
|
66
|
+
{medication?.order?.orderReasonNonCoded}
|
|
67
|
+
</span>
|
|
68
|
+
) : null}
|
|
69
|
+
{medication?.order?.quantity ? (
|
|
70
|
+
<span>
|
|
71
|
+
<span className={styles.label01}> — {t('quantity', 'Quantity').toUpperCase()}</span>{' '}
|
|
72
|
+
{medication?.order?.quantity}
|
|
73
|
+
</span>
|
|
74
|
+
) : null}
|
|
75
|
+
{medication?.order?.dateStopped ? (
|
|
76
|
+
<span className={styles.bodyShort01}>
|
|
77
|
+
<span className={styles.label01}>
|
|
78
|
+
{medication?.order?.quantity ? ` — ` : ''} {t('endDate', 'End date').toUpperCase()}
|
|
79
|
+
</span>{' '}
|
|
80
|
+
{formatDate(new Date(medication?.order?.dateStopped))}
|
|
81
|
+
</span>
|
|
82
|
+
) : null}
|
|
83
|
+
</p>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<p className={styles.metadata}>
|
|
88
|
+
{formatTime(parseDate(medication?.order?.dateActivated))}
|
|
89
|
+
{medication?.provider?.name && <> · {medication?.provider?.name}</>}
|
|
90
|
+
{medication?.provider?.role && <>, {medication?.provider?.role}</>}
|
|
91
|
+
</p>
|
|
92
|
+
</React.Fragment>
|
|
93
|
+
),
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
) : (
|
|
97
|
+
<p className={classNames(styles.bodyLong01, styles.text02)}>
|
|
98
|
+
{t('noMedicationsFound', 'No medications found')}
|
|
99
|
+
</p>
|
|
100
|
+
)}
|
|
101
|
+
</React.Fragment>
|
|
102
|
+
);
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
export default MedicationSummary;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Layer, Tile } from '@carbon/react';
|
|
5
|
+
import { isDesktop, useLayoutType } from '@openmrs/esm-framework';
|
|
6
|
+
import type { Note } from '../visit.resource';
|
|
7
|
+
import { EmptyDataIllustration } from '../../active-visits-widget/empty-data-illustration.component';
|
|
8
|
+
import styles from '../visit-detail-overview.scss';
|
|
9
|
+
|
|
10
|
+
interface NotesSummaryProps {
|
|
11
|
+
notes: Array<Note>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const NotesSummary: React.FC<NotesSummaryProps> = ({ notes }) => {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const layout = useLayoutType();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<>
|
|
20
|
+
{notes.length ? (
|
|
21
|
+
notes.map((note: Note, i) => (
|
|
22
|
+
<div className={styles.notesContainer} key={i}>
|
|
23
|
+
<p className={classNames(styles.noteText, styles.bodyLong01)} data-testid="note">
|
|
24
|
+
{note.note}
|
|
25
|
+
</p>
|
|
26
|
+
<p className={styles.metadata}>
|
|
27
|
+
{note.time} {note.provider.name ? <span>· {note.provider.name} </span> : null}
|
|
28
|
+
{note.provider.role ? <span>· {note.provider.role}</span> : null}
|
|
29
|
+
</p>
|
|
30
|
+
</div>
|
|
31
|
+
))
|
|
32
|
+
) : (
|
|
33
|
+
<div className={styles.emptyStateContainer}>
|
|
34
|
+
<Layer>
|
|
35
|
+
<Tile className={styles.tile}>
|
|
36
|
+
<div className={!isDesktop(layout) ? styles.tabletHeading : styles.desktopHeading}>
|
|
37
|
+
<h4>{t('notes', 'Notes')}</h4>
|
|
38
|
+
</div>
|
|
39
|
+
<EmptyDataIllustration />
|
|
40
|
+
<p className={styles.emptyStateContent}>
|
|
41
|
+
{t('noNotesToShowForPatient', 'There are no notes to display for this patient')}
|
|
42
|
+
</p>
|
|
43
|
+
</Tile>
|
|
44
|
+
</Layer>
|
|
45
|
+
</div>
|
|
46
|
+
)}
|
|
47
|
+
</>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export default NotesSummary;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { ExtensionSlot } from '@openmrs/esm-framework';
|
|
3
|
+
import { type Encounter } from '../visit.resource';
|
|
4
|
+
import styles from '../visit-detail-overview.scss';
|
|
5
|
+
|
|
6
|
+
const TestsSummary = ({ patientUuid, encounters }: { patientUuid: string; encounters: Array<Encounter> }) => {
|
|
7
|
+
const filter = useMemo(() => {
|
|
8
|
+
const encounterIds = encounters.map((e) => `Encounter/${e.uuid}`);
|
|
9
|
+
return ([entry]) => {
|
|
10
|
+
return encounterIds.includes(entry.encounter?.reference);
|
|
11
|
+
};
|
|
12
|
+
}, [encounters]);
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<div className={styles.bodyLong01}>
|
|
16
|
+
<ExtensionSlot name="test-results-filtered-overview-slot" state={{ filter, patientUuid }} />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export default TestsSummary;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import React, { useState, useMemo } from 'react';
|
|
2
|
+
import classNames from 'classnames';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
import { Tab, Tabs, TabList, TabPanel, TabPanels, Tag } from '@carbon/react';
|
|
5
|
+
import { type OpenmrsResource, formatTime, parseDate } from '@openmrs/esm-framework';
|
|
6
|
+
import NotesSummary from './notes-summary.component';
|
|
7
|
+
import MedicationSummary from './medications-summary.component';
|
|
8
|
+
import TestsSummary from './tests-summary.component';
|
|
9
|
+
import { type Order, type Encounter, type Note, type Observation, type OrderItem } from '../visit.resource';
|
|
10
|
+
import styles from '../visit-detail-overview.scss';
|
|
11
|
+
|
|
12
|
+
interface DiagnosisItem {
|
|
13
|
+
diagnosis: string;
|
|
14
|
+
order: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface VisitSummaryProps {
|
|
18
|
+
encounters: Array<Encounter | OpenmrsResource>;
|
|
19
|
+
patientUuid: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const VisitSummary: React.FC<VisitSummaryProps> = ({ encounters, patientUuid }) => {
|
|
23
|
+
const { t } = useTranslation();
|
|
24
|
+
const [tabSelected, setSelectedTab] = useState(0);
|
|
25
|
+
|
|
26
|
+
const [diagnoses, notes, medications]: [Array<DiagnosisItem>, Array<Note>, Array<OrderItem>] = useMemo(() => {
|
|
27
|
+
// Medication Tab
|
|
28
|
+
const medications: Array<OrderItem> = [];
|
|
29
|
+
// Diagnoses in a Visit
|
|
30
|
+
const diagnoses: Array<DiagnosisItem> = [];
|
|
31
|
+
// Notes Tab
|
|
32
|
+
const notes: Array<Note> = [];
|
|
33
|
+
|
|
34
|
+
// Iterating through every Encounter
|
|
35
|
+
encounters.forEach((enc: Encounter) => {
|
|
36
|
+
// Orders of every encounter put in a single array.
|
|
37
|
+
medications.push(
|
|
38
|
+
...enc.orders.map((order: Order) => ({
|
|
39
|
+
order,
|
|
40
|
+
provider: {
|
|
41
|
+
name: enc.encounterProviders.length ? enc.encounterProviders[0].provider.person.display : '',
|
|
42
|
+
role: enc.encounterProviders.length ? enc.encounterProviders[0].encounterRole.display : '',
|
|
43
|
+
},
|
|
44
|
+
})),
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// Check for Visit Diagnoses and Notes
|
|
48
|
+
if (enc.encounterType.display === 'Visit Note') {
|
|
49
|
+
enc.obs.forEach((obs: Observation) => {
|
|
50
|
+
if (obs.concept.display === 'Visit Diagnoses') {
|
|
51
|
+
// Putting all the diagnoses in a single array.
|
|
52
|
+
diagnoses.push({
|
|
53
|
+
diagnosis: obs.groupMembers.find((mem) => mem.concept.display === 'PROBLEM LIST').value.display,
|
|
54
|
+
order: obs.groupMembers.find((mem) => mem.concept.display === 'Diagnosis order').value.display,
|
|
55
|
+
});
|
|
56
|
+
} else if (obs.concept.display === 'Text of encounter note') {
|
|
57
|
+
// Putting all notes in a single array.
|
|
58
|
+
notes.push({
|
|
59
|
+
note: obs.value,
|
|
60
|
+
provider: {
|
|
61
|
+
name: enc.encounterProviders.length ? enc.encounterProviders[0].provider.person.display : '',
|
|
62
|
+
role: enc.encounterProviders.length ? enc.encounterProviders[0].encounterRole.display : '',
|
|
63
|
+
},
|
|
64
|
+
time: formatTime(parseDate(obs.obsDatetime)),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
return [diagnoses, notes, medications];
|
|
71
|
+
}, [encounters]);
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div className={styles.summaryContainer}>
|
|
75
|
+
<p className={styles.diagnosisLabel}>{t('diagnoses', 'Diagnoses')}</p>
|
|
76
|
+
|
|
77
|
+
<div className={styles.diagnosesList}>
|
|
78
|
+
{diagnoses.length > 0 ? (
|
|
79
|
+
diagnoses.map((diagnosis, i) => (
|
|
80
|
+
<Tag key={i} type={diagnosis.order === 'Primary' ? 'blue' : 'red'}>
|
|
81
|
+
{diagnosis.diagnosis}
|
|
82
|
+
</Tag>
|
|
83
|
+
))
|
|
84
|
+
) : (
|
|
85
|
+
<p className={classNames(styles.bodyLong01, styles.text02)} style={{ marginBottom: '0.5rem' }}>
|
|
86
|
+
{t('noDiagnosesFound', 'No diagnoses found')}
|
|
87
|
+
</p>
|
|
88
|
+
)}
|
|
89
|
+
</div>
|
|
90
|
+
<Tabs className={styles.verticalTabs}>
|
|
91
|
+
<TabList aria-label="Visit summary tabs" className={styles.tablist}>
|
|
92
|
+
<Tab className={classNames(styles.tab, styles.bodyLong01)} onClick={() => setSelectedTab(0)} id="notes-tab">
|
|
93
|
+
{t('notes', 'Notes')}
|
|
94
|
+
</Tab>
|
|
95
|
+
<Tab className={styles.tab} onClick={() => setSelectedTab(1)} id="tests-tab">
|
|
96
|
+
{t('tests', 'Tests')}
|
|
97
|
+
</Tab>
|
|
98
|
+
<Tab className={styles.tab} onClick={() => setSelectedTab(2)} id="tab-3">
|
|
99
|
+
{t('medications', 'Medications')}
|
|
100
|
+
</Tab>
|
|
101
|
+
</TabList>
|
|
102
|
+
<TabPanels>
|
|
103
|
+
<TabPanel>
|
|
104
|
+
<NotesSummary notes={notes} />
|
|
105
|
+
</TabPanel>
|
|
106
|
+
<TabPanel>
|
|
107
|
+
<TestsSummary patientUuid={patientUuid} encounters={encounters as Array<Encounter>} />
|
|
108
|
+
</TabPanel>
|
|
109
|
+
<TabPanel>
|
|
110
|
+
<MedicationSummary medications={medications} />
|
|
111
|
+
</TabPanel>
|
|
112
|
+
</TabPanels>
|
|
113
|
+
</Tabs>
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export default VisitSummary;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activeVisits": "Active Visits",
|
|
3
|
+
"age": "Age",
|
|
4
|
+
"allEncounters": "All Encounters",
|
|
5
|
+
"checkFilters": "Check the filters above",
|
|
6
|
+
"diagnoses": "Diagnoses",
|
|
7
|
+
"dose": "Dose",
|
|
8
|
+
"encounterType": "Encounter Type",
|
|
9
|
+
"endDate": "End date",
|
|
10
|
+
"filterTable": "Filter table",
|
|
11
|
+
"gender": "Gender",
|
|
12
|
+
"idNumber": "ID Number",
|
|
13
|
+
"indication": "Indication",
|
|
14
|
+
"medications": "Medications",
|
|
15
|
+
"name": "Name",
|
|
16
|
+
"noActiveVisitsForLocation": "There are no active visits to display for this location.",
|
|
17
|
+
"noDiagnosesFound": "No diagnoses found",
|
|
18
|
+
"noEncountersFound": "No encounters found",
|
|
19
|
+
"noMedicationsFound": "No medications found",
|
|
20
|
+
"noNotesToShowForPatient": "There are no notes to display for this patient",
|
|
21
|
+
"noObservationsFound": "No observations found",
|
|
22
|
+
"notes": "Notes",
|
|
23
|
+
"noVisitsToDisplay": "No visits to display",
|
|
24
|
+
"orderDurationAndUnit": "for {{duration}} {{durationUnit}}",
|
|
25
|
+
"orderIndefiniteDuration": "Indefinite duration",
|
|
26
|
+
"provider": "Provider",
|
|
27
|
+
"quantity": "Quantity",
|
|
28
|
+
"refills": "Refills",
|
|
29
|
+
"tests": "Tests",
|
|
30
|
+
"thereIsNoInformationToDisplayHere": "There is no information to display here",
|
|
31
|
+
"time": "Time",
|
|
32
|
+
"visitStartTime": "Visit Time",
|
|
33
|
+
"visitSummary": "Visit Summary",
|
|
34
|
+
"visitType": "Visit Type"
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activeVisits": "الزيارات النشطة",
|
|
3
|
+
"age": "العمر",
|
|
4
|
+
"allEncounters": "جميع اللقاءات",
|
|
5
|
+
"checkFilters": "تحقق من الفلاتر أعلاه",
|
|
6
|
+
"diagnoses": "التشخيصات",
|
|
7
|
+
"dose": "الجرعة",
|
|
8
|
+
"encounterType": "نوع اللقاء",
|
|
9
|
+
"endDate": "End date",
|
|
10
|
+
"filterTable": "فلتر الجدول",
|
|
11
|
+
"gender": "الجنس",
|
|
12
|
+
"idNumber": "رقم الهوية",
|
|
13
|
+
"indication": "Indication",
|
|
14
|
+
"medications": "الأدوية",
|
|
15
|
+
"name": "الاسم",
|
|
16
|
+
"noActiveVisitsForLocation": "لا توجد زيارات نشطة لعرضها لهذا الموقع.",
|
|
17
|
+
"noDiagnosesFound": "لم يتم العثور على تشخيصات",
|
|
18
|
+
"noEncountersFound": "لم يتم العثور على لقاءات",
|
|
19
|
+
"noMedicationsFound": "لم يتم العثور على أدوية",
|
|
20
|
+
"noNotesToShowForPatient": "There are no notes to display for this patient",
|
|
21
|
+
"noObservationsFound": "لم يتم العثور على ملاحظات",
|
|
22
|
+
"notes": "الملاحظات",
|
|
23
|
+
"noVisitsToDisplay": "لا زيارات للعرض",
|
|
24
|
+
"orderDurationAndUnit": "لمدة {{duration}} {{durationUnit}}",
|
|
25
|
+
"orderIndefiniteDuration": "مدة غير محددة",
|
|
26
|
+
"provider": "مقدم الخدمة",
|
|
27
|
+
"quantity": "Quantity",
|
|
28
|
+
"refills": "إعادة التعبئة",
|
|
29
|
+
"tests": "الاختبارات",
|
|
30
|
+
"thereIsNoInformationToDisplayHere": "لا توجد معلومات لعرضها هنا",
|
|
31
|
+
"time": "الوقت",
|
|
32
|
+
"visitStartTime": "وقت الزيارة",
|
|
33
|
+
"visitSummary": "ملخص الزيارة",
|
|
34
|
+
"visitType": "نوع الزيارة"
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activeVisits": "Active Visits",
|
|
3
|
+
"age": "Age",
|
|
4
|
+
"allEncounters": "All Encounters",
|
|
5
|
+
"checkFilters": "Check the filters above",
|
|
6
|
+
"diagnoses": "Diagnoses",
|
|
7
|
+
"dose": "Dose",
|
|
8
|
+
"encounterType": "Encounter Type",
|
|
9
|
+
"endDate": "End date",
|
|
10
|
+
"filterTable": "Filter table",
|
|
11
|
+
"gender": "Gender",
|
|
12
|
+
"idNumber": "ID Number",
|
|
13
|
+
"indication": "Indication",
|
|
14
|
+
"medications": "Medications",
|
|
15
|
+
"name": "Name",
|
|
16
|
+
"noActiveVisitsForLocation": "There are no active visits to display for this location.",
|
|
17
|
+
"noDiagnosesFound": "No diagnoses found",
|
|
18
|
+
"noEncountersFound": "No encounters found",
|
|
19
|
+
"noMedicationsFound": "No medications found",
|
|
20
|
+
"noNotesToShowForPatient": "There are no notes to display for this patient",
|
|
21
|
+
"noObservationsFound": "No observations found",
|
|
22
|
+
"notes": "Notes",
|
|
23
|
+
"noVisitsToDisplay": "No visits to display",
|
|
24
|
+
"orderDurationAndUnit": "for {{duration}} {{durationUnit}}",
|
|
25
|
+
"orderIndefiniteDuration": "Indefinite duration",
|
|
26
|
+
"provider": "Provider",
|
|
27
|
+
"quantity": "Quantity",
|
|
28
|
+
"refills": "Refills",
|
|
29
|
+
"tests": "Tests",
|
|
30
|
+
"thereIsNoInformationToDisplayHere": "There is no information to display here",
|
|
31
|
+
"time": "Time",
|
|
32
|
+
"visitStartTime": "Visit Time",
|
|
33
|
+
"visitSummary": "Visit Summary",
|
|
34
|
+
"visitType": "Visit Type"
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"activeVisits": "Visitas Activas",
|
|
3
|
+
"age": "Edad",
|
|
4
|
+
"allEncounters": "Todos Los Encuentros",
|
|
5
|
+
"checkFilters": "Compruebe los filtros anteriores",
|
|
6
|
+
"diagnoses": "Diagnósticos",
|
|
7
|
+
"dose": "Dosis",
|
|
8
|
+
"encounterType": "Tipo De Encuentro",
|
|
9
|
+
"endDate": "End date",
|
|
10
|
+
"filterTable": "Filtrar la tabla",
|
|
11
|
+
"gender": "Género",
|
|
12
|
+
"idNumber": "Número ID",
|
|
13
|
+
"indication": "Indication",
|
|
14
|
+
"medications": "Medicaciones",
|
|
15
|
+
"name": "Nombre",
|
|
16
|
+
"noActiveVisitsForLocation": "No hay visitas activas para esta ubicación.",
|
|
17
|
+
"noDiagnosesFound": "No se encontraron diagnósticos",
|
|
18
|
+
"noEncountersFound": "No se encontraron encuentros",
|
|
19
|
+
"noMedicationsFound": "No se encontraron medicaciones",
|
|
20
|
+
"noNotesToShowForPatient": "There are no notes to display for this patient",
|
|
21
|
+
"noObservationsFound": "No se encontraron observaciones",
|
|
22
|
+
"notes": "Notas",
|
|
23
|
+
"noVisitsToDisplay": "No hay visitas para mostrar",
|
|
24
|
+
"orderDurationAndUnit": "para {duración} {duraciónUnidad}",
|
|
25
|
+
"orderIndefiniteDuration": "Duración indefinida",
|
|
26
|
+
"provider": "Proveedor",
|
|
27
|
+
"quantity": "Quantity",
|
|
28
|
+
"refills": "Recargas",
|
|
29
|
+
"tests": "Pruebas",
|
|
30
|
+
"thereIsNoInformationToDisplayHere": "No hay información para mostrar aquí",
|
|
31
|
+
"time": "Tiempo",
|
|
32
|
+
"visitStartTime": "Tiempo de Visita",
|
|
33
|
+
"visitSummary": "Resumen de Visita",
|
|
34
|
+
"visitType": "Tipo de Visita"
|
|
35
|
+
}
|