@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.
Files changed (62) hide show
  1. package/.turbo/turbo-build.log +2 -2
  2. package/dist/1011.js +1 -1
  3. package/dist/1011.js.map +1 -1
  4. package/dist/1369.js +1 -1
  5. package/dist/1369.js.map +1 -1
  6. package/dist/1554.js +1 -0
  7. package/dist/1554.js.map +1 -0
  8. package/dist/1895.js +1 -1
  9. package/dist/1895.js.map +1 -1
  10. package/dist/2117.js +1 -1
  11. package/dist/2117.js.map +1 -1
  12. package/dist/2640.js +1 -1
  13. package/dist/2640.js.map +1 -1
  14. package/dist/3624.js +1 -1
  15. package/dist/3948.js +11 -0
  16. package/dist/3948.js.map +1 -0
  17. package/dist/4285.js +1 -1
  18. package/dist/4285.js.map +1 -1
  19. package/dist/4373.js +1 -1
  20. package/dist/6026.js +1 -1
  21. package/dist/6026.js.map +1 -1
  22. package/dist/6199.js +1 -1
  23. package/dist/6285.js +1 -0
  24. package/dist/6285.js.map +1 -0
  25. package/dist/6515.js +1 -1
  26. package/dist/7073.js +1 -0
  27. package/dist/7073.js.map +1 -0
  28. package/dist/7607.js +1 -1
  29. package/dist/7715.js +1 -1
  30. package/dist/7715.js.map +1 -1
  31. package/dist/{3954.js → 8622.js} +17 -17
  32. package/dist/8622.js.map +1 -0
  33. package/dist/9727.js +1 -1
  34. package/dist/9727.js.map +1 -1
  35. package/dist/main.js +3 -3
  36. package/dist/main.js.map +1 -1
  37. package/dist/openmrs-esm-ward-app.js +3 -3
  38. package/dist/openmrs-esm-ward-app.js.buildmanifest.json +126 -102
  39. package/dist/routes.json +1 -1
  40. package/package.json +2 -2
  41. package/src/index.ts +5 -0
  42. package/src/routes.json +6 -0
  43. package/src/types/index.ts +1 -0
  44. package/src/ward-workspace/ward-patient-notes/history/delete-note.modal.tsx +63 -0
  45. package/src/ward-workspace/ward-patient-notes/history/note.component.tsx +138 -13
  46. package/src/ward-workspace/ward-patient-notes/history/notes-container.component.tsx +14 -8
  47. package/src/ward-workspace/ward-patient-notes/history/notes-container.test.tsx +20 -13
  48. package/src/ward-workspace/ward-patient-notes/history/styles.scss +16 -3
  49. package/src/ward-workspace/ward-patient-notes/notes.resource.ts +17 -4
  50. package/src/ward-workspace/ward-patient-notes/notes.scss +5 -1
  51. package/src/ward-workspace/ward-patient-notes/notes.test.tsx +7 -7
  52. package/src/ward-workspace/ward-patient-notes/notes.workspace.tsx +26 -17
  53. package/src/ward-workspace/ward-patient-notes/types.ts +5 -1
  54. package/src/ward.resource.ts +6 -0
  55. package/translations/en.json +6 -0
  56. package/dist/3954.js.map +0 -1
  57. package/dist/5420.js +0 -6
  58. package/dist/5420.js.map +0 -1
  59. package/dist/6203.js +0 -1
  60. package/dist/6203.js.map +0 -1
  61. package/dist/7965.js +0 -1
  62. 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 { SkeletonText, Tile } from '@carbon/react';
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
- const InPatientNote: React.FC<InPatientNoteProps> = ({ note }) => {
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
- <Tile className={styles.noteTile}>
35
- <div className={styles.noteHeader}>
36
- <span className={styles.noteProviderRole}>{t('note', 'Note')}</span>
37
- <span className={styles.noteDateAndTime}>
38
- {formattedDate}, {formattedTime}
39
- </span>
40
- </div>
41
- <div className={styles.noteBody}>{note.encounterNote}</div>
42
- <div className={styles.noteProviderName}>{note.encounterProvider}</div>
43
- </Tile>
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, useConfig } from '@openmrs/esm-framework';
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(patientUuid, visitUuid, [
22
- emrConfiguration?.consultFreeTextCommentsConcept.uuid,
23
- ...config.additionalInpatientNotesConceptUuids,
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 key={patientNote.id} note={patientNote} />
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
- id: 'note-1',
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
- id: 'note-2',
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
- usePatientNotes.mockReturnValue({
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
- usePatientNotes.mockReturnValue({
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: 0.875rem;
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: layout.$spacing-04;
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 savePatientNote(payload: EncounterPayload, abortController: AbortController = new AbortController()) {
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
- id: encounter.uuid,
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(),
@@ -21,8 +21,12 @@
21
21
  color: colors.$gray-70;
22
22
  }
23
23
 
24
+ .saveButtonContainer {
25
+ display: flex;
26
+ justify-content: flex-end;
27
+ }
28
+
24
29
  .saveButton {
25
- margin-left: layout.$spacing-04;
26
30
  display: flex;
27
31
  align-content: flex-start;
28
32
  align-items: baseline;
@@ -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 { savePatientNote, usePatientNotes } from './notes.resource';
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 mockSavePatientNote = savePatientNote as jest.Mock;
33
+ const mockCreatePatientNote = createPatientNote as jest.Mock;
34
34
  const mockedShowSnackbar = jest.mocked(showSnackbar);
35
35
 
36
36
  jest.mock('./notes.resource', () => ({
37
- savePatientNote: jest.fn(),
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
- mockSavePatientNote.mockResolvedValue({ status: 201, body: 'Condition created' });
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(mockSavePatientNote).toHaveBeenCalledTimes(1);
102
- expect(mockSavePatientNote).toHaveBeenCalledWith(expect.objectContaining(successPayload), new AbortController());
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
- mockSavePatientNote.mockRejectedValueOnce(error);
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 { savePatientNote } from './notes.resource';
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 [rows, setRows] = useState(0);
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
- savePatientNote(notePayload, abortController)
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 hasUnsavedChanges={isDirty} title={t('inpatientNotesWorkspaceTitle', 'In-patient notes')}>
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={2}>
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={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 patientUuid={patientUuid} visitUuid={wardPatient?.visit?.uuid} />
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
- id: string;
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<{
@@ -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();
@@ -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",