@tebokaroa/openmrs-esm-patient-notes-app 12.1.0-custom.1
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/README.md +4 -0
- package/jest.config.js +3 -0
- package/package.json +56 -0
- package/rspack.config.js +1 -0
- package/src/config-schema.ts +22 -0
- package/src/dashboard.meta.ts +7 -0
- package/src/declarations.d.ts +4 -0
- package/src/index.ts +34 -0
- package/src/notes/notes-overview.extension.tsx +74 -0
- package/src/notes/notes-overview.scss +40 -0
- package/src/notes/notes-overview.test.tsx +100 -0
- package/src/notes/paginated-notes.component.tsx +182 -0
- package/src/notes/visit-note-config-schema.ts +38 -0
- package/src/notes/visit-notes-form.scss +228 -0
- package/src/notes/visit-notes-form.test.tsx +494 -0
- package/src/notes/visit-notes-form.workspace.tsx +943 -0
- package/src/notes/visit-notes.resource.ts +113 -0
- package/src/routes.json +32 -0
- package/src/types/index.ts +202 -0
- package/src/visit-note-action-button.extension.tsx +28 -0
- package/src/visit-note-action-button.test.tsx +41 -0
- package/translations/am.json +39 -0
- package/translations/ar.json +39 -0
- package/translations/ar_SY.json +39 -0
- package/translations/bn.json +39 -0
- package/translations/cs.json +39 -0
- package/translations/de.json +39 -0
- package/translations/en.json +41 -0
- package/translations/en_US.json +39 -0
- package/translations/es.json +39 -0
- package/translations/es_MX.json +39 -0
- package/translations/fr.json +39 -0
- package/translations/he.json +39 -0
- package/translations/hi.json +39 -0
- package/translations/hi_IN.json +39 -0
- package/translations/id.json +39 -0
- package/translations/it.json +39 -0
- package/translations/ka.json +39 -0
- package/translations/km.json +39 -0
- package/translations/ku.json +39 -0
- package/translations/ky.json +39 -0
- package/translations/lg.json +39 -0
- package/translations/ne.json +39 -0
- package/translations/pl.json +39 -0
- package/translations/pt.json +39 -0
- package/translations/pt_BR.json +39 -0
- package/translations/qu.json +39 -0
- package/translations/ro_RO.json +39 -0
- package/translations/ru_RU.json +39 -0
- package/translations/si.json +39 -0
- package/translations/sq.json +39 -0
- package/translations/sw.json +39 -0
- package/translations/sw_KE.json +39 -0
- package/translations/tr.json +39 -0
- package/translations/tr_TR.json +39 -0
- package/translations/uk.json +39 -0
- package/translations/uz.json +39 -0
- package/translations/uz@Latn.json +39 -0
- package/translations/uz_UZ.json +39 -0
- package/translations/vi.json +39 -0
- package/translations/zh.json +39 -0
- package/translations/zh_CN.json +39 -0
- package/translations/zh_TW.json +39 -0
- package/tsconfig.json +4 -0
|
@@ -0,0 +1,943 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
|
+
import classnames from 'classnames';
|
|
3
|
+
import dayjs from 'dayjs';
|
|
4
|
+
import { debounce } from 'lodash-es';
|
|
5
|
+
import { useTranslation } from 'react-i18next';
|
|
6
|
+
import { useSWRConfig } from 'swr';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
9
|
+
import { Controller, useForm, type Control } from 'react-hook-form';
|
|
10
|
+
import type { TFunction } from 'i18next';
|
|
11
|
+
import {
|
|
12
|
+
Button,
|
|
13
|
+
ButtonSet,
|
|
14
|
+
Column,
|
|
15
|
+
Form,
|
|
16
|
+
FormGroup,
|
|
17
|
+
InlineLoading,
|
|
18
|
+
InlineNotification,
|
|
19
|
+
Row,
|
|
20
|
+
Search,
|
|
21
|
+
SkeletonText,
|
|
22
|
+
Stack,
|
|
23
|
+
Tag,
|
|
24
|
+
TextArea,
|
|
25
|
+
Tile,
|
|
26
|
+
} from '@carbon/react';
|
|
27
|
+
import { Add, CloseFilled, WarningFilled } from '@carbon/react/icons';
|
|
28
|
+
import {
|
|
29
|
+
createAttachment,
|
|
30
|
+
createErrorHandler,
|
|
31
|
+
ExtensionSlot,
|
|
32
|
+
OpenmrsDatePicker,
|
|
33
|
+
ResponsiveWrapper,
|
|
34
|
+
restBaseUrl,
|
|
35
|
+
showModal,
|
|
36
|
+
showSnackbar,
|
|
37
|
+
useConfig,
|
|
38
|
+
useLayoutType,
|
|
39
|
+
useSession,
|
|
40
|
+
Workspace2,
|
|
41
|
+
type Encounter,
|
|
42
|
+
type UploadedFile,
|
|
43
|
+
} from '@openmrs/esm-framework';
|
|
44
|
+
import {
|
|
45
|
+
invalidateVisitAndEncounterData,
|
|
46
|
+
type PatientWorkspace2DefinitionProps,
|
|
47
|
+
useAllowedFileExtensions,
|
|
48
|
+
} from '@openmrs/esm-patient-common-lib';
|
|
49
|
+
import type { ConfigObject } from '../config-schema';
|
|
50
|
+
import type { Concept, Diagnosis, DiagnosisPayload, FreeConcept, VisitNotePayload } from '../types';
|
|
51
|
+
import {
|
|
52
|
+
deletePatientDiagnosis,
|
|
53
|
+
fetchDiagnosisConceptsByName,
|
|
54
|
+
savePatientDiagnosis,
|
|
55
|
+
saveVisitNote,
|
|
56
|
+
updateVisitNote,
|
|
57
|
+
useVisitNotes,
|
|
58
|
+
} from './visit-notes.resource';
|
|
59
|
+
import styles from './visit-notes-form.scss';
|
|
60
|
+
|
|
61
|
+
type VisitNotesFormData = Omit<z.infer<ReturnType<typeof createSchema>>, 'images'> & {
|
|
62
|
+
images?: UploadedFile[];
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
interface DiagnosesDisplayProps {
|
|
66
|
+
fieldName: string;
|
|
67
|
+
isDiagnosisNotSelected: (diagnosis: FreeConcept) => boolean;
|
|
68
|
+
isLoading: boolean;
|
|
69
|
+
isSearching: boolean;
|
|
70
|
+
onAddDiagnosis: (diagnosis: FreeConcept, searchInputField: string) => void;
|
|
71
|
+
searchResults: Array<Concept>;
|
|
72
|
+
t: TFunction;
|
|
73
|
+
value: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
interface DiagnosisSearchProps {
|
|
77
|
+
control: Control<VisitNotesFormData>;
|
|
78
|
+
error?: object;
|
|
79
|
+
handleSearch: (fieldName) => void;
|
|
80
|
+
labelText: string;
|
|
81
|
+
name: 'noteDate' | 'primaryDiagnosisSearch' | 'secondaryDiagnosisSearch' | 'clinicalNote';
|
|
82
|
+
placeholder: string;
|
|
83
|
+
setIsSearching: (isSearching: boolean) => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const createSchema = (t: TFunction) => {
|
|
87
|
+
return z.object({
|
|
88
|
+
noteDate: z.date(),
|
|
89
|
+
primaryDiagnosisSearch: z.string(),
|
|
90
|
+
secondaryDiagnosisSearch: z.string().optional(),
|
|
91
|
+
clinicalNote: z.string().optional(),
|
|
92
|
+
images: z.array(z.any()).optional(),
|
|
93
|
+
});
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
export interface VisitNotesFormProps {
|
|
97
|
+
encounter?: Encounter;
|
|
98
|
+
formContext: 'creating' | 'editing';
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const VisitNotesForm: React.FC<PatientWorkspace2DefinitionProps<VisitNotesFormProps, {}>> = ({
|
|
102
|
+
closeWorkspace,
|
|
103
|
+
workspaceProps: { formContext, encounter },
|
|
104
|
+
groupProps: { patientUuid, patient },
|
|
105
|
+
}) => {
|
|
106
|
+
const isEditing: boolean = Boolean(formContext === 'editing' && encounter?.id);
|
|
107
|
+
const searchTimeoutInMs = 500;
|
|
108
|
+
const { t } = useTranslation();
|
|
109
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
110
|
+
const session = useSession();
|
|
111
|
+
const { isPrimaryDiagnosisRequired, ...config } = useConfig<ConfigObject>();
|
|
112
|
+
const memoizedState = useMemo(() => ({ patientUuid, patient }), [patientUuid, patient]);
|
|
113
|
+
const { clinicianEncounterRole, encounterNoteTextConceptUuid, encounterTypeUuid, formConceptUuid } =
|
|
114
|
+
config.visitNoteConfig;
|
|
115
|
+
const [isLoadingPrimaryDiagnoses, setIsLoadingPrimaryDiagnoses] = useState(false);
|
|
116
|
+
const [isLoadingSecondaryDiagnoses, setIsLoadingSecondaryDiagnoses] = useState(false);
|
|
117
|
+
const [isSearching, setIsSearching] = useState(false);
|
|
118
|
+
const [selectedPrimaryDiagnoses, setSelectedPrimaryDiagnoses] = useState<Array<Diagnosis>>([]);
|
|
119
|
+
const [selectedSecondaryDiagnoses, setSelectedSecondaryDiagnoses] = useState<Array<Diagnosis>>([]);
|
|
120
|
+
const [searchPrimaryResults, setSearchPrimaryResults] = useState<Array<Concept>>(null);
|
|
121
|
+
const [searchSecondaryResults, setSearchSecondaryResults] = useState<Array<Concept>>(null);
|
|
122
|
+
const [combinedDiagnoses, setCombinedDiagnoses] = useState<Array<Diagnosis>>([]);
|
|
123
|
+
const [rows, setRows] = useState<number>();
|
|
124
|
+
const [error, setError] = useState<Error>(null);
|
|
125
|
+
const { allowedFileExtensions } = useAllowedFileExtensions();
|
|
126
|
+
|
|
127
|
+
const visitNoteFormSchema = useMemo(() => createSchema(t), [t]);
|
|
128
|
+
|
|
129
|
+
const customResolver = useCallback(
|
|
130
|
+
async (data, context, options) => {
|
|
131
|
+
const zodResult = await zodResolver(visitNoteFormSchema)(data, context, options);
|
|
132
|
+
|
|
133
|
+
if (isPrimaryDiagnosisRequired && selectedPrimaryDiagnoses.length === 0) {
|
|
134
|
+
return {
|
|
135
|
+
...zodResult,
|
|
136
|
+
errors: {
|
|
137
|
+
...zodResult.errors,
|
|
138
|
+
primaryDiagnosisSearch: {
|
|
139
|
+
type: 'custom',
|
|
140
|
+
message: t('primaryDiagnosisRequired', 'Choose at least one primary diagnosis'),
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return zodResult;
|
|
147
|
+
},
|
|
148
|
+
[visitNoteFormSchema, isPrimaryDiagnosisRequired, selectedPrimaryDiagnoses, t],
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const {
|
|
152
|
+
clearErrors,
|
|
153
|
+
control,
|
|
154
|
+
formState: { errors, dirtyFields, isSubmitting },
|
|
155
|
+
handleSubmit,
|
|
156
|
+
setValue,
|
|
157
|
+
watch,
|
|
158
|
+
} = useForm<VisitNotesFormData>({
|
|
159
|
+
mode: 'onSubmit',
|
|
160
|
+
resolver: customResolver,
|
|
161
|
+
defaultValues: {
|
|
162
|
+
primaryDiagnosisSearch: '',
|
|
163
|
+
noteDate: isEditing ? new Date(encounter.rawDatetime) : new Date(),
|
|
164
|
+
clinicalNote: isEditing
|
|
165
|
+
? String(encounter?.obs?.find((obs) => obs.concept.uuid === encounterNoteTextConceptUuid)?.value || '')
|
|
166
|
+
: '',
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
useEffect(() => {
|
|
171
|
+
if (encounter?.diagnoses?.length) {
|
|
172
|
+
try {
|
|
173
|
+
const transformedDiagnoses = encounter.diagnoses.map((d) => ({
|
|
174
|
+
patient: patientUuid,
|
|
175
|
+
diagnosis: {
|
|
176
|
+
coded: d.diagnosis.coded?.uuid,
|
|
177
|
+
nonCoded: d.diagnosis.nonCoded,
|
|
178
|
+
},
|
|
179
|
+
certainty: d.certainty,
|
|
180
|
+
rank: d.rank,
|
|
181
|
+
display: d.display,
|
|
182
|
+
}));
|
|
183
|
+
|
|
184
|
+
const primaryDiagnoses = transformedDiagnoses.filter((d) => d.rank === 1);
|
|
185
|
+
const secondaryDiagnoses = transformedDiagnoses.filter((d) => d.rank === 2);
|
|
186
|
+
|
|
187
|
+
setSelectedPrimaryDiagnoses(primaryDiagnoses);
|
|
188
|
+
setSelectedSecondaryDiagnoses(secondaryDiagnoses);
|
|
189
|
+
setCombinedDiagnoses([...primaryDiagnoses, ...secondaryDiagnoses]);
|
|
190
|
+
} catch (err) {
|
|
191
|
+
setError(new Error(t('errorTransformingDiagnoses', 'Error transforming diagnoses')));
|
|
192
|
+
createErrorHandler();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}, [encounter, patientUuid, t]);
|
|
196
|
+
|
|
197
|
+
const currentImages = watch('images');
|
|
198
|
+
|
|
199
|
+
const { mutateVisitNotes } = useVisitNotes(patientUuid);
|
|
200
|
+
const { mutate: globalMutate } = useSWRConfig();
|
|
201
|
+
|
|
202
|
+
const mutateAttachments = useCallback(
|
|
203
|
+
() => globalMutate((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/attachment`)),
|
|
204
|
+
[globalMutate],
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const locationUuid = session?.sessionLocation?.uuid;
|
|
208
|
+
const providerUuid = session?.currentProvider?.uuid;
|
|
209
|
+
|
|
210
|
+
const debouncedSearch = useMemo(
|
|
211
|
+
() =>
|
|
212
|
+
debounce((fieldQuery, fieldName) => {
|
|
213
|
+
clearErrors('primaryDiagnosisSearch');
|
|
214
|
+
if (fieldQuery) {
|
|
215
|
+
if (fieldName === 'primaryDiagnosisSearch') {
|
|
216
|
+
setIsLoadingPrimaryDiagnoses(true);
|
|
217
|
+
} else if (fieldName === 'secondaryDiagnosisSearch') {
|
|
218
|
+
setIsLoadingSecondaryDiagnoses(true);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
fetchDiagnosisConceptsByName(fieldQuery, config.diagnosisConceptClass)
|
|
222
|
+
.then((matchingConceptDiagnoses: Array<Concept>) => {
|
|
223
|
+
if (fieldName === 'primaryDiagnosisSearch') {
|
|
224
|
+
setSearchPrimaryResults(matchingConceptDiagnoses);
|
|
225
|
+
setIsLoadingPrimaryDiagnoses(false);
|
|
226
|
+
} else if (fieldName === 'secondaryDiagnosisSearch') {
|
|
227
|
+
setSearchSecondaryResults(matchingConceptDiagnoses);
|
|
228
|
+
setIsLoadingSecondaryDiagnoses(false);
|
|
229
|
+
}
|
|
230
|
+
})
|
|
231
|
+
.catch((e) => {
|
|
232
|
+
setError(e);
|
|
233
|
+
createErrorHandler();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}, searchTimeoutInMs),
|
|
237
|
+
[config.diagnosisConceptClass, clearErrors],
|
|
238
|
+
);
|
|
239
|
+
|
|
240
|
+
const handleSearch = useCallback(
|
|
241
|
+
(fieldName) => {
|
|
242
|
+
const fieldQuery = watch(fieldName);
|
|
243
|
+
if (fieldQuery) {
|
|
244
|
+
debouncedSearch(fieldQuery, fieldName);
|
|
245
|
+
}
|
|
246
|
+
setIsSearching(false);
|
|
247
|
+
},
|
|
248
|
+
[debouncedSearch, watch],
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const createDiagnosis = useCallback(
|
|
252
|
+
(concept: FreeConcept) => ({
|
|
253
|
+
certainty: 'PROVISIONAL',
|
|
254
|
+
display: concept.display,
|
|
255
|
+
diagnosis: concept.uuid ? { coded: concept.uuid } : { nonCoded: concept.display }, // Fallback to nonCoded if no UUID
|
|
256
|
+
patient: patientUuid,
|
|
257
|
+
rank: 2,
|
|
258
|
+
}),
|
|
259
|
+
[patientUuid],
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const handleAddDiagnosis = useCallback(
|
|
263
|
+
(conceptDiagnosisToAdd: FreeConcept, searchInputField: string) => {
|
|
264
|
+
const newDiagnosis = createDiagnosis(conceptDiagnosisToAdd);
|
|
265
|
+
if (searchInputField === 'primaryDiagnosisSearch') {
|
|
266
|
+
newDiagnosis.rank = 1;
|
|
267
|
+
setValue('primaryDiagnosisSearch', '');
|
|
268
|
+
setSearchPrimaryResults([]);
|
|
269
|
+
setSelectedPrimaryDiagnoses((selectedDiagnoses) => [...selectedDiagnoses, newDiagnosis]);
|
|
270
|
+
clearErrors('primaryDiagnosisSearch');
|
|
271
|
+
} else if (searchInputField === 'secondaryDiagnosisSearch') {
|
|
272
|
+
setValue('secondaryDiagnosisSearch', '');
|
|
273
|
+
setSearchSecondaryResults([]);
|
|
274
|
+
setSelectedSecondaryDiagnoses((selectedDiagnoses) => [...selectedDiagnoses, newDiagnosis]);
|
|
275
|
+
}
|
|
276
|
+
setCombinedDiagnoses((combinedDiagnoses) => [...combinedDiagnoses, newDiagnosis]);
|
|
277
|
+
},
|
|
278
|
+
[createDiagnosis, setValue, clearErrors],
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const handleRemoveDiagnosis = useCallback(
|
|
282
|
+
(diagnosisToRemove: Diagnosis, searchInputField) => {
|
|
283
|
+
if (searchInputField === 'primaryInputSearch') {
|
|
284
|
+
setSelectedPrimaryDiagnoses(
|
|
285
|
+
selectedPrimaryDiagnoses.filter((diagnosis) =>
|
|
286
|
+
diagnosis.diagnosis.coded
|
|
287
|
+
? diagnosis.diagnosis.coded !== diagnosisToRemove.diagnosis.coded
|
|
288
|
+
: diagnosis.diagnosis.nonCoded !== diagnosisToRemove.diagnosis.nonCoded,
|
|
289
|
+
),
|
|
290
|
+
);
|
|
291
|
+
} else if (searchInputField === 'secondaryInputSearch') {
|
|
292
|
+
setSelectedSecondaryDiagnoses(
|
|
293
|
+
selectedSecondaryDiagnoses.filter((diagnosis) =>
|
|
294
|
+
diagnosis.diagnosis.coded
|
|
295
|
+
? diagnosis.diagnosis.coded !== diagnosisToRemove.diagnosis.coded
|
|
296
|
+
: diagnosis.diagnosis.nonCoded !== diagnosisToRemove.diagnosis.nonCoded,
|
|
297
|
+
),
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
setCombinedDiagnoses(
|
|
301
|
+
combinedDiagnoses.filter((diagnosis) =>
|
|
302
|
+
diagnosis.diagnosis.coded
|
|
303
|
+
? diagnosis.diagnosis.coded !== diagnosisToRemove.diagnosis.coded
|
|
304
|
+
: diagnosis.diagnosis.nonCoded !== diagnosisToRemove.diagnosis.nonCoded,
|
|
305
|
+
),
|
|
306
|
+
);
|
|
307
|
+
},
|
|
308
|
+
[combinedDiagnoses, selectedPrimaryDiagnoses, selectedSecondaryDiagnoses],
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
const isDiagnosisNotSelected = (diagnosis: Concept | FreeConcept) => {
|
|
312
|
+
const isPrimaryDiagnosisSelected = selectedPrimaryDiagnoses.some((selectedDiagnosis) =>
|
|
313
|
+
diagnosis.uuid
|
|
314
|
+
? diagnosis.uuid === selectedDiagnosis.diagnosis.coded
|
|
315
|
+
: diagnosis.display.toLocaleLowerCase() === selectedDiagnosis.diagnosis.nonCoded.toLocaleLowerCase(),
|
|
316
|
+
);
|
|
317
|
+
const isSecondaryDiagnosisSelected = selectedSecondaryDiagnoses.some((selectedDiagnosis) =>
|
|
318
|
+
diagnosis.uuid
|
|
319
|
+
? diagnosis.uuid === selectedDiagnosis.diagnosis.coded
|
|
320
|
+
: diagnosis.display.toLocaleLowerCase() === selectedDiagnosis.diagnosis.nonCoded.toLocaleLowerCase(),
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
return !isPrimaryDiagnosisSelected && !isSecondaryDiagnosisSelected;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
const showImageCaptureModal = useCallback(() => {
|
|
327
|
+
const close = showModal('capture-photo-modal', {
|
|
328
|
+
saveFile: (file: UploadedFile) => {
|
|
329
|
+
if (file.capturedFromWebcam && !file.fileName.includes('.')) {
|
|
330
|
+
file.fileName = `${file.fileName}.png`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
setValue('images', currentImages ? [...currentImages, file] : [file]);
|
|
334
|
+
close();
|
|
335
|
+
return Promise.resolve();
|
|
336
|
+
},
|
|
337
|
+
closeModal: () => {
|
|
338
|
+
close();
|
|
339
|
+
},
|
|
340
|
+
allowedExtensions:
|
|
341
|
+
allowedFileExtensions && Array.isArray(allowedFileExtensions)
|
|
342
|
+
? allowedFileExtensions.filter((ext) => !/pdf/i.test(ext))
|
|
343
|
+
: [],
|
|
344
|
+
collectDescription: true,
|
|
345
|
+
multipleFiles: true,
|
|
346
|
+
});
|
|
347
|
+
}, [allowedFileExtensions, currentImages, setValue]);
|
|
348
|
+
|
|
349
|
+
const handleRemoveImage = (index: number) => {
|
|
350
|
+
const updatedImages = [...currentImages];
|
|
351
|
+
updatedImages.splice(index, 1);
|
|
352
|
+
setValue('images', updatedImages);
|
|
353
|
+
|
|
354
|
+
showSnackbar({
|
|
355
|
+
title: t('imageRemoved', 'Image removed'),
|
|
356
|
+
kind: 'success',
|
|
357
|
+
isLowContrast: true,
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const onSubmit = useCallback(
|
|
362
|
+
(data: VisitNotesFormData) => {
|
|
363
|
+
const { noteDate, clinicalNote, images } = data;
|
|
364
|
+
|
|
365
|
+
if (isPrimaryDiagnosisRequired && !selectedPrimaryDiagnoses.length) {
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
let finalNoteDate = dayjs(noteDate);
|
|
370
|
+
const now = new Date();
|
|
371
|
+
if (finalNoteDate.diff(now, 'minute') <= 30) {
|
|
372
|
+
finalNoteDate = null;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const existingClinicalNoteObs = encounter?.obs?.find((obs) => obs.concept.uuid === encounterNoteTextConceptUuid);
|
|
376
|
+
|
|
377
|
+
const visitNotePayload: VisitNotePayload = {
|
|
378
|
+
encounterDatetime: finalNoteDate?.format(),
|
|
379
|
+
form: formConceptUuid,
|
|
380
|
+
patient: patientUuid,
|
|
381
|
+
location: locationUuid,
|
|
382
|
+
encounterProviders: [
|
|
383
|
+
{
|
|
384
|
+
encounterRole: clinicianEncounterRole,
|
|
385
|
+
provider: providerUuid,
|
|
386
|
+
},
|
|
387
|
+
],
|
|
388
|
+
encounterType: encounterTypeUuid,
|
|
389
|
+
obs: clinicalNote
|
|
390
|
+
? [
|
|
391
|
+
{
|
|
392
|
+
concept: { uuid: encounterNoteTextConceptUuid, display: '' },
|
|
393
|
+
value: clinicalNote,
|
|
394
|
+
...(existingClinicalNoteObs && { uuid: existingClinicalNoteObs.uuid }),
|
|
395
|
+
},
|
|
396
|
+
]
|
|
397
|
+
: [],
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const abortController = new AbortController();
|
|
401
|
+
|
|
402
|
+
const savePromise = isEditing
|
|
403
|
+
? updateVisitNote(abortController, encounter.id, visitNotePayload)
|
|
404
|
+
: saveVisitNote(abortController, visitNotePayload);
|
|
405
|
+
|
|
406
|
+
return savePromise
|
|
407
|
+
.then((response) => {
|
|
408
|
+
if (response.status === 201 || response.status === 200) {
|
|
409
|
+
const encounterUuid = encounter?.id || response.data.uuid;
|
|
410
|
+
|
|
411
|
+
// If editing, first delete existing diagnoses
|
|
412
|
+
if (isEditing && encounter?.diagnoses?.length) {
|
|
413
|
+
return Promise.all(
|
|
414
|
+
encounter.diagnoses.map((diagnosis) => deletePatientDiagnosis(abortController, diagnosis.uuid)),
|
|
415
|
+
).then(() => encounterUuid);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
return encounterUuid;
|
|
419
|
+
}
|
|
420
|
+
})
|
|
421
|
+
.then((encounterUuid) => {
|
|
422
|
+
return Promise.all(
|
|
423
|
+
combinedDiagnoses.map((diagnosis) => {
|
|
424
|
+
const diagnosesPayload: DiagnosisPayload = {
|
|
425
|
+
encounter: encounterUuid,
|
|
426
|
+
patient: patientUuid,
|
|
427
|
+
condition: null,
|
|
428
|
+
diagnosis: {
|
|
429
|
+
coded: diagnosis.diagnosis.coded,
|
|
430
|
+
nonCoded: diagnosis.diagnosis.nonCoded,
|
|
431
|
+
},
|
|
432
|
+
certainty: diagnosis.certainty,
|
|
433
|
+
rank: diagnosis.rank,
|
|
434
|
+
};
|
|
435
|
+
return savePatientDiagnosis(abortController, diagnosesPayload);
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
})
|
|
439
|
+
.then(() => {
|
|
440
|
+
if (images?.length) {
|
|
441
|
+
return Promise.all(
|
|
442
|
+
images.map((image) => {
|
|
443
|
+
const imageToUpload: UploadedFile = {
|
|
444
|
+
base64Content: image.base64Content,
|
|
445
|
+
file: image.file,
|
|
446
|
+
fileName: image.fileName,
|
|
447
|
+
fileType: image.fileType,
|
|
448
|
+
fileDescription: image.fileDescription || '',
|
|
449
|
+
};
|
|
450
|
+
return createAttachment(patientUuid, imageToUpload);
|
|
451
|
+
}),
|
|
452
|
+
);
|
|
453
|
+
} else {
|
|
454
|
+
return Promise.resolve([]);
|
|
455
|
+
}
|
|
456
|
+
})
|
|
457
|
+
.then(() => {
|
|
458
|
+
// Invalidate encounter and notes data since we created a new encounter with notes
|
|
459
|
+
// Also invalidate visit history table since the visit now has new encounters
|
|
460
|
+
invalidateVisitAndEncounterData(globalMutate, patientUuid);
|
|
461
|
+
mutateVisitNotes();
|
|
462
|
+
|
|
463
|
+
if (images?.length) {
|
|
464
|
+
mutateAttachments();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
closeWorkspace({ discardUnsavedChanges: true });
|
|
468
|
+
|
|
469
|
+
showSnackbar({
|
|
470
|
+
isLowContrast: true,
|
|
471
|
+
subtitle: t('visitNoteNowVisible', 'It is now visible on the Visits page'),
|
|
472
|
+
kind: 'success',
|
|
473
|
+
title: t('visitNoteSaved', 'Visit note saved'),
|
|
474
|
+
});
|
|
475
|
+
})
|
|
476
|
+
.catch((err) => {
|
|
477
|
+
createErrorHandler();
|
|
478
|
+
|
|
479
|
+
showSnackbar({
|
|
480
|
+
title: t('visitNoteSaveError', 'Error saving visit note'),
|
|
481
|
+
kind: 'error',
|
|
482
|
+
isLowContrast: false,
|
|
483
|
+
subtitle: err?.responseBody?.error?.message ?? err.message,
|
|
484
|
+
});
|
|
485
|
+
});
|
|
486
|
+
},
|
|
487
|
+
[
|
|
488
|
+
clinicianEncounterRole,
|
|
489
|
+
closeWorkspace,
|
|
490
|
+
combinedDiagnoses,
|
|
491
|
+
encounter?.diagnoses,
|
|
492
|
+
encounter?.id,
|
|
493
|
+
encounter?.obs,
|
|
494
|
+
encounterNoteTextConceptUuid,
|
|
495
|
+
encounterTypeUuid,
|
|
496
|
+
formConceptUuid,
|
|
497
|
+
globalMutate,
|
|
498
|
+
isEditing,
|
|
499
|
+
isPrimaryDiagnosisRequired,
|
|
500
|
+
locationUuid,
|
|
501
|
+
mutateAttachments,
|
|
502
|
+
mutateVisitNotes,
|
|
503
|
+
patientUuid,
|
|
504
|
+
providerUuid,
|
|
505
|
+
selectedPrimaryDiagnoses.length,
|
|
506
|
+
t,
|
|
507
|
+
],
|
|
508
|
+
);
|
|
509
|
+
|
|
510
|
+
const onError = (errors) => console.error(errors);
|
|
511
|
+
|
|
512
|
+
const hasUserUnsavedChanges = Object.keys(dirtyFields).length > 0;
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
<Workspace2 title={t('visitNoteWorkspaceTitle', 'Visit note')} hasUnsavedChanges={hasUserUnsavedChanges}>
|
|
516
|
+
<Form className={styles.form} onSubmit={handleSubmit(onSubmit, onError)}>
|
|
517
|
+
<ExtensionSlot name="visit-context-header-slot" state={{ patientUuid }} />
|
|
518
|
+
|
|
519
|
+
{isTablet && (
|
|
520
|
+
<Row className={styles.headerGridRow}>
|
|
521
|
+
<ExtensionSlot name="visit-form-header-slot" className={styles.dataGridRow} state={memoizedState} />
|
|
522
|
+
</Row>
|
|
523
|
+
)}
|
|
524
|
+
|
|
525
|
+
<div className={styles.formContainer}>
|
|
526
|
+
<Stack gap={2}>
|
|
527
|
+
{isTablet ? <h2 className={styles.heading}>{t('addVisitNote', 'Add a visit note')}</h2> : null}
|
|
528
|
+
<Row className={styles.row}>
|
|
529
|
+
<Column sm={1}>
|
|
530
|
+
<span className={styles.columnLabel}>{t('date', 'Date')}</span>
|
|
531
|
+
</Column>
|
|
532
|
+
<Column sm={3}>
|
|
533
|
+
<Controller
|
|
534
|
+
name="noteDate"
|
|
535
|
+
control={control}
|
|
536
|
+
render={({ field, fieldState }) => (
|
|
537
|
+
<ResponsiveWrapper>
|
|
538
|
+
<OpenmrsDatePicker
|
|
539
|
+
{...field}
|
|
540
|
+
data-testid="visitDateTimePicker"
|
|
541
|
+
id="visitDateTimePicker"
|
|
542
|
+
invalid={Boolean(fieldState?.error?.message)}
|
|
543
|
+
invalidText={fieldState?.error?.message}
|
|
544
|
+
isDisabled={isEditing}
|
|
545
|
+
labelText={t('visitDate', 'Visit date')}
|
|
546
|
+
maxDate={new Date()}
|
|
547
|
+
/>
|
|
548
|
+
</ResponsiveWrapper>
|
|
549
|
+
)}
|
|
550
|
+
/>
|
|
551
|
+
</Column>
|
|
552
|
+
</Row>
|
|
553
|
+
<div className={styles.diagnosesText}>
|
|
554
|
+
{selectedPrimaryDiagnoses && selectedPrimaryDiagnoses.length ? (
|
|
555
|
+
<>
|
|
556
|
+
{selectedPrimaryDiagnoses.map((diagnosis, index) => (
|
|
557
|
+
<Tag
|
|
558
|
+
className={styles.tag}
|
|
559
|
+
filter
|
|
560
|
+
key={index}
|
|
561
|
+
onClose={() => handleRemoveDiagnosis(diagnosis, 'primaryInputSearch')}
|
|
562
|
+
type="red"
|
|
563
|
+
>
|
|
564
|
+
{diagnosis.display}
|
|
565
|
+
</Tag>
|
|
566
|
+
))}
|
|
567
|
+
</>
|
|
568
|
+
) : null}
|
|
569
|
+
{selectedSecondaryDiagnoses && selectedSecondaryDiagnoses.length ? (
|
|
570
|
+
<>
|
|
571
|
+
{selectedSecondaryDiagnoses.map((diagnosis, index) => (
|
|
572
|
+
<Tag
|
|
573
|
+
className={styles.tag}
|
|
574
|
+
filter
|
|
575
|
+
key={index}
|
|
576
|
+
onClose={() => handleRemoveDiagnosis(diagnosis, 'secondaryInputSearch')}
|
|
577
|
+
type="blue"
|
|
578
|
+
>
|
|
579
|
+
{diagnosis.display}
|
|
580
|
+
</Tag>
|
|
581
|
+
))}
|
|
582
|
+
</>
|
|
583
|
+
) : null}
|
|
584
|
+
{selectedPrimaryDiagnoses &&
|
|
585
|
+
!selectedPrimaryDiagnoses.length &&
|
|
586
|
+
selectedSecondaryDiagnoses &&
|
|
587
|
+
!selectedSecondaryDiagnoses.length && (
|
|
588
|
+
<span>{t('emptyDiagnosisText', 'No diagnosis selected — Enter a diagnosis below')}</span>
|
|
589
|
+
)}
|
|
590
|
+
</div>
|
|
591
|
+
<Row className={styles.row}>
|
|
592
|
+
<Column sm={1}>
|
|
593
|
+
<span className={styles.columnLabel}>{t('primaryDiagnosis', 'Primary diagnosis')}</span>
|
|
594
|
+
</Column>
|
|
595
|
+
<Column sm={3}>
|
|
596
|
+
<FormGroup legendText={t('searchForPrimaryDiagnosis', 'Search for a primary diagnosis')}>
|
|
597
|
+
<DiagnosisSearch
|
|
598
|
+
name="primaryDiagnosisSearch"
|
|
599
|
+
control={control}
|
|
600
|
+
labelText={t('enterPrimaryDiagnoses', 'Enter Primary diagnoses')}
|
|
601
|
+
placeholder={t('primaryDiagnosisInputPlaceholder', 'Choose a primary diagnosis')}
|
|
602
|
+
handleSearch={handleSearch}
|
|
603
|
+
error={errors?.primaryDiagnosisSearch}
|
|
604
|
+
setIsSearching={setIsSearching}
|
|
605
|
+
/>
|
|
606
|
+
{error ? (
|
|
607
|
+
<InlineNotification
|
|
608
|
+
className={styles.errorNotification}
|
|
609
|
+
lowContrast
|
|
610
|
+
title={t('error', 'Error')}
|
|
611
|
+
subtitle={t('errorFetchingConcepts', 'There was a problem fetching concepts') + '.'}
|
|
612
|
+
onClose={() => setError(null)}
|
|
613
|
+
/>
|
|
614
|
+
) : null}
|
|
615
|
+
<DiagnosesDisplay
|
|
616
|
+
fieldName={'primaryDiagnosisSearch'}
|
|
617
|
+
isDiagnosisNotSelected={isDiagnosisNotSelected}
|
|
618
|
+
isLoading={isLoadingPrimaryDiagnoses}
|
|
619
|
+
isSearching={isSearching}
|
|
620
|
+
onAddDiagnosis={handleAddDiagnosis}
|
|
621
|
+
searchResults={searchPrimaryResults}
|
|
622
|
+
t={t}
|
|
623
|
+
value={watch('primaryDiagnosisSearch')}
|
|
624
|
+
/>
|
|
625
|
+
</FormGroup>
|
|
626
|
+
</Column>
|
|
627
|
+
</Row>
|
|
628
|
+
<Row className={styles.row}>
|
|
629
|
+
<Column sm={1}>
|
|
630
|
+
<span className={styles.columnLabel}>{t('secondaryDiagnosis', 'Secondary diagnosis')}</span>
|
|
631
|
+
</Column>
|
|
632
|
+
<Column sm={3}>
|
|
633
|
+
<FormGroup legendText={t('searchForSecondaryDiagnosis', 'Search for a secondary diagnosis')}>
|
|
634
|
+
<DiagnosisSearch
|
|
635
|
+
name="secondaryDiagnosisSearch"
|
|
636
|
+
control={control}
|
|
637
|
+
labelText={t('enterSecondaryDiagnoses', 'Enter Secondary diagnoses')}
|
|
638
|
+
placeholder={t('secondaryDiagnosisInputPlaceholder', 'Choose a secondary diagnosis')}
|
|
639
|
+
handleSearch={handleSearch}
|
|
640
|
+
setIsSearching={setIsSearching}
|
|
641
|
+
/>
|
|
642
|
+
{error ? (
|
|
643
|
+
<InlineNotification
|
|
644
|
+
className={styles.errorNotification}
|
|
645
|
+
lowContrast
|
|
646
|
+
title={t('error', 'Error')}
|
|
647
|
+
subtitle={t('errorFetchingConcepts', 'There was a problem fetching concepts') + '.'}
|
|
648
|
+
onClose={() => setError(null)}
|
|
649
|
+
/>
|
|
650
|
+
) : null}
|
|
651
|
+
<DiagnosesDisplay
|
|
652
|
+
fieldName={'secondaryDiagnosisSearch'}
|
|
653
|
+
isDiagnosisNotSelected={isDiagnosisNotSelected}
|
|
654
|
+
isLoading={isLoadingSecondaryDiagnoses}
|
|
655
|
+
isSearching={isSearching}
|
|
656
|
+
onAddDiagnosis={handleAddDiagnosis}
|
|
657
|
+
searchResults={searchSecondaryResults}
|
|
658
|
+
t={t}
|
|
659
|
+
value={watch('secondaryDiagnosisSearch')}
|
|
660
|
+
/>
|
|
661
|
+
</FormGroup>
|
|
662
|
+
</Column>
|
|
663
|
+
</Row>
|
|
664
|
+
<Row className={styles.row}>
|
|
665
|
+
<Column sm={1}>
|
|
666
|
+
<span className={styles.columnLabel}>{t('note', 'Note')}</span>
|
|
667
|
+
</Column>
|
|
668
|
+
<Column sm={3}>
|
|
669
|
+
<Controller
|
|
670
|
+
name="clinicalNote"
|
|
671
|
+
control={control}
|
|
672
|
+
render={({ field: { onChange, onBlur, value } }) => (
|
|
673
|
+
<ResponsiveWrapper>
|
|
674
|
+
<TextArea
|
|
675
|
+
id="additionalNote"
|
|
676
|
+
rows={rows}
|
|
677
|
+
labelText={t('clinicalNoteLabel', 'Write your notes')}
|
|
678
|
+
placeholder={t('clinicalNotePlaceholder', 'Write any notes here')}
|
|
679
|
+
value={value}
|
|
680
|
+
onBlur={onBlur}
|
|
681
|
+
onChange={(event) => {
|
|
682
|
+
onChange(event);
|
|
683
|
+
const textareaLineHeight = 24; // This is the default line height for Carbon's TextArea component
|
|
684
|
+
const newRows = Math.ceil(event.target.scrollHeight / textareaLineHeight);
|
|
685
|
+
setRows(newRows);
|
|
686
|
+
}}
|
|
687
|
+
/>
|
|
688
|
+
</ResponsiveWrapper>
|
|
689
|
+
)}
|
|
690
|
+
/>
|
|
691
|
+
</Column>
|
|
692
|
+
</Row>
|
|
693
|
+
<Row className={styles.row}>
|
|
694
|
+
<Column sm={1}>
|
|
695
|
+
<span className={styles.columnLabel}>{t('image', 'Image')}</span>
|
|
696
|
+
</Column>
|
|
697
|
+
<Column sm={3}>
|
|
698
|
+
<FormGroup legendText="">
|
|
699
|
+
<p className={styles.imgUploadHelperText}>
|
|
700
|
+
{t('imageUploadHelperText', "Upload images or use this device's camera to capture images")}
|
|
701
|
+
</p>
|
|
702
|
+
<Button
|
|
703
|
+
className={styles.uploadButton}
|
|
704
|
+
kind={isTablet ? 'ghost' : 'tertiary'}
|
|
705
|
+
onClick={showImageCaptureModal}
|
|
706
|
+
renderIcon={(props) => <Add size={16} {...props} />}
|
|
707
|
+
>
|
|
708
|
+
{t('addImage', 'Add image')}
|
|
709
|
+
</Button>
|
|
710
|
+
<div className={styles.imgThumbnailGrid}>
|
|
711
|
+
{currentImages?.map((image, index) => (
|
|
712
|
+
<div key={index} className={styles.imgThumbnailItem}>
|
|
713
|
+
<div className={styles.imgThumbnailContainer}>
|
|
714
|
+
<img
|
|
715
|
+
className={styles.imgThumbnail}
|
|
716
|
+
src={image.base64Content}
|
|
717
|
+
alt={image.fileDescription ?? image.fileName}
|
|
718
|
+
/>
|
|
719
|
+
</div>
|
|
720
|
+
<Button kind="ghost" className={styles.removeButton} onClick={() => handleRemoveImage(index)}>
|
|
721
|
+
<CloseFilled size={16} className={styles.closeIcon} />
|
|
722
|
+
</Button>
|
|
723
|
+
</div>
|
|
724
|
+
))}
|
|
725
|
+
</div>
|
|
726
|
+
</FormGroup>
|
|
727
|
+
</Column>
|
|
728
|
+
</Row>
|
|
729
|
+
</Stack>
|
|
730
|
+
</div>
|
|
731
|
+
<ButtonSet className={classnames({ [styles.tablet]: isTablet, [styles.desktop]: !isTablet })}>
|
|
732
|
+
<Button className={styles.button} kind="secondary" onClick={() => closeWorkspace()}>
|
|
733
|
+
{t('discard', 'Discard')}
|
|
734
|
+
</Button>
|
|
735
|
+
<Button
|
|
736
|
+
className={styles.button}
|
|
737
|
+
kind="primary"
|
|
738
|
+
disabled={!hasUserUnsavedChanges || isSubmitting}
|
|
739
|
+
type="submit"
|
|
740
|
+
>
|
|
741
|
+
{isSubmitting ? (
|
|
742
|
+
<InlineLoading className={styles.spinner} description={t('saving', 'Saving') + '...'} />
|
|
743
|
+
) : (
|
|
744
|
+
<span>{t('saveAndClose', 'Save and close')}</span>
|
|
745
|
+
)}
|
|
746
|
+
</Button>
|
|
747
|
+
</ButtonSet>
|
|
748
|
+
</Form>
|
|
749
|
+
</Workspace2>
|
|
750
|
+
);
|
|
751
|
+
};
|
|
752
|
+
|
|
753
|
+
function DiagnosisSearch({
|
|
754
|
+
name,
|
|
755
|
+
control,
|
|
756
|
+
labelText,
|
|
757
|
+
placeholder,
|
|
758
|
+
handleSearch,
|
|
759
|
+
error,
|
|
760
|
+
setIsSearching,
|
|
761
|
+
}: DiagnosisSearchProps) {
|
|
762
|
+
const isTablet = useLayoutType() === 'tablet';
|
|
763
|
+
const inputRef = useRef(null);
|
|
764
|
+
|
|
765
|
+
const searchInputFocus = () => {
|
|
766
|
+
inputRef.current.focus();
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
useEffect(() => {
|
|
770
|
+
if (error) {
|
|
771
|
+
searchInputFocus();
|
|
772
|
+
}
|
|
773
|
+
}, [error]);
|
|
774
|
+
|
|
775
|
+
return (
|
|
776
|
+
<Controller
|
|
777
|
+
name={name}
|
|
778
|
+
control={control}
|
|
779
|
+
render={({ field: { value, onChange, onBlur }, fieldState }) => (
|
|
780
|
+
<>
|
|
781
|
+
<ResponsiveWrapper>
|
|
782
|
+
<Search
|
|
783
|
+
ref={inputRef}
|
|
784
|
+
size={isTablet ? 'lg' : 'md'}
|
|
785
|
+
id={name}
|
|
786
|
+
labelText={labelText}
|
|
787
|
+
className={error && styles.diagnoserrorOutline}
|
|
788
|
+
placeholder={placeholder}
|
|
789
|
+
renderIcon={error && ((props) => <WarningFilled fill="red" {...props} />)}
|
|
790
|
+
onChange={(e) => {
|
|
791
|
+
setIsSearching(true);
|
|
792
|
+
onChange(e);
|
|
793
|
+
handleSearch(name);
|
|
794
|
+
}}
|
|
795
|
+
value={value instanceof Date ? value.toISOString() : value}
|
|
796
|
+
onBlur={onBlur}
|
|
797
|
+
/>
|
|
798
|
+
</ResponsiveWrapper>
|
|
799
|
+
{fieldState?.error?.message && <p className={styles.errorMessage}>{fieldState?.error?.message}</p>}
|
|
800
|
+
</>
|
|
801
|
+
)}
|
|
802
|
+
/>
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
function DiagnosesDisplay({
|
|
807
|
+
fieldName,
|
|
808
|
+
isDiagnosisNotSelected,
|
|
809
|
+
isLoading,
|
|
810
|
+
isSearching,
|
|
811
|
+
onAddDiagnosis,
|
|
812
|
+
searchResults,
|
|
813
|
+
t,
|
|
814
|
+
value: rawValue,
|
|
815
|
+
}: DiagnosesDisplayProps) {
|
|
816
|
+
const value = rawValue?.trim();
|
|
817
|
+
|
|
818
|
+
if (!value) {
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
if (isSearching || isLoading) {
|
|
823
|
+
return <Loader />;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
if (!isSearching && searchResults?.length > 0) {
|
|
827
|
+
if (
|
|
828
|
+
searchResults?.length === 1 &&
|
|
829
|
+
!isDiagnosisNotSelected(searchResults[0]) &&
|
|
830
|
+
searchResults[0]?.display?.toLocaleLowerCase() === value?.toLocaleLowerCase()
|
|
831
|
+
)
|
|
832
|
+
return (
|
|
833
|
+
<Tile className={styles.emptyResults}>
|
|
834
|
+
<span>
|
|
835
|
+
{t('diagnosisAlreadySelected', 'Diagnosis already selected')}: <strong>"{value}"</strong>
|
|
836
|
+
</span>
|
|
837
|
+
</Tile>
|
|
838
|
+
);
|
|
839
|
+
return (
|
|
840
|
+
<ul className={styles.diagnosisList}>
|
|
841
|
+
{searchResults.map((diagnosis, index) => {
|
|
842
|
+
if (isDiagnosisNotSelected(diagnosis)) {
|
|
843
|
+
return (
|
|
844
|
+
<li
|
|
845
|
+
className={styles.diagnosis}
|
|
846
|
+
key={index}
|
|
847
|
+
onClick={() => onAddDiagnosis(diagnosis, fieldName)}
|
|
848
|
+
role="menuitem"
|
|
849
|
+
>
|
|
850
|
+
{diagnosis.display}
|
|
851
|
+
</li>
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
})}
|
|
855
|
+
|
|
856
|
+
{isDiagnosisNotSelected({ display: value }) ? (
|
|
857
|
+
// if the searchResults doesn't contain the exact search term,
|
|
858
|
+
// still allow proposing adding it as a custom free-text diagnosis
|
|
859
|
+
!searchResults
|
|
860
|
+
.map((diagnosis) => diagnosis.display.toLocaleLowerCase())
|
|
861
|
+
.includes(value.toLocaleLowerCase()) && (
|
|
862
|
+
<li className={styles.diagnosis} role="menuitem">
|
|
863
|
+
<Button
|
|
864
|
+
size="md"
|
|
865
|
+
kind="ghost"
|
|
866
|
+
onClick={() =>
|
|
867
|
+
onAddDiagnosis(
|
|
868
|
+
{
|
|
869
|
+
display: value,
|
|
870
|
+
},
|
|
871
|
+
fieldName,
|
|
872
|
+
)
|
|
873
|
+
}
|
|
874
|
+
className={styles.customDiagnosisButton}
|
|
875
|
+
>
|
|
876
|
+
{t('addCustomDiagnosis', 'Add custom diagnosis')} <strong> "{value}"</strong>
|
|
877
|
+
</Button>
|
|
878
|
+
</li>
|
|
879
|
+
)
|
|
880
|
+
) : (
|
|
881
|
+
<Tile className={styles.emptyResults}>
|
|
882
|
+
<span>
|
|
883
|
+
{t('diagnosisAlreadySelected', 'Diagnosis already selected')}: <strong>"{value}"</strong>
|
|
884
|
+
</span>
|
|
885
|
+
</Tile>
|
|
886
|
+
)}
|
|
887
|
+
</ul>
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
if (searchResults?.length === 0) {
|
|
892
|
+
return (
|
|
893
|
+
<ResponsiveWrapper>
|
|
894
|
+
{isDiagnosisNotSelected({ display: value }) ? (
|
|
895
|
+
<>
|
|
896
|
+
<Tile className={styles.emptyResults}>
|
|
897
|
+
<span>
|
|
898
|
+
{t('noMatchingDiagnoses', 'No diagnoses found matching')} <strong>"{value}"</strong>
|
|
899
|
+
</span>
|
|
900
|
+
</Tile>
|
|
901
|
+
<ul className={styles.diagnosisList}>
|
|
902
|
+
<li className={styles.diagnosis} role="menuitem">
|
|
903
|
+
<Button
|
|
904
|
+
size="md"
|
|
905
|
+
kind="ghost"
|
|
906
|
+
onClick={() =>
|
|
907
|
+
onAddDiagnosis(
|
|
908
|
+
{
|
|
909
|
+
display: value,
|
|
910
|
+
},
|
|
911
|
+
fieldName,
|
|
912
|
+
)
|
|
913
|
+
}
|
|
914
|
+
className={styles.customDiagnosisButton}
|
|
915
|
+
>
|
|
916
|
+
{t('addCustomDiagnosis', 'Add custom diagnosis')} <strong> "{value}"</strong>
|
|
917
|
+
</Button>
|
|
918
|
+
</li>
|
|
919
|
+
</ul>
|
|
920
|
+
</>
|
|
921
|
+
) : (
|
|
922
|
+
<Tile className={styles.emptyResults}>
|
|
923
|
+
<span>
|
|
924
|
+
{t('diagnosisAlreadySelected', 'Diagnosis already selected')}: <strong>"{value}"</strong>
|
|
925
|
+
</span>
|
|
926
|
+
</Tile>
|
|
927
|
+
)}
|
|
928
|
+
</ResponsiveWrapper>
|
|
929
|
+
);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function Loader() {
|
|
934
|
+
return (
|
|
935
|
+
<>
|
|
936
|
+
{Array.from({ length: 5 }).map((_, index) => (
|
|
937
|
+
<SkeletonText key={index} className={styles.skeleton} />
|
|
938
|
+
))}
|
|
939
|
+
</>
|
|
940
|
+
);
|
|
941
|
+
}
|
|
942
|
+
|
|
943
|
+
export default VisitNotesForm;
|