@openmrs/esm-ward-app 9.2.1-pre.7455 → 9.2.1-pre.7460
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 +2 -2
- package/dist/1011.js +1 -1
- package/dist/1011.js.map +1 -1
- package/dist/1369.js +1 -1
- package/dist/1369.js.map +1 -1
- package/dist/1554.js +1 -0
- package/dist/1554.js.map +1 -0
- package/dist/1895.js +1 -1
- package/dist/1895.js.map +1 -1
- package/dist/2117.js +1 -1
- package/dist/2117.js.map +1 -1
- package/dist/2640.js +1 -1
- package/dist/2640.js.map +1 -1
- package/dist/3624.js +1 -1
- package/dist/3948.js +11 -0
- package/dist/3948.js.map +1 -0
- package/dist/4285.js +1 -1
- package/dist/4285.js.map +1 -1
- package/dist/4373.js +1 -1
- package/dist/6026.js +1 -1
- package/dist/6026.js.map +1 -1
- package/dist/6199.js +1 -1
- package/dist/6285.js +1 -0
- package/dist/6285.js.map +1 -0
- package/dist/6515.js +1 -1
- package/dist/7073.js +1 -0
- package/dist/7073.js.map +1 -0
- package/dist/7607.js +1 -1
- package/dist/7715.js +1 -1
- package/dist/7715.js.map +1 -1
- package/dist/{3954.js → 8622.js} +17 -17
- package/dist/8622.js.map +1 -0
- package/dist/9727.js +1 -1
- package/dist/9727.js.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +1 -1
- package/dist/openmrs-esm-ward-app.js +3 -3
- package/dist/openmrs-esm-ward-app.js.buildmanifest.json +126 -102
- package/dist/routes.json +1 -1
- package/package.json +2 -2
- package/src/index.ts +5 -0
- package/src/routes.json +6 -0
- package/src/types/index.ts +1 -0
- package/src/ward-workspace/ward-patient-notes/history/delete-note.modal.tsx +63 -0
- package/src/ward-workspace/ward-patient-notes/history/note.component.tsx +138 -13
- package/src/ward-workspace/ward-patient-notes/history/notes-container.component.tsx +14 -8
- package/src/ward-workspace/ward-patient-notes/history/notes-container.test.tsx +20 -13
- package/src/ward-workspace/ward-patient-notes/history/styles.scss +16 -3
- package/src/ward-workspace/ward-patient-notes/notes.resource.ts +17 -4
- package/src/ward-workspace/ward-patient-notes/notes.scss +5 -1
- package/src/ward-workspace/ward-patient-notes/notes.test.tsx +7 -7
- package/src/ward-workspace/ward-patient-notes/notes.workspace.tsx +26 -17
- package/src/ward-workspace/ward-patient-notes/types.ts +5 -1
- package/src/ward.resource.ts +6 -0
- package/translations/en.json +6 -0
- package/dist/3954.js.map +0 -1
- package/dist/5420.js +0 -6
- package/dist/5420.js.map +0 -1
- package/dist/6203.js +0 -1
- package/dist/6203.js.map +0 -1
- package/dist/7965.js +0 -1
- package/dist/7965.js.map +0 -1
|
@@ -1,8 +1,27 @@
|
|
|
1
|
-
import React from 'react';
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
|
2
2
|
import dayjs from 'dayjs';
|
|
3
3
|
import { useTranslation } from 'react-i18next';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
SkeletonText,
|
|
6
|
+
Tile,
|
|
7
|
+
OverflowMenu,
|
|
8
|
+
OverflowMenuItem,
|
|
9
|
+
Stack,
|
|
10
|
+
TextArea,
|
|
11
|
+
Button,
|
|
12
|
+
Layer,
|
|
13
|
+
InlineLoading,
|
|
14
|
+
} from '@carbon/react';
|
|
15
|
+
import {
|
|
16
|
+
getCoreTranslation,
|
|
17
|
+
isDesktop,
|
|
18
|
+
showModal,
|
|
19
|
+
showSnackbar,
|
|
20
|
+
useEmrConfiguration,
|
|
21
|
+
useLayoutType,
|
|
22
|
+
} from '@openmrs/esm-framework';
|
|
5
23
|
import { type PatientNote } from '../types';
|
|
24
|
+
import { editPatientNote } from '../notes.resource';
|
|
6
25
|
import styles from './styles.scss';
|
|
7
26
|
|
|
8
27
|
export const InPatientNoteSkeleton: React.FC = () => {
|
|
@@ -20,27 +39,133 @@ export const InPatientNoteSkeleton: React.FC = () => {
|
|
|
20
39
|
};
|
|
21
40
|
|
|
22
41
|
interface InPatientNoteProps {
|
|
42
|
+
mutatePatientNotes(): void;
|
|
23
43
|
note: PatientNote;
|
|
44
|
+
promptBeforeClosing(hasUnsavedChanges: boolean): void;
|
|
24
45
|
}
|
|
25
46
|
|
|
26
|
-
|
|
47
|
+
/**
|
|
48
|
+
* This component shows a note (obs) created with concept as either:
|
|
49
|
+
* - `consultFreeTextCommentsConcept` from emrapi configuration
|
|
50
|
+
* - one of the concepts defined in additionalInpatientNotesConceptUuids.
|
|
51
|
+
*
|
|
52
|
+
* Note that only notes with encounter type emrConfiguration.inpatientNoteEncounterType are creatable,
|
|
53
|
+
* editable and deletable from the ward app.
|
|
54
|
+
*
|
|
55
|
+
*/
|
|
56
|
+
const InPatientNote: React.FC<InPatientNoteProps> = ({ note, mutatePatientNotes, promptBeforeClosing }) => {
|
|
27
57
|
const { t } = useTranslation();
|
|
28
58
|
const formattedDate = note.encounterNoteRecordedAt
|
|
29
59
|
? dayjs(note.encounterNoteRecordedAt).format('dddd, D MMM YYYY')
|
|
30
60
|
: '';
|
|
31
61
|
const formattedTime = note.encounterNoteRecordedAt ? dayjs(note.encounterNoteRecordedAt).format('HH:mm') : '';
|
|
62
|
+
const [editMode, setEditMode] = useState(false);
|
|
63
|
+
const [editedNote, setEditedNote] = useState(note.encounterNote);
|
|
64
|
+
const isTablet = !isDesktop(useLayoutType());
|
|
65
|
+
const [isSaving, setIsSaving] = useState(false);
|
|
66
|
+
const { emrConfiguration } = useEmrConfiguration();
|
|
67
|
+
const isInpatientNoteEncounter = note.encounterTypeUuid === emrConfiguration?.inpatientNoteEncounterType?.uuid;
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
promptBeforeClosing(editMode);
|
|
71
|
+
}, [editMode, promptBeforeClosing]);
|
|
72
|
+
|
|
73
|
+
const onSave = async () => {
|
|
74
|
+
try {
|
|
75
|
+
setIsSaving(true);
|
|
76
|
+
await editPatientNote(note.obsUuid, editedNote);
|
|
77
|
+
setEditMode(false);
|
|
78
|
+
showSnackbar({
|
|
79
|
+
isLowContrast: true,
|
|
80
|
+
kind: 'success',
|
|
81
|
+
subtitle: t('patientNoteNowVisible', 'It should be now visible in the notes history'),
|
|
82
|
+
title: t('visitNoteSaved', 'Patient note saved'),
|
|
83
|
+
});
|
|
84
|
+
mutatePatientNotes();
|
|
85
|
+
} catch (e) {
|
|
86
|
+
showSnackbar({
|
|
87
|
+
isLowContrast: true,
|
|
88
|
+
kind: 'error',
|
|
89
|
+
subtitle: e?.responseBody?.error?.translatedMessage ?? e?.responseBody?.error?.message,
|
|
90
|
+
title: t('errorSavingPatientNote', 'Error saving patient note'),
|
|
91
|
+
});
|
|
92
|
+
} finally {
|
|
93
|
+
setIsSaving(false);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
32
96
|
|
|
33
97
|
return (
|
|
34
|
-
<
|
|
35
|
-
<
|
|
36
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
98
|
+
<div className={styles.noteTile}>
|
|
99
|
+
<Stack gap={4}>
|
|
100
|
+
<div className={styles.noteHeader}>
|
|
101
|
+
<span className={styles.noteDateAndTime}>
|
|
102
|
+
{formattedDate}, {formattedTime}
|
|
103
|
+
</span>
|
|
104
|
+
{isInpatientNoteEncounter && (
|
|
105
|
+
<OverflowMenu className={styles.overflowMenu} flipped>
|
|
106
|
+
{!editMode && note.obsUuid && (
|
|
107
|
+
<OverflowMenuItem
|
|
108
|
+
aria-label={getCoreTranslation('edit')}
|
|
109
|
+
id={'edit note-' + note.encounterUuid}
|
|
110
|
+
className={styles.menuItem}
|
|
111
|
+
hasDivider
|
|
112
|
+
itemText={getCoreTranslation('edit')}
|
|
113
|
+
onClick={() => {
|
|
114
|
+
setEditMode(true);
|
|
115
|
+
}}
|
|
116
|
+
/>
|
|
117
|
+
)}
|
|
118
|
+
<OverflowMenuItem
|
|
119
|
+
aria-label={getCoreTranslation('delete')}
|
|
120
|
+
id={'delete-note-' + note.encounterUuid}
|
|
121
|
+
isDelete
|
|
122
|
+
className={styles.menuItem}
|
|
123
|
+
itemText={getCoreTranslation('delete')}
|
|
124
|
+
onClick={() => {
|
|
125
|
+
const dispose = showModal('delete-note-modal', {
|
|
126
|
+
close: () => dispose(),
|
|
127
|
+
encounterUuid: note.encounterUuid,
|
|
128
|
+
onDelete: () => mutatePatientNotes(),
|
|
129
|
+
});
|
|
130
|
+
}}
|
|
131
|
+
/>
|
|
132
|
+
</OverflowMenu>
|
|
133
|
+
)}
|
|
134
|
+
</div>
|
|
135
|
+
{editMode ? (
|
|
136
|
+
<Layer>
|
|
137
|
+
<Stack gap={3}>
|
|
138
|
+
<TextArea
|
|
139
|
+
className={styles.textArea}
|
|
140
|
+
rows={6}
|
|
141
|
+
value={editedNote}
|
|
142
|
+
onChange={(e) => setEditedNote(e.target.value)}
|
|
143
|
+
labelText={t('editNote', 'Edit note')}
|
|
144
|
+
/>
|
|
145
|
+
<div className={styles.editButtons}>
|
|
146
|
+
<Button
|
|
147
|
+
onClick={() => {
|
|
148
|
+
setEditMode(false);
|
|
149
|
+
setEditedNote(note.encounterNote); // Reset to original note on cancel
|
|
150
|
+
}}
|
|
151
|
+
kind={'secondary'}
|
|
152
|
+
size={isTablet ? 'lg' : 'sm'}>
|
|
153
|
+
{t('cancel', 'Cancel')}
|
|
154
|
+
</Button>
|
|
155
|
+
<Button onClick={onSave} kind={'primary'} size={isTablet ? 'lg' : 'sm'}>
|
|
156
|
+
{isSaving ? <InlineLoading description={t('saving', 'Saving...')} /> : t('save', 'Save')}
|
|
157
|
+
</Button>
|
|
158
|
+
</div>
|
|
159
|
+
</Stack>
|
|
160
|
+
</Layer>
|
|
161
|
+
) : (
|
|
162
|
+
<>
|
|
163
|
+
<div className={styles.noteBody}>{note.encounterNote}</div>
|
|
164
|
+
<div className={styles.noteProviderName}>{note.encounterProvider}</div>
|
|
165
|
+
</>
|
|
166
|
+
)}
|
|
167
|
+
</Stack>
|
|
168
|
+
</div>
|
|
44
169
|
);
|
|
45
170
|
};
|
|
46
171
|
|
|
@@ -1,27 +1,28 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
|
-
import { type PatientUuid
|
|
3
|
+
import { useConfig, useEmrConfiguration, type PatientUuid } from '@openmrs/esm-framework';
|
|
4
4
|
import { usePatientNotes } from '../notes.resource';
|
|
5
5
|
import InPatientNote, { InPatientNoteSkeleton } from './note.component';
|
|
6
6
|
import styles from './styles.scss';
|
|
7
7
|
import { InlineNotification } from '@carbon/react';
|
|
8
|
-
import useEmrConfiguration from '../../../hooks/useEmrConfiguration';
|
|
9
8
|
import { type WardConfigObject } from '../../../config-schema';
|
|
10
9
|
|
|
11
10
|
interface PatientNotesHistoryProps {
|
|
12
11
|
patientUuid: PatientUuid;
|
|
13
12
|
visitUuid: string;
|
|
13
|
+
promptBeforeClosing(hasUnsavedChanges: boolean): void;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
const PatientNotesHistory: React.FC<PatientNotesHistoryProps> = ({ patientUuid, visitUuid }) => {
|
|
16
|
+
const PatientNotesHistory: React.FC<PatientNotesHistoryProps> = ({ patientUuid, visitUuid, promptBeforeClosing }) => {
|
|
17
17
|
const { t } = useTranslation();
|
|
18
18
|
const { emrConfiguration, isLoadingEmrConfiguration } = useEmrConfiguration();
|
|
19
19
|
const config = useConfig<WardConfigObject>();
|
|
20
20
|
|
|
21
|
-
const { patientNotes, isLoadingPatientNotes, errorFetchingPatientNotes } = usePatientNotes(
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
const { patientNotes, mutatePatientNotes, isLoadingPatientNotes, errorFetchingPatientNotes } = usePatientNotes(
|
|
22
|
+
patientUuid,
|
|
23
|
+
visitUuid,
|
|
24
|
+
[emrConfiguration?.consultFreeTextCommentsConcept.uuid, ...config.additionalInpatientNotesConceptUuids],
|
|
25
|
+
);
|
|
25
26
|
|
|
26
27
|
const isLoading = isLoadingPatientNotes || isLoadingEmrConfiguration;
|
|
27
28
|
|
|
@@ -34,7 +35,12 @@ const PatientNotesHistory: React.FC<PatientNotesHistoryProps> = ({ patientUuid,
|
|
|
34
35
|
</div>
|
|
35
36
|
{isLoading ? [1, 2, 3, 4].map((item, index) => <InPatientNoteSkeleton key={index} />) : null}
|
|
36
37
|
{patientNotes.map((patientNote) => (
|
|
37
|
-
<InPatientNote
|
|
38
|
+
<InPatientNote
|
|
39
|
+
key={patientNote.encounterUuid}
|
|
40
|
+
note={patientNote}
|
|
41
|
+
mutatePatientNotes={mutatePatientNotes}
|
|
42
|
+
promptBeforeClosing={promptBeforeClosing}
|
|
43
|
+
/>
|
|
38
44
|
))}
|
|
39
45
|
{errorFetchingPatientNotes && (
|
|
40
46
|
<InlineNotification
|
|
@@ -2,16 +2,15 @@ import React from 'react';
|
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import PatientNotesHistory from './notes-container.component';
|
|
4
4
|
import { usePatientNotes } from '../notes.resource';
|
|
5
|
-
import useEmrConfiguration from '../../../hooks/useEmrConfiguration';
|
|
6
5
|
import { emrConfigurationMock } from '__mocks__';
|
|
7
|
-
import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
|
|
6
|
+
import { getDefaultsFromConfigSchema, useConfig, useEmrConfiguration } from '@openmrs/esm-framework';
|
|
8
7
|
import { configSchema, type WardConfigObject } from '../../../config-schema';
|
|
8
|
+
import { type PatientNote } from '../types';
|
|
9
9
|
|
|
10
10
|
const mockUseConfig = jest.mocked(useConfig<WardConfigObject>);
|
|
11
11
|
|
|
12
12
|
const mockedUseEmrConfiguration = jest.mocked(useEmrConfiguration);
|
|
13
|
-
|
|
14
|
-
jest.mock('../../../hooks/useEmrConfiguration', () => jest.fn());
|
|
13
|
+
const mockedUsePatientNotes = jest.mocked(usePatientNotes);
|
|
15
14
|
|
|
16
15
|
jest.mock('../notes.resource', () => ({
|
|
17
16
|
usePatientNotes: jest.fn(),
|
|
@@ -19,20 +18,24 @@ jest.mock('../notes.resource', () => ({
|
|
|
19
18
|
|
|
20
19
|
const mockPatientUuid = 'sample-patient-uuid';
|
|
21
20
|
|
|
22
|
-
const mockPatientNotes = [
|
|
21
|
+
const mockPatientNotes: PatientNote[] = [
|
|
23
22
|
{
|
|
24
|
-
|
|
25
|
-
diagnoses: '',
|
|
23
|
+
encounterUuid: 'note-1',
|
|
26
24
|
encounterNote: 'Patient shows improvement with current medication.',
|
|
27
25
|
encounterNoteRecordedAt: '2024-08-01T12:34:56Z',
|
|
28
26
|
encounterProvider: 'Dr. John Doe',
|
|
27
|
+
obsUuid: 'obsUuid1',
|
|
28
|
+
conceptUuid: 'concept1',
|
|
29
|
+
encounterTypeUuid: 'inpatientNoteEncounterTypeUuid',
|
|
29
30
|
},
|
|
30
31
|
{
|
|
31
|
-
|
|
32
|
-
diagnoses: '',
|
|
32
|
+
encounterUuid: 'note-2',
|
|
33
33
|
encounterNote: 'Blood pressure is slightly elevated. Consider adjusting medication.',
|
|
34
34
|
encounterNoteRecordedAt: '2024-08-02T14:22:00Z',
|
|
35
35
|
encounterProvider: 'Dr. Jane Smith',
|
|
36
|
+
obsUuid: 'obsUuid2',
|
|
37
|
+
conceptUuid: 'concept1',
|
|
38
|
+
encounterTypeUuid: 'inpatientNoteEncounterTypeUuid',
|
|
36
39
|
},
|
|
37
40
|
];
|
|
38
41
|
|
|
@@ -50,12 +53,14 @@ describe('PatientNotesHistory', () => {
|
|
|
50
53
|
errorFetchingEmrConfiguration: null,
|
|
51
54
|
});
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
mockedUsePatientNotes.mockReturnValue({
|
|
54
57
|
patientNotes: [],
|
|
55
58
|
isLoadingPatientNotes: true,
|
|
59
|
+
errorFetchingPatientNotes: undefined,
|
|
60
|
+
mutatePatientNotes: jest.fn(),
|
|
56
61
|
});
|
|
57
62
|
|
|
58
|
-
render(<PatientNotesHistory patientUuid={mockPatientUuid} />);
|
|
63
|
+
render(<PatientNotesHistory patientUuid={mockPatientUuid} promptBeforeClosing={jest.fn()} visitUuid={''} />);
|
|
59
64
|
|
|
60
65
|
expect(screen.getAllByTestId('in-patient-note-skeleton')).toHaveLength(4);
|
|
61
66
|
});
|
|
@@ -68,12 +73,14 @@ describe('PatientNotesHistory', () => {
|
|
|
68
73
|
errorFetchingEmrConfiguration: null,
|
|
69
74
|
});
|
|
70
75
|
|
|
71
|
-
|
|
76
|
+
mockedUsePatientNotes.mockReturnValue({
|
|
72
77
|
patientNotes: mockPatientNotes,
|
|
73
78
|
isLoadingPatientNotes: false,
|
|
79
|
+
errorFetchingPatientNotes: undefined,
|
|
80
|
+
mutatePatientNotes: jest.fn(),
|
|
74
81
|
});
|
|
75
82
|
|
|
76
|
-
render(<PatientNotesHistory patientUuid={mockPatientUuid} />);
|
|
83
|
+
render(<PatientNotesHistory patientUuid={mockPatientUuid} promptBeforeClosing={jest.fn()} visitUuid={''} />);
|
|
77
84
|
|
|
78
85
|
expect(screen.getByText('History')).toBeInTheDocument();
|
|
79
86
|
|
|
@@ -24,13 +24,13 @@
|
|
|
24
24
|
.noteTile {
|
|
25
25
|
padding: layout.$spacing-04;
|
|
26
26
|
margin-bottom: layout.$spacing-04;
|
|
27
|
+
background-color: $ui-01;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
.noteHeader {
|
|
30
31
|
display: flex;
|
|
31
32
|
justify-content: space-between;
|
|
32
33
|
align-items: center;
|
|
33
|
-
margin-bottom: layout.$spacing-04;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
.noteProviderRole {
|
|
@@ -41,8 +41,9 @@
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
.noteBody {
|
|
44
|
-
font-size:
|
|
44
|
+
font-size: 1rem;
|
|
45
45
|
color: $text-02;
|
|
46
|
+
white-space: pre-wrap;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
.noteDateAndTime {
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
.noteProviderName {
|
|
54
|
-
font-size:
|
|
55
|
+
font-size: 0.75rem;
|
|
55
56
|
margin-top: layout.$spacing-04;
|
|
56
57
|
color: $text-02;
|
|
57
58
|
}
|
|
@@ -59,3 +60,15 @@
|
|
|
59
60
|
.noteSkeletonContainer {
|
|
60
61
|
height: layout.$spacing-13;
|
|
61
62
|
}
|
|
63
|
+
|
|
64
|
+
.editFields {
|
|
65
|
+
display: flex;
|
|
66
|
+
flex-direction: column;
|
|
67
|
+
gap: layout.$spacing-03;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.editButtons {
|
|
71
|
+
display: flex;
|
|
72
|
+
gap: layout.$spacing-02;
|
|
73
|
+
justify-content: flex-end;
|
|
74
|
+
}
|
|
@@ -3,7 +3,7 @@ import { useMemo } from 'react';
|
|
|
3
3
|
import { type EncounterPayload } from '../../types';
|
|
4
4
|
import { type PatientNote, type RESTPatientNote, type UsePatientNotes } from './types';
|
|
5
5
|
|
|
6
|
-
export function
|
|
6
|
+
export function createPatientNote(payload: EncounterPayload, abortController: AbortController = new AbortController()) {
|
|
7
7
|
return openmrsFetch(`${restBaseUrl}/encounter`, {
|
|
8
8
|
headers: {
|
|
9
9
|
'Content-Type': 'application/json',
|
|
@@ -14,9 +14,19 @@ export function savePatientNote(payload: EncounterPayload, abortController: Abor
|
|
|
14
14
|
});
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
export function editPatientNote(obsUuid: string, note: string) {
|
|
18
|
+
return openmrsFetch(`${restBaseUrl}/obs/${obsUuid}`, {
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/json',
|
|
21
|
+
},
|
|
22
|
+
method: 'POST',
|
|
23
|
+
body: { value: note },
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
17
27
|
export function usePatientNotes(patientUuid: string, visitUuid: string, conceptUuids: Array<string>): UsePatientNotes {
|
|
18
28
|
const customRepresentation =
|
|
19
|
-
'custom:(uuid,patient:(uuid),obs:(uuid,concept:(uuid),obsDatetime,value:(uuid)),' +
|
|
29
|
+
'custom:(uuid,patient:(uuid),obs:(uuid,concept:(uuid),obsDatetime,value:(uuid)),encounterType,' +
|
|
20
30
|
'encounterProviders:(uuid,provider:(uuid,person:(uuid,display)))';
|
|
21
31
|
const encountersApiUrl = `${restBaseUrl}/encounter?patient=${patientUuid}&visit=${visitUuid}&v=${customRepresentation}`;
|
|
22
32
|
|
|
@@ -32,14 +42,17 @@ export function usePatientNotes(patientUuid: string, visitUuid: string, conceptU
|
|
|
32
42
|
return encounter.obs?.reduce((acc, obs) => {
|
|
33
43
|
if (conceptUuids.includes(obs.concept.uuid)) {
|
|
34
44
|
acc.push({
|
|
35
|
-
|
|
45
|
+
encounterUuid: encounter.uuid,
|
|
46
|
+
obsUuid: obs.uuid,
|
|
36
47
|
encounterNote: obs ? obs.value : '',
|
|
37
48
|
encounterNoteRecordedAt: obs ? obs.obsDatetime : '',
|
|
38
49
|
encounterProvider: encounter.encounterProviders.map((ep) => ep.provider.person.display).join(', '),
|
|
50
|
+
conceptUuid: obs.concept.uuid,
|
|
51
|
+
encounterTypeUuid: encounter.encounterType.uuid,
|
|
39
52
|
});
|
|
40
53
|
}
|
|
41
54
|
return acc;
|
|
42
|
-
}, []);
|
|
55
|
+
}, [] as Array<PatientNote>);
|
|
43
56
|
})
|
|
44
57
|
.sort(
|
|
45
58
|
(a, b) => new Date(b.encounterNoteRecordedAt).getTime() - new Date(a.encounterNoteRecordedAt).getTime(),
|
|
@@ -2,7 +2,7 @@ import React from 'react';
|
|
|
2
2
|
import userEvent from '@testing-library/user-event';
|
|
3
3
|
import { render, screen } from '@testing-library/react';
|
|
4
4
|
import { getDefaultsFromConfigSchema, showSnackbar, useConfig } from '@openmrs/esm-framework';
|
|
5
|
-
import {
|
|
5
|
+
import { createPatientNote, usePatientNotes } from './notes.resource';
|
|
6
6
|
import WardPatientNotesWorkspace from './notes.workspace';
|
|
7
7
|
import { emrConfigurationMock, mockInpatientRequestAlice, mockPatientAlice } from '__mocks__';
|
|
8
8
|
import useEmrConfiguration from '../../hooks/useEmrConfiguration';
|
|
@@ -30,11 +30,11 @@ const testProps: WardPatientWorkspaceDefinition = {
|
|
|
30
30
|
isRootWorkspace: false,
|
|
31
31
|
};
|
|
32
32
|
|
|
33
|
-
const
|
|
33
|
+
const mockCreatePatientNote = createPatientNote as jest.Mock;
|
|
34
34
|
const mockedShowSnackbar = jest.mocked(showSnackbar);
|
|
35
35
|
|
|
36
36
|
jest.mock('./notes.resource', () => ({
|
|
37
|
-
|
|
37
|
+
createPatientNote: jest.fn(),
|
|
38
38
|
usePatientNotes: jest.fn(),
|
|
39
39
|
}));
|
|
40
40
|
|
|
@@ -86,7 +86,7 @@ describe('<WardPatientNotesWorkspace>', () => {
|
|
|
86
86
|
patient: mockPatientAlice.uuid,
|
|
87
87
|
};
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
mockCreatePatientNote.mockResolvedValue({ status: 201, body: 'Condition created' });
|
|
90
90
|
|
|
91
91
|
renderWardPatientNotesForm();
|
|
92
92
|
|
|
@@ -98,8 +98,8 @@ describe('<WardPatientNotesWorkspace>', () => {
|
|
|
98
98
|
const submitButton = screen.getByRole('button', { name: /Save/i });
|
|
99
99
|
await userEvent.click(submitButton);
|
|
100
100
|
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
101
|
+
expect(mockCreatePatientNote).toHaveBeenCalledTimes(1);
|
|
102
|
+
expect(mockCreatePatientNote).toHaveBeenCalledWith(expect.objectContaining(successPayload), new AbortController());
|
|
103
103
|
});
|
|
104
104
|
|
|
105
105
|
test('renders an error snackbar if there was a problem recording a visit note', async () => {
|
|
@@ -111,7 +111,7 @@ describe('<WardPatientNotesWorkspace>', () => {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
|
|
114
|
+
mockCreatePatientNote.mockRejectedValueOnce(error);
|
|
115
115
|
renderWardPatientNotesForm();
|
|
116
116
|
|
|
117
117
|
const note = screen.getByRole('textbox', { name: /Write your notes/i });
|
|
@@ -6,6 +6,7 @@ import { Controller, useForm } from 'react-hook-form';
|
|
|
6
6
|
import { Button, Column, Form, InlineLoading, InlineNotification, Row, Stack, TextArea } from '@carbon/react';
|
|
7
7
|
import {
|
|
8
8
|
closeWorkspaceGroup2,
|
|
9
|
+
getCoreTranslation,
|
|
9
10
|
ResponsiveWrapper,
|
|
10
11
|
showSnackbar,
|
|
11
12
|
translateFrom,
|
|
@@ -13,7 +14,7 @@ import {
|
|
|
13
14
|
Workspace2,
|
|
14
15
|
} from '@openmrs/esm-framework';
|
|
15
16
|
import { moduleName } from '../../constant';
|
|
16
|
-
import {
|
|
17
|
+
import { createPatientNote } from './notes.resource';
|
|
17
18
|
import useEmrConfiguration from '../../hooks/useEmrConfiguration';
|
|
18
19
|
import styles from './notes.scss';
|
|
19
20
|
import { type WardPatientWorkspaceDefinition, type EncounterPayload } from '../../types';
|
|
@@ -39,7 +40,7 @@ const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceDefinition> = ({
|
|
|
39
40
|
const session = useSession();
|
|
40
41
|
|
|
41
42
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
|
42
|
-
const [
|
|
43
|
+
const [hasEditChanges, setHasEditChanges] = useState(false);
|
|
43
44
|
|
|
44
45
|
const {
|
|
45
46
|
control,
|
|
@@ -83,7 +84,7 @@ const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceDefinition> = ({
|
|
|
83
84
|
|
|
84
85
|
const abortController = new AbortController();
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
createPatientNote(notePayload, abortController)
|
|
87
88
|
.then(async () => {
|
|
88
89
|
showSnackbar({
|
|
89
90
|
isLowContrast: true,
|
|
@@ -119,7 +120,9 @@ const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceDefinition> = ({
|
|
|
119
120
|
const onError = (errors) => console.error(errors);
|
|
120
121
|
|
|
121
122
|
return (
|
|
122
|
-
<Workspace2
|
|
123
|
+
<Workspace2
|
|
124
|
+
hasUnsavedChanges={isDirty || hasEditChanges}
|
|
125
|
+
title={t('inpatientNotesWorkspaceTitle', 'In-patient notes')}>
|
|
123
126
|
<WardPatientWorkspaceBanner {...{ wardPatient }} />
|
|
124
127
|
<Form className={styles.form} onSubmit={handleSubmit(onSubmit, onError)}>
|
|
125
128
|
{errorFetchingEmrConfiguration && (
|
|
@@ -136,7 +139,7 @@ const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceDefinition> = ({
|
|
|
136
139
|
/>
|
|
137
140
|
</div>
|
|
138
141
|
)}
|
|
139
|
-
<Stack className={styles.formContainer} gap={
|
|
142
|
+
<Stack className={styles.formContainer} gap={4}>
|
|
140
143
|
<Row className={styles.row}>
|
|
141
144
|
<Column sm={1}>
|
|
142
145
|
<span className={styles.columnLabel}>{t('note', 'Note')}</span>
|
|
@@ -155,12 +158,9 @@ const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceDefinition> = ({
|
|
|
155
158
|
onBlur={onBlur}
|
|
156
159
|
onChange={(event) => {
|
|
157
160
|
onChange(event);
|
|
158
|
-
const textAreaLineHeight = 24; // This is the default line height for Carbon's TextArea component
|
|
159
|
-
const newRows = Math.ceil(event.target.scrollHeight / textAreaLineHeight);
|
|
160
|
-
setRows(newRows);
|
|
161
161
|
}}
|
|
162
162
|
placeholder={t('wardClinicalNotePlaceholder', 'Write any notes here')}
|
|
163
|
-
rows={
|
|
163
|
+
rows={6}
|
|
164
164
|
value={value}
|
|
165
165
|
/>
|
|
166
166
|
</ResponsiveWrapper>
|
|
@@ -168,17 +168,26 @@ const WardPatientNotesWorkspace: React.FC<WardPatientWorkspaceDefinition> = ({
|
|
|
168
168
|
/>
|
|
169
169
|
</Column>
|
|
170
170
|
</Row>
|
|
171
|
+
|
|
172
|
+
<Button
|
|
173
|
+
className={styles.saveButton}
|
|
174
|
+
disabled={isSubmitting || isLoadingEmrConfiguration || errorFetchingEmrConfiguration}
|
|
175
|
+
kind="primary"
|
|
176
|
+
type="submit">
|
|
177
|
+
{isSubmitting ? (
|
|
178
|
+
<InlineLoading description={t('saving', 'Saving...')} />
|
|
179
|
+
) : (
|
|
180
|
+
<span>{getCoreTranslation('save')}</span>
|
|
181
|
+
)}
|
|
182
|
+
</Button>
|
|
171
183
|
</Stack>
|
|
172
|
-
<Button
|
|
173
|
-
className={styles.saveButton}
|
|
174
|
-
disabled={isSubmitting || isLoadingEmrConfiguration || errorFetchingEmrConfiguration}
|
|
175
|
-
kind="primary"
|
|
176
|
-
type="submit">
|
|
177
|
-
{isSubmitting ? <InlineLoading description={t('saving', 'Saving...')} /> : <span>{t('save', 'Save')}</span>}
|
|
178
|
-
</Button>
|
|
179
184
|
</Form>
|
|
180
185
|
|
|
181
|
-
<PatientNotesHistory
|
|
186
|
+
<PatientNotesHistory
|
|
187
|
+
patientUuid={patientUuid}
|
|
188
|
+
visitUuid={wardPatient?.visit?.uuid}
|
|
189
|
+
promptBeforeClosing={setHasEditChanges}
|
|
190
|
+
/>
|
|
182
191
|
</Workspace2>
|
|
183
192
|
);
|
|
184
193
|
};
|
|
@@ -12,10 +12,13 @@ export interface RESTPatientNote extends OpenmrsResource {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export interface PatientNote {
|
|
15
|
-
|
|
15
|
+
encounterUuid: string;
|
|
16
|
+
obsUuid: string;
|
|
16
17
|
encounterNote: string;
|
|
17
18
|
encounterNoteRecordedAt: string;
|
|
18
19
|
encounterProvider: string;
|
|
20
|
+
conceptUuid: string;
|
|
21
|
+
encounterTypeUuid: string;
|
|
19
22
|
}
|
|
20
23
|
|
|
21
24
|
export interface UsePatientNotes {
|
|
@@ -27,6 +30,7 @@ export interface UsePatientNotes {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
export interface ObsData {
|
|
33
|
+
uuid: string;
|
|
30
34
|
concept: Concept;
|
|
31
35
|
value?: string | any;
|
|
32
36
|
groupMembers?: Array<{
|
package/src/ward.resource.ts
CHANGED
|
@@ -40,6 +40,12 @@ export function useCreateEncounter() {
|
|
|
40
40
|
return { createEncounter, emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration };
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
export function deleteEncounter(encounterUuid: string) {
|
|
44
|
+
return openmrsFetch(`${restBaseUrl}/encounter/${encounterUuid}`, {
|
|
45
|
+
method: 'DELETE',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
43
49
|
export function useAdmitPatient() {
|
|
44
50
|
const { createEncounter, emrConfiguration, isLoadingEmrConfiguration, errorFetchingEmrConfiguration } =
|
|
45
51
|
useCreateEncounter();
|
package/translations/en.json
CHANGED
|
@@ -25,7 +25,10 @@
|
|
|
25
25
|
"countItems_other": "{{count}} {{item}}",
|
|
26
26
|
"days_one": "{{count}} day",
|
|
27
27
|
"days_other": "{{count}} days",
|
|
28
|
+
"deleteNote": "Delete Note",
|
|
29
|
+
"deleteNoteConfirmationText": "Are you sure you want to delete this note? This action can't be undone.",
|
|
28
30
|
"discharge": "Discharge",
|
|
31
|
+
"editNote": "Edit note",
|
|
29
32
|
"emptyBed": "Empty bed",
|
|
30
33
|
"emptyStateIllustration": "Empty state illustration",
|
|
31
34
|
"emptyText": "Empty",
|
|
@@ -36,6 +39,7 @@
|
|
|
36
39
|
"errorConfiguringPatientCardMessage": "Unable to find configuration for {{elementType}}, id: {{id}}",
|
|
37
40
|
"errorCreatingEncounter": "Failed to admit patient",
|
|
38
41
|
"errorCreatingTransferRequest": "Error creating transfer request",
|
|
42
|
+
"errorDeletingNote": "Error deleting note",
|
|
39
43
|
"errorDischargingPatient": "Error discharging patient",
|
|
40
44
|
"errorLoadingBedDetails": "Error loading bed details",
|
|
41
45
|
"errorLoadingChildren": "Error loading children info",
|
|
@@ -44,6 +48,7 @@
|
|
|
44
48
|
"errorLoadingPatientInfo": "Error loading patient info",
|
|
45
49
|
"errorLoadingPatients": "Error loading admitted patients",
|
|
46
50
|
"errorLoadingWardLocation": "Error loading ward location",
|
|
51
|
+
"errorSavingPatientNote": "Error saving patient note",
|
|
47
52
|
"female": "Female",
|
|
48
53
|
"fetchingEmrConfigurationFailed": "Fetching EMR configuration failed. Try refreshing the page or contact your system administrator.",
|
|
49
54
|
"fetchingPatientNotesFailed": "Fetching patient notes failed. Try refreshing the page or contact your system administrator.",
|
|
@@ -70,6 +75,7 @@
|
|
|
70
75
|
"noLocationsFound": "No locations found",
|
|
71
76
|
"noPendingAdmissionRequests": "No pending admission requests",
|
|
72
77
|
"note": "Note",
|
|
78
|
+
"noteDeletedSuccessfully": "Note deleted successfully",
|
|
73
79
|
"notes": "Notes",
|
|
74
80
|
"notesRequiredForCancellingRequest": "Notes required for cancelling admission or transfer request",
|
|
75
81
|
"orderBasket": "Order basket",
|