@ampath/esm-patient-registration-app 6.0.1-pre.6
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 +41 -0
- package/README.md +7 -0
- package/dist/130.js +2 -0
- package/dist/130.js.LICENSE.txt +3 -0
- package/dist/130.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/249.js +2 -0
- package/dist/249.js.LICENSE.txt +46 -0
- package/dist/249.js.map +1 -0
- package/dist/255.js +2 -0
- package/dist/255.js.LICENSE.txt +9 -0
- package/dist/255.js.map +1 -0
- package/dist/271.js +1 -0
- package/dist/303.js +1 -0
- package/dist/303.js.map +1 -0
- package/dist/319.js +1 -0
- package/dist/365.js +1 -0
- package/dist/365.js.map +1 -0
- package/dist/460.js +1 -0
- package/dist/525.js +1 -0
- package/dist/525.js.map +1 -0
- package/dist/537.js +1 -0
- package/dist/537.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/591.js +2 -0
- package/dist/591.js.LICENSE.txt +32 -0
- package/dist/591.js.map +1 -0
- package/dist/621.js +1 -0
- package/dist/621.js.map +1 -0
- package/dist/644.js +1 -0
- package/dist/729.js +1 -0
- package/dist/729.js.map +1 -0
- package/dist/735.js +1 -0
- package/dist/735.js.map +1 -0
- package/dist/757.js +1 -0
- package/dist/784.js +2 -0
- package/dist/784.js.LICENSE.txt +9 -0
- package/dist/784.js.map +1 -0
- package/dist/788.js +1 -0
- package/dist/807.js +1 -0
- package/dist/833.js +1 -0
- package/dist/879.js +1 -0
- package/dist/879.js.map +1 -0
- package/dist/ampath-esm-patient-registration-app.js +1 -0
- package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
- package/dist/ampath-esm-patient-registration-app.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +56 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/docs/images/patient-registration-hierarchy.png +0 -0
- package/jest.config.js +3 -0
- package/package.json +61 -0
- package/src/add-patient-link.scss +3 -0
- package/src/add-patient-link.test.tsx +20 -0
- package/src/add-patient-link.tsx +21 -0
- package/src/config-schema.ts +410 -0
- package/src/constants.ts +14 -0
- package/src/declarations.d.ts +6 -0
- package/src/index.ts +71 -0
- package/src/nav-link.test.tsx +13 -0
- package/src/nav-link.tsx +10 -0
- package/src/offline.resources.ts +155 -0
- package/src/offline.ts +91 -0
- package/src/patient-registration/before-save-prompt.tsx +73 -0
- package/src/patient-registration/date-util.ts +52 -0
- package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
- package/src/patient-registration/field/address/address-field.component.tsx +153 -0
- package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
- package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
- package/src/patient-registration/field/address/address-search.component.tsx +85 -0
- package/src/patient-registration/field/address/address-search.scss +53 -0
- package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
- package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
- package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
- package/src/patient-registration/field/custom-field.component.tsx +25 -0
- package/src/patient-registration/field/dob/dob.component.tsx +159 -0
- package/src/patient-registration/field/dob/dob.test.tsx +75 -0
- package/src/patient-registration/field/field.component.tsx +47 -0
- package/src/patient-registration/field/field.resource.ts +35 -0
- package/src/patient-registration/field/field.scss +127 -0
- package/src/patient-registration/field/field.test.tsx +294 -0
- package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
- package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
- package/src/patient-registration/field/id/id-field.component.tsx +144 -0
- package/src/patient-registration/field/id/id-field.test.tsx +107 -0
- package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
- package/src/patient-registration/field/id/identifier-selection.scss +37 -0
- package/src/patient-registration/field/name/name-field.component.tsx +142 -0
- package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
- package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
- package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
- package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
- package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
- package/src/patient-registration/form-manager.test.ts +67 -0
- package/src/patient-registration/form-manager.ts +414 -0
- package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
- package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
- package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
- package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
- package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
- package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
- package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
- package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
- package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
- package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
- package/src/patient-registration/input/input.scss +118 -0
- package/src/patient-registration/patient-registration-context.ts +24 -0
- package/src/patient-registration/patient-registration-hooks.ts +287 -0
- package/src/patient-registration/patient-registration-utils.ts +216 -0
- package/src/patient-registration/patient-registration.component.tsx +240 -0
- package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
- package/src/patient-registration/patient-registration.resource.ts +250 -0
- package/src/patient-registration/patient-registration.scss +122 -0
- package/src/patient-registration/patient-registration.test.tsx +471 -0
- package/src/patient-registration/patient-registration.types.ts +318 -0
- package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
- package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
- package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
- package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
- package/src/patient-registration/section/generic-section.component.tsx +17 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
- package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
- package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
- package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
- package/src/patient-registration/section/section.component.tsx +23 -0
- package/src/patient-registration/section/section.scss +1 -0
- package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
- package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
- package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
- package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
- package/src/patient-verification/client-registry-constants.ts +13 -0
- package/src/patient-verification/client-registry.component.tsx +66 -0
- package/src/patient-verification/client-registry.scss +1 -0
- package/src/patient-verification/utils.tsx +56 -0
- package/src/patient-verification/verification-modal.scss +20 -0
- package/src/patient-verification/verification.component.tsx +48 -0
- package/src/resource.ts +12 -0
- package/src/root.component.tsx +63 -0
- package/src/root.scss +7 -0
- package/src/root.test.tsx +32 -0
- package/src/routes.json +66 -0
- package/src/widgets/cancel-patient-edit.component.tsx +37 -0
- package/src/widgets/cancel-patient-edit.test.tsx +27 -0
- package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
- package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
- package/src/widgets/delete-identifier-modal.scss +34 -0
- package/src/widgets/display-photo.component.tsx +30 -0
- package/src/widgets/display-photo.test.tsx +37 -0
- package/src/widgets/edit-patient-details-button.component.tsx +34 -0
- package/src/widgets/edit-patient-details-button.scss +3 -0
- package/src/widgets/edit-patient-details-button.test.tsx +41 -0
- package/translations/am.json +97 -0
- package/translations/ar.json +97 -0
- package/translations/en.json +103 -0
- package/translations/es.json +97 -0
- package/translations/fr.json +97 -0
- package/translations/he.json +97 -0
- package/translations/km.json +97 -0
- package/translations/zh.json +89 -0
- package/translations/zh_CN.json +89 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { type FetchResponse, openmrsFetch, queueSynchronizationItem, type Session } from '@openmrs/esm-framework';
|
|
2
|
+
import { patientRegistration } from '../constants';
|
|
3
|
+
import {
|
|
4
|
+
type FormValues,
|
|
5
|
+
type AttributeValue,
|
|
6
|
+
type PatientUuidMapType,
|
|
7
|
+
type Patient,
|
|
8
|
+
type CapturePhotoProps,
|
|
9
|
+
type PatientIdentifier,
|
|
10
|
+
type PatientRegistration,
|
|
11
|
+
type RelationshipValue,
|
|
12
|
+
type Encounter,
|
|
13
|
+
} from './patient-registration.types';
|
|
14
|
+
import {
|
|
15
|
+
addPatientIdentifier,
|
|
16
|
+
deletePatientIdentifier,
|
|
17
|
+
deletePersonName,
|
|
18
|
+
deleteRelationship,
|
|
19
|
+
generateIdentifier,
|
|
20
|
+
savePatient,
|
|
21
|
+
savePatientPhoto,
|
|
22
|
+
saveRelationship,
|
|
23
|
+
updateRelationship,
|
|
24
|
+
updatePatientIdentifier,
|
|
25
|
+
saveEncounter,
|
|
26
|
+
} from './patient-registration.resource';
|
|
27
|
+
import { type RegistrationConfig } from '../config-schema';
|
|
28
|
+
|
|
29
|
+
export type SavePatientForm = (
|
|
30
|
+
isNewPatient: boolean,
|
|
31
|
+
values: FormValues,
|
|
32
|
+
patientUuidMap: PatientUuidMapType,
|
|
33
|
+
initialAddressFieldValues: Record<string, any>,
|
|
34
|
+
capturePhotoProps: CapturePhotoProps,
|
|
35
|
+
currentLocation: string,
|
|
36
|
+
initialIdentifierValues: FormValues['identifiers'],
|
|
37
|
+
currentUser: Session,
|
|
38
|
+
config: RegistrationConfig,
|
|
39
|
+
savePatientTransactionManager: SavePatientTransactionManager,
|
|
40
|
+
abortController?: AbortController,
|
|
41
|
+
) => Promise<string | void>;
|
|
42
|
+
|
|
43
|
+
export class FormManager {
|
|
44
|
+
static savePatientFormOffline: SavePatientForm = async (
|
|
45
|
+
isNewPatient,
|
|
46
|
+
values,
|
|
47
|
+
patientUuidMap,
|
|
48
|
+
initialAddressFieldValues,
|
|
49
|
+
capturePhotoProps,
|
|
50
|
+
currentLocation,
|
|
51
|
+
initialIdentifierValues,
|
|
52
|
+
currentUser,
|
|
53
|
+
config,
|
|
54
|
+
) => {
|
|
55
|
+
const syncItem: PatientRegistration = {
|
|
56
|
+
fhirPatient: FormManager.mapPatientToFhirPatient(
|
|
57
|
+
FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, []),
|
|
58
|
+
),
|
|
59
|
+
_patientRegistrationData: {
|
|
60
|
+
isNewPatient,
|
|
61
|
+
formValues: values,
|
|
62
|
+
patientUuidMap,
|
|
63
|
+
initialAddressFieldValues,
|
|
64
|
+
capturePhotoProps,
|
|
65
|
+
currentLocation,
|
|
66
|
+
initialIdentifierValues,
|
|
67
|
+
currentUser,
|
|
68
|
+
config,
|
|
69
|
+
savePatientTransactionManager: new SavePatientTransactionManager(),
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
await queueSynchronizationItem(patientRegistration, syncItem, {
|
|
74
|
+
id: values.patientUuid,
|
|
75
|
+
displayName: 'Patient registration',
|
|
76
|
+
patientUuid: syncItem.fhirPatient.id,
|
|
77
|
+
dependencies: [],
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
return null;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
static savePatientFormOnline: SavePatientForm = async (
|
|
84
|
+
isNewPatient,
|
|
85
|
+
values,
|
|
86
|
+
patientUuidMap,
|
|
87
|
+
initialAddressFieldValues,
|
|
88
|
+
capturePhotoProps,
|
|
89
|
+
currentLocation,
|
|
90
|
+
initialIdentifierValues,
|
|
91
|
+
currentUser,
|
|
92
|
+
config,
|
|
93
|
+
savePatientTransactionManager,
|
|
94
|
+
abortController,
|
|
95
|
+
) => {
|
|
96
|
+
const patientIdentifiers: Array<PatientIdentifier> = await FormManager.savePatientIdentifiers(
|
|
97
|
+
isNewPatient,
|
|
98
|
+
values.patientUuid,
|
|
99
|
+
values.identifiers,
|
|
100
|
+
initialIdentifierValues,
|
|
101
|
+
currentLocation,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
const createdPatient = FormManager.getPatientToCreate(
|
|
105
|
+
isNewPatient,
|
|
106
|
+
values,
|
|
107
|
+
patientUuidMap,
|
|
108
|
+
initialAddressFieldValues,
|
|
109
|
+
patientIdentifiers,
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
|
|
113
|
+
await deletePersonName(name.nameUuid, name.personUuid);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const savePatientResponse = await savePatient(
|
|
117
|
+
createdPatient,
|
|
118
|
+
isNewPatient && !savePatientTransactionManager.patientSaved ? undefined : values.patientUuid,
|
|
119
|
+
);
|
|
120
|
+
|
|
121
|
+
if (savePatientResponse.ok) {
|
|
122
|
+
savePatientTransactionManager.patientSaved = true;
|
|
123
|
+
await this.saveRelationships(values.relationships, savePatientResponse);
|
|
124
|
+
|
|
125
|
+
await this.saveObservations(values.obs, savePatientResponse, currentLocation, currentUser, config);
|
|
126
|
+
|
|
127
|
+
if (config.concepts.patientPhotoUuid && capturePhotoProps?.imageData) {
|
|
128
|
+
await savePatientPhoto(
|
|
129
|
+
savePatientResponse.data.uuid,
|
|
130
|
+
capturePhotoProps.imageData,
|
|
131
|
+
'/ws/rest/v1/obs',
|
|
132
|
+
capturePhotoProps.dateTime || new Date().toISOString(),
|
|
133
|
+
config.concepts.patientPhotoUuid,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return savePatientResponse.data.uuid;
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
static async saveRelationships(relationships: Array<RelationshipValue>, savePatientResponse: FetchResponse) {
|
|
142
|
+
return Promise.all(
|
|
143
|
+
relationships
|
|
144
|
+
.filter((m) => m.relationshipType)
|
|
145
|
+
.filter((relationship) => !!relationship.action)
|
|
146
|
+
.map(({ relatedPersonUuid, relationshipType, uuid: relationshipUuid, action }) => {
|
|
147
|
+
const [type, direction] = relationshipType.split('/');
|
|
148
|
+
const thisPatientUuid = savePatientResponse.data.uuid;
|
|
149
|
+
const isAToB = direction === 'aIsToB';
|
|
150
|
+
const relationshipToSave = {
|
|
151
|
+
personA: isAToB ? relatedPersonUuid : thisPatientUuid,
|
|
152
|
+
personB: isAToB ? thisPatientUuid : relatedPersonUuid,
|
|
153
|
+
relationshipType: type,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
switch (action) {
|
|
157
|
+
case 'ADD':
|
|
158
|
+
return saveRelationship(relationshipToSave);
|
|
159
|
+
case 'UPDATE':
|
|
160
|
+
return updateRelationship(relationshipUuid, relationshipToSave);
|
|
161
|
+
case 'DELETE':
|
|
162
|
+
return deleteRelationship(relationshipUuid);
|
|
163
|
+
}
|
|
164
|
+
}),
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
static async saveObservations(
|
|
169
|
+
obss: { [conceptUuid: string]: string },
|
|
170
|
+
savePatientResponse: FetchResponse,
|
|
171
|
+
currentLocation: string,
|
|
172
|
+
currentUser: Session,
|
|
173
|
+
config: RegistrationConfig,
|
|
174
|
+
) {
|
|
175
|
+
if (obss && Object.keys(obss).length > 0) {
|
|
176
|
+
if (!config.registrationObs.encounterTypeUuid) {
|
|
177
|
+
console.error(
|
|
178
|
+
'The registration form has been configured to have obs fields, ' +
|
|
179
|
+
'but no registration encounter type has been configured. Obs field values ' +
|
|
180
|
+
'will not be saved.',
|
|
181
|
+
);
|
|
182
|
+
} else {
|
|
183
|
+
const encounterToSave: Encounter = {
|
|
184
|
+
encounterDatetime: new Date(),
|
|
185
|
+
patient: savePatientResponse.data.uuid,
|
|
186
|
+
encounterType: config.registrationObs.encounterTypeUuid,
|
|
187
|
+
location: currentLocation,
|
|
188
|
+
encounterProviders: [
|
|
189
|
+
{
|
|
190
|
+
provider: currentUser.currentProvider.uuid,
|
|
191
|
+
encounterRole: config.registrationObs.encounterProviderRoleUuid,
|
|
192
|
+
},
|
|
193
|
+
],
|
|
194
|
+
form: config.registrationObs.registrationFormUuid,
|
|
195
|
+
obs: Object.entries(obss)
|
|
196
|
+
.filter(([, value]) => value !== '')
|
|
197
|
+
.map(([conceptUuid, value]) => ({ concept: conceptUuid, value })),
|
|
198
|
+
};
|
|
199
|
+
return saveEncounter(encounterToSave);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
static async savePatientIdentifiers(
|
|
205
|
+
isNewPatient: boolean,
|
|
206
|
+
patientUuid: string,
|
|
207
|
+
patientIdentifiers: FormValues['identifiers'], // values.identifiers
|
|
208
|
+
initialIdentifierValues: FormValues['identifiers'], // Initial identifiers assigned to the patient
|
|
209
|
+
location: string,
|
|
210
|
+
): Promise<Array<PatientIdentifier>> {
|
|
211
|
+
let identifierTypeRequests = Object.values(patientIdentifiers)
|
|
212
|
+
/* Since default identifier-types will be present on the form and are also in the not-required state,
|
|
213
|
+
therefore we might be running into situations when there's no value and no source associated,
|
|
214
|
+
hence filtering these fields out.
|
|
215
|
+
*/
|
|
216
|
+
.filter(
|
|
217
|
+
({ identifierValue, autoGeneration, selectedSource }) => identifierValue || (autoGeneration && selectedSource),
|
|
218
|
+
)
|
|
219
|
+
.map(async (patientIdentifier) => {
|
|
220
|
+
const {
|
|
221
|
+
identifierTypeUuid,
|
|
222
|
+
identifierValue,
|
|
223
|
+
identifierUuid,
|
|
224
|
+
selectedSource,
|
|
225
|
+
preferred,
|
|
226
|
+
autoGeneration,
|
|
227
|
+
initialValue,
|
|
228
|
+
} = patientIdentifier;
|
|
229
|
+
|
|
230
|
+
const identifier = !autoGeneration
|
|
231
|
+
? identifierValue
|
|
232
|
+
: await (
|
|
233
|
+
await generateIdentifier(selectedSource.uuid)
|
|
234
|
+
).data.identifier;
|
|
235
|
+
const identifierToCreate = {
|
|
236
|
+
uuid: identifierUuid,
|
|
237
|
+
identifier,
|
|
238
|
+
identifierType: identifierTypeUuid,
|
|
239
|
+
location,
|
|
240
|
+
preferred,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (!isNewPatient) {
|
|
244
|
+
if (!initialValue) {
|
|
245
|
+
await addPatientIdentifier(patientUuid, identifierToCreate);
|
|
246
|
+
} else if (initialValue !== identifier) {
|
|
247
|
+
await updatePatientIdentifier(patientUuid, identifierUuid, identifierToCreate.identifier);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return identifierToCreate;
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
/*
|
|
255
|
+
If there was initially an identifier assigned to the patient,
|
|
256
|
+
which is now not present in the patientIdentifiers(values.identifiers),
|
|
257
|
+
this means that the identifier is meant to be deleted, hence we need
|
|
258
|
+
to delete the respective identifiers.
|
|
259
|
+
*/
|
|
260
|
+
|
|
261
|
+
if (patientUuid) {
|
|
262
|
+
Object.keys(initialIdentifierValues)
|
|
263
|
+
.filter((identifierFieldName) => !patientIdentifiers[identifierFieldName])
|
|
264
|
+
.forEach(async (identifierFieldName) => {
|
|
265
|
+
await deletePatientIdentifier(patientUuid, initialIdentifierValues[identifierFieldName].identifierUuid);
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return Promise.all(identifierTypeRequests);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static getDeletedNames(patientUuid: string, patientUuidMap: PatientUuidMapType) {
|
|
273
|
+
if (patientUuidMap?.additionalNameUuid) {
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
nameUuid: patientUuidMap.additionalNameUuid,
|
|
277
|
+
personUuid: patientUuid,
|
|
278
|
+
},
|
|
279
|
+
];
|
|
280
|
+
}
|
|
281
|
+
return [];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
static getPatientToCreate(
|
|
285
|
+
isNewPatient: boolean,
|
|
286
|
+
values: FormValues,
|
|
287
|
+
patientUuidMap: PatientUuidMapType,
|
|
288
|
+
initialAddressFieldValues: Record<string, any>,
|
|
289
|
+
identifiers: Array<PatientIdentifier>,
|
|
290
|
+
): Patient {
|
|
291
|
+
let birthdate;
|
|
292
|
+
if (values.birthdate instanceof Date) {
|
|
293
|
+
birthdate = [values.birthdate.getFullYear(), values.birthdate.getMonth() + 1, values.birthdate.getDate()].join(
|
|
294
|
+
'-',
|
|
295
|
+
);
|
|
296
|
+
} else {
|
|
297
|
+
birthdate = values.birthdate;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return {
|
|
301
|
+
uuid: values.patientUuid,
|
|
302
|
+
person: {
|
|
303
|
+
uuid: values.patientUuid,
|
|
304
|
+
names: FormManager.getNames(values, patientUuidMap),
|
|
305
|
+
gender: values.gender.charAt(0).toUpperCase(),
|
|
306
|
+
birthdate,
|
|
307
|
+
birthdateEstimated: values.birthdateEstimated,
|
|
308
|
+
attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
|
|
309
|
+
addresses: [values.address],
|
|
310
|
+
...FormManager.getPatientDeathInfo(values),
|
|
311
|
+
},
|
|
312
|
+
identifiers,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
static getNames(values: FormValues, patientUuidMap: PatientUuidMapType) {
|
|
317
|
+
const names = [
|
|
318
|
+
{
|
|
319
|
+
uuid: patientUuidMap.preferredNameUuid,
|
|
320
|
+
preferred: true,
|
|
321
|
+
givenName: values.givenName,
|
|
322
|
+
middleName: values.middleName,
|
|
323
|
+
familyName: values.familyName,
|
|
324
|
+
},
|
|
325
|
+
];
|
|
326
|
+
|
|
327
|
+
if (values.addNameInLocalLanguage) {
|
|
328
|
+
names.push({
|
|
329
|
+
uuid: patientUuidMap.additionalNameUuid,
|
|
330
|
+
preferred: false,
|
|
331
|
+
givenName: values.additionalGivenName,
|
|
332
|
+
middleName: values.additionalMiddleName,
|
|
333
|
+
familyName: values.additionalFamilyName,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return names;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
static getPatientAttributes(isNewPatient: boolean, values: FormValues, patientUuidMap: PatientUuidMapType) {
|
|
341
|
+
const attributes: Array<AttributeValue> = [];
|
|
342
|
+
if (values.attributes) {
|
|
343
|
+
Object.entries(values.attributes)
|
|
344
|
+
.filter(([, value]) => !!value)
|
|
345
|
+
.forEach(([key, value]) => {
|
|
346
|
+
attributes.push({
|
|
347
|
+
attributeType: key,
|
|
348
|
+
value,
|
|
349
|
+
});
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
if (!isNewPatient && values.patientUuid) {
|
|
353
|
+
Object.entries(values.attributes)
|
|
354
|
+
.filter(([, value]) => !value)
|
|
355
|
+
.forEach(async ([key]) => {
|
|
356
|
+
const attributeUuid = patientUuidMap[`attribute.${key}`];
|
|
357
|
+
await openmrsFetch(`/ws/rest/v1/person/${values.patientUuid}/attribute/${attributeUuid}`, {
|
|
358
|
+
method: 'DELETE',
|
|
359
|
+
}).catch((err) => {
|
|
360
|
+
console.error(err);
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return attributes;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
static getPatientDeathInfo(values: FormValues) {
|
|
370
|
+
const { isDead, deathDate, deathCause } = values;
|
|
371
|
+
return {
|
|
372
|
+
dead: isDead,
|
|
373
|
+
deathDate: isDead ? deathDate : undefined,
|
|
374
|
+
causeOfDeath: isDead ? deathCause : undefined,
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
static mapPatientToFhirPatient(patient: Partial<Patient>): fhir.Patient {
|
|
379
|
+
// Important:
|
|
380
|
+
// When changing this code, ideally assume that `patient` can be missing any attribute.
|
|
381
|
+
// The `fhir.Patient` provides us with the benefit that all properties are nullable and thus
|
|
382
|
+
// not required (technically, at least). -> Even if we cannot map some props here, we still
|
|
383
|
+
// provide a valid fhir.Patient object. The various patient chart modules should be able to handle
|
|
384
|
+
// such missing props correctly (and should be updated if they don't).
|
|
385
|
+
|
|
386
|
+
// Mapping inspired by:
|
|
387
|
+
// https://github.com/openmrs/openmrs-module-fhir/blob/669b3c52220bb9abc622f815f4dc0d8523687a57/api/src/main/java/org/openmrs/module/fhir/api/util/FHIRPatientUtil.java#L36
|
|
388
|
+
// https://github.com/openmrs/openmrs-esm-patient-management/blob/94e6f637fb37cf4984163c355c5981ea6b8ca38c/packages/esm-patient-search-app/src/patient-search-result/patient-search-result.component.tsx#L21
|
|
389
|
+
// Update as required.
|
|
390
|
+
return {
|
|
391
|
+
id: patient.uuid,
|
|
392
|
+
gender: patient.person?.gender,
|
|
393
|
+
birthDate: patient.person?.birthdate,
|
|
394
|
+
deceasedBoolean: patient.person.dead,
|
|
395
|
+
deceasedDateTime: patient.person.deathDate,
|
|
396
|
+
name: patient.person?.names?.map((name) => ({
|
|
397
|
+
given: [name.givenName, name.middleName].filter(Boolean),
|
|
398
|
+
family: name.familyName,
|
|
399
|
+
})),
|
|
400
|
+
address: patient.person?.addresses.map((address) => ({
|
|
401
|
+
city: address.cityVillage,
|
|
402
|
+
country: address.country,
|
|
403
|
+
postalCode: address.postalCode,
|
|
404
|
+
state: address.stateProvince,
|
|
405
|
+
use: 'home',
|
|
406
|
+
})),
|
|
407
|
+
telecom: patient.person.attributes?.filter((attribute) => attribute.attributeType === 'Telephone Number'),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export class SavePatientTransactionManager {
|
|
413
|
+
patientSaved = false;
|
|
414
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Layer, TextInput } from '@carbon/react';
|
|
4
|
+
import { useField } from 'formik';
|
|
5
|
+
|
|
6
|
+
// FIXME Temporarily imported here
|
|
7
|
+
export interface TextInputProps
|
|
8
|
+
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'defaultValue' | 'id' | 'size' | 'value'> {
|
|
9
|
+
/**
|
|
10
|
+
* Specify an optional className to be applied to the `<input>` node
|
|
11
|
+
*/
|
|
12
|
+
className?: string;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Optionally provide the default value of the `<input>`
|
|
16
|
+
*/
|
|
17
|
+
defaultValue?: string | number;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Specify whether the `<input>` should be disabled
|
|
21
|
+
*/
|
|
22
|
+
disabled?: boolean;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Specify whether to display the character counter
|
|
26
|
+
*/
|
|
27
|
+
enableCounter?: boolean;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Provide text that is used alongside the control label for additional help
|
|
31
|
+
*/
|
|
32
|
+
helperText?: React.ReactNode;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Specify whether you want the underlying label to be visually hidden
|
|
36
|
+
*/
|
|
37
|
+
hideLabel?: boolean;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Specify a custom `id` for the `<input>`
|
|
41
|
+
*/
|
|
42
|
+
id: string;
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `true` to use the inline version.
|
|
46
|
+
*/
|
|
47
|
+
inline?: boolean;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Specify whether the control is currently invalid
|
|
51
|
+
*/
|
|
52
|
+
invalid?: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Provide the text that is displayed when the control is in an invalid state
|
|
56
|
+
*/
|
|
57
|
+
invalidText?: React.ReactNode;
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Provide the text that will be read by a screen reader when visiting this
|
|
61
|
+
* control
|
|
62
|
+
*/
|
|
63
|
+
labelText: React.ReactNode;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `true` to use the light version. For use on $ui-01 backgrounds only.
|
|
67
|
+
* Don't use this to make tile background color same as container background color.
|
|
68
|
+
* 'The `light` prop for `TextInput` has ' +
|
|
69
|
+
'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'
|
|
70
|
+
*/
|
|
71
|
+
light?: boolean;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Max character count allowed for the input. This is needed in order for enableCounter to display
|
|
75
|
+
*/
|
|
76
|
+
maxCount?: number;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Optionally provide an `onChange` handler that is called whenever `<input>`
|
|
80
|
+
* is updated
|
|
81
|
+
*/
|
|
82
|
+
onChange?: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Optionally provide an `onClick` handler that is called whenever the
|
|
86
|
+
* `<input>` is clicked
|
|
87
|
+
*/
|
|
88
|
+
onClick?: (event: React.MouseEvent<HTMLElement>) => void;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Specify the placeholder attribute for the `<input>`
|
|
92
|
+
*/
|
|
93
|
+
placeholder?: string;
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Whether the input should be read-only
|
|
97
|
+
*/
|
|
98
|
+
readOnly?: boolean;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Specify the size of the Text Input. Currently supports the following:
|
|
102
|
+
*/
|
|
103
|
+
size?: 'sm' | 'md' | 'lg' | 'xl';
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Specify the type of the `<input>`
|
|
107
|
+
*/
|
|
108
|
+
type?: string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Specify the value of the `<input>`
|
|
112
|
+
*/
|
|
113
|
+
value?: string | number | undefined;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Specify whether the control is currently in warning state
|
|
117
|
+
*/
|
|
118
|
+
warn?: boolean;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Provide the text that is displayed when the control is in warning state
|
|
122
|
+
*/
|
|
123
|
+
warnText?: React.ReactNode;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
interface InputProps extends TextInputProps {
|
|
127
|
+
checkWarning?(value: string): string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export const Input: React.FC<InputProps> = ({ checkWarning, ...props }) => {
|
|
131
|
+
const [field, meta] = useField(props.name);
|
|
132
|
+
const { t } = useTranslation();
|
|
133
|
+
|
|
134
|
+
/*
|
|
135
|
+
Do not remove these comments
|
|
136
|
+
t('givenNameRequired')
|
|
137
|
+
t('familyNameRequired')
|
|
138
|
+
t('genderUnspecified')
|
|
139
|
+
t('genderRequired')
|
|
140
|
+
t('birthdayRequired')
|
|
141
|
+
t('birthdayNotInTheFuture')
|
|
142
|
+
t('negativeYears')
|
|
143
|
+
t('negativeMonths')
|
|
144
|
+
t('deathdayNotInTheFuture')
|
|
145
|
+
t('invalidEmail')
|
|
146
|
+
t('numberInNameDubious')
|
|
147
|
+
t('yearsEstimateRequired')
|
|
148
|
+
*/
|
|
149
|
+
|
|
150
|
+
const value = field.value || '';
|
|
151
|
+
const invalidText = meta.error && t(meta.error);
|
|
152
|
+
const warnText = useMemo(() => {
|
|
153
|
+
if (!invalidText && typeof checkWarning === 'function') {
|
|
154
|
+
const warning = checkWarning(value);
|
|
155
|
+
return warning && t(warning);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
return undefined;
|
|
159
|
+
}, [checkWarning, invalidText, value, t]);
|
|
160
|
+
|
|
161
|
+
const labelText = props.required ? props.labelText : `${props.labelText} (${t('optional', 'optional')})`;
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<div style={{ marginBottom: '1rem' }}>
|
|
165
|
+
<Layer>
|
|
166
|
+
<TextInput
|
|
167
|
+
{...props}
|
|
168
|
+
{...field}
|
|
169
|
+
labelText={labelText}
|
|
170
|
+
invalid={!!(meta.touched && meta.error)}
|
|
171
|
+
invalidText={invalidText}
|
|
172
|
+
warn={!!warnText}
|
|
173
|
+
warnText={warnText}
|
|
174
|
+
value={value}
|
|
175
|
+
/>
|
|
176
|
+
</Layer>
|
|
177
|
+
</div>
|
|
178
|
+
);
|
|
179
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { Formik, Form } from 'formik';
|
|
5
|
+
import { Input } from './input.component';
|
|
6
|
+
|
|
7
|
+
describe('text input', () => {
|
|
8
|
+
const setupInput = async () => {
|
|
9
|
+
render(
|
|
10
|
+
<Formik initialValues={{ text: '' }} onSubmit={() => {}}>
|
|
11
|
+
<Form>
|
|
12
|
+
<Input
|
|
13
|
+
id="text"
|
|
14
|
+
labelText="Text"
|
|
15
|
+
name="text"
|
|
16
|
+
placeholder="Enter text"
|
|
17
|
+
required
|
|
18
|
+
checkWarning={(value) => {
|
|
19
|
+
if (value.length > 5) {
|
|
20
|
+
return 'name should be of 5 char';
|
|
21
|
+
}
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
</Form>
|
|
25
|
+
</Formik>,
|
|
26
|
+
);
|
|
27
|
+
return screen.getByLabelText('Text') as HTMLInputElement;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
it('exists', async () => {
|
|
31
|
+
const input = await setupInput();
|
|
32
|
+
expect(input.type).toEqual('text');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('can input valid data without warning', async () => {
|
|
36
|
+
const user = userEvent.setup();
|
|
37
|
+
|
|
38
|
+
const input = await setupInput();
|
|
39
|
+
const userInput = 'text';
|
|
40
|
+
|
|
41
|
+
await user.type(input, userInput);
|
|
42
|
+
await user.tab();
|
|
43
|
+
|
|
44
|
+
expect(input.value).toEqual(userInput);
|
|
45
|
+
expect(screen.queryByText('name should be of 5 char')).not.toBeInTheDocument();
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should show a warning when the invalid input is entered', async () => {
|
|
49
|
+
const user = userEvent.setup();
|
|
50
|
+
|
|
51
|
+
const input = await setupInput();
|
|
52
|
+
const userInput = 'Hello World';
|
|
53
|
+
|
|
54
|
+
await userEvent.clear(input);
|
|
55
|
+
|
|
56
|
+
await user.type(input, userInput);
|
|
57
|
+
await user.tab();
|
|
58
|
+
|
|
59
|
+
expect(screen.getByText('name should be of 5 char')).toBeInTheDocument();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should show the correct label text if the field is not required', () => {
|
|
63
|
+
render(
|
|
64
|
+
<Formik initialValues={{ text: '' }} onSubmit={() => {}}>
|
|
65
|
+
<Form>
|
|
66
|
+
<Input id="text" labelText="Text" name="text" placeholder="Enter text" />
|
|
67
|
+
</Form>
|
|
68
|
+
</Formik>,
|
|
69
|
+
);
|
|
70
|
+
expect(screen.getByLabelText('Text (optional)')).toBeInTheDocument();
|
|
71
|
+
});
|
|
72
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Layer, Select, SelectItem } from '@carbon/react';
|
|
3
|
+
import { useField } from 'formik';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
interface SelectInputProps {
|
|
7
|
+
name: string;
|
|
8
|
+
options: Array<string>;
|
|
9
|
+
label: string;
|
|
10
|
+
required?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const SelectInput: React.FC<SelectInputProps> = ({ name, options, label, required }) => {
|
|
14
|
+
const [field] = useField(name);
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const selectOptions = [
|
|
17
|
+
<SelectItem disabled hidden text={`Select ${label}`} key="" value="" />,
|
|
18
|
+
...options.map((currentOption, index) => <SelectItem text={currentOption} value={currentOption} key={index} />),
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
const labelText = required ? label : `${label} (${t('optional', 'optional')})`;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<div style={{ marginBottom: '1rem' }}>
|
|
25
|
+
<Layer>
|
|
26
|
+
<Select id="identifier" {...field} labelText={labelText}>
|
|
27
|
+
{selectOptions}
|
|
28
|
+
</Select>
|
|
29
|
+
</Layer>
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
};
|