@digihmis/esm-patient-registration-app 1.0.0

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 (146) hide show
  1. package/README.md +4 -0
  2. package/dist/117.js +1 -0
  3. package/dist/117.js.map +1 -0
  4. package/dist/131.js +1 -0
  5. package/dist/131.js.map +1 -0
  6. package/dist/132.js +1 -0
  7. package/dist/132.js.map +1 -0
  8. package/dist/152.js +1 -0
  9. package/dist/152.js.map +1 -0
  10. package/dist/209.js +1 -0
  11. package/dist/209.js.map +1 -0
  12. package/dist/247.js +1 -0
  13. package/dist/247.js.map +1 -0
  14. package/dist/28.js +1 -0
  15. package/dist/28.js.map +1 -0
  16. package/dist/317.js +1 -0
  17. package/dist/317.js.map +1 -0
  18. package/dist/349.js +1 -0
  19. package/dist/349.js.map +1 -0
  20. package/dist/371.js +1 -0
  21. package/dist/371.js.map +1 -0
  22. package/dist/378.js +1 -0
  23. package/dist/378.js.map +1 -0
  24. package/dist/442.js +1 -0
  25. package/dist/442.js.map +1 -0
  26. package/dist/45.js +1 -0
  27. package/dist/45.js.map +1 -0
  28. package/dist/459.js +12 -0
  29. package/dist/459.js.map +1 -0
  30. package/dist/466.js +1 -0
  31. package/dist/466.js.map +1 -0
  32. package/dist/508.js +1 -0
  33. package/dist/508.js.map +1 -0
  34. package/dist/61.js +1 -0
  35. package/dist/61.js.map +1 -0
  36. package/dist/640.js +1 -0
  37. package/dist/640.js.map +1 -0
  38. package/dist/689.js +1 -0
  39. package/dist/689.js.map +1 -0
  40. package/dist/709.js +1 -0
  41. package/dist/709.js.map +1 -0
  42. package/dist/712.js +1 -0
  43. package/dist/712.js.map +1 -0
  44. package/dist/720.js +1 -0
  45. package/dist/720.js.map +1 -0
  46. package/dist/742.js +1 -0
  47. package/dist/742.js.map +1 -0
  48. package/dist/771.js +1 -0
  49. package/dist/771.js.map +1 -0
  50. package/dist/806.js +1 -0
  51. package/dist/806.js.map +1 -0
  52. package/dist/912.js +1 -0
  53. package/dist/912.js.map +1 -0
  54. package/dist/913.js +1 -0
  55. package/dist/913.js.map +1 -0
  56. package/dist/940.js +1 -0
  57. package/dist/940.js.map +1 -0
  58. package/dist/957.js +15 -0
  59. package/dist/957.js.map +1 -0
  60. package/dist/digihmis-esm-patient-registration-app.js +6 -0
  61. package/dist/digihmis-esm-patient-registration-app.js.buildmanifest.json +827 -0
  62. package/dist/digihmis-esm-patient-registration-app.js.map +1 -0
  63. package/dist/main.js +17 -0
  64. package/dist/main.js.map +1 -0
  65. package/dist/routes.json +1 -0
  66. package/jest.config.js +26 -0
  67. package/package.json +51 -0
  68. package/rspack.config.js +1 -0
  69. package/src/active-visit-table/active-visit-table.component.tsx +240 -0
  70. package/src/active-visit-table/active-visits.resource.ts +338 -0
  71. package/src/active-visit-table/active-visits.scss +211 -0
  72. package/src/active-visit-table/empty-data-illustration.component.tsx +39 -0
  73. package/src/checkin-form/base-visit-type.component.tsx +102 -0
  74. package/src/checkin-form/base-visit-type.scss +75 -0
  75. package/src/checkin-form/checkin-form.scss +167 -0
  76. package/src/checkin-form/checkin-form.workspace.tsx +703 -0
  77. package/src/checkin-form/hooks/useDefaultFacilityLocation.ts +15 -0
  78. package/src/checkin-form/hooks/useDefaultVisitLocation.ts +24 -0
  79. package/src/checkin-form/hooks/useOfflineVisitType.ts +18 -0
  80. package/src/checkin-form/hooks/usePatientHistory.ts +40 -0
  81. package/src/checkin-form/hooks/useRecommendedVisitTypes.ts +34 -0
  82. package/src/checkin-form/hooks/useVisitAttributeType.ts +102 -0
  83. package/src/checkin-form/location-selector.component.tsx +86 -0
  84. package/src/checkin-form/recommended-visit-type.component.tsx +32 -0
  85. package/src/checkin-form/visit-attribute-type.component.tsx +256 -0
  86. package/src/checkin-form/visit-attribute-type.scss +5 -0
  87. package/src/checkin-form/visit-date-time.component.tsx +201 -0
  88. package/src/checkin-form/visit-form.resource.ts +308 -0
  89. package/src/components/custom-card/compact-patient-banner.component.tsx +70 -0
  90. package/src/components/custom-card/compact-patient-banner.scss +34 -0
  91. package/src/components/custom-card/custom-loader.component.tsx +22 -0
  92. package/src/components/custom-card/patient-search-context.ts +30 -0
  93. package/src/components/custom-input/autosuggest/autosuggest.component.tsx +213 -0
  94. package/src/components/custom-input/autosuggest/autosuggest.scss +158 -0
  95. package/src/components/custom-input/autosuggest/autosuggest.test.tsx +168 -0
  96. package/src/components/custom-patient-banner/custom-patient-banner.component.tsx +39 -0
  97. package/src/components/custom-patient-banner/custom-patient-banner.resource.ts +32 -0
  98. package/src/components/custom-patient-banner/custom-patient-banner.scss +26 -0
  99. package/src/config-schema.ts +511 -0
  100. package/src/constant.ts +6 -0
  101. package/src/constants/index.ts +1 -0
  102. package/src/dashboard.meta.ts +8 -0
  103. package/src/declarations.d.ts +6 -0
  104. package/src/empty-svg/empty-svg.component.tsx +565 -0
  105. package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
  106. package/src/identifier-workspace/identifier-configure-form.workspace.tsx +278 -0
  107. package/src/identifier-workspace/identifier-configure.scss +106 -0
  108. package/src/identifier-workspace/identifier.resource.ts +90 -0
  109. package/src/index.ts +33 -0
  110. package/src/left-panel/left-panel.component.tsx +54 -0
  111. package/src/left-panel/left-panel.scss +13 -0
  112. package/src/page-header/page-header.component.tsx +35 -0
  113. package/src/page-header/page-header.scss +76 -0
  114. package/src/patient-tags/deceased-patient-tag.extension.tsx +41 -0
  115. package/src/patient-tags/deceased-patient-tag.scss +15 -0
  116. package/src/patient-tags/patient-tags.resource.ts +30 -0
  117. package/src/patient-tags/visit-tag.extension.tsx +43 -0
  118. package/src/patient-tags/visit-tag.scss +19 -0
  119. package/src/registration-form/form-manager.ts +449 -0
  120. package/src/registration-form/registration-form.component.tsx +1565 -0
  121. package/src/registration-form/registration-form.resource.ts +264 -0
  122. package/src/registration-form/registration-form.scss +282 -0
  123. package/src/routes.json +62 -0
  124. package/src/search-bar/search-bar.component.tsx +285 -0
  125. package/src/search-bar/search-bar.resource.ts +274 -0
  126. package/src/search-bar/search-bar.scss +427 -0
  127. package/src/search-dashboard/search-dashboard.component.tsx +17 -0
  128. package/src/search-dashboard/search-dashboard.scss +13 -0
  129. package/src/setup-tests.ts +1 -0
  130. package/src/type/index.ts +463 -0
  131. package/src/utils/fhir-mapper.ts +73 -0
  132. package/src/visit-summary/visit-summary.component.tsx +32 -0
  133. package/src/visit-summary/visit-summary.resource.ts +56 -0
  134. package/src/visit-summary/visit-summary.scss +74 -0
  135. package/translations/am.json +112 -0
  136. package/translations/ar.json +120 -0
  137. package/translations/en.json +112 -0
  138. package/translations/es.json +114 -0
  139. package/translations/fr.json +114 -0
  140. package/translations/he.json +114 -0
  141. package/translations/km.json +111 -0
  142. package/translations/pt.json +114 -0
  143. package/translations/sw.json +112 -0
  144. package/translations/vi.json +111 -0
  145. package/translations/zh.json +111 -0
  146. package/tsconfig.json +5 -0
@@ -0,0 +1,41 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { DefinitionTooltip, Tag } from '@carbon/react';
4
+ import { formatDatetime, parseDate } from '@openmrs/esm-framework';
5
+ import styles from './deceased-patient-tag.scss';
6
+ import { useCauseOfDeath } from './patient-tags.resource';
7
+
8
+ interface DeceasedPatientBannerTagProps {
9
+ patient: fhir.Patient;
10
+ }
11
+
12
+ const DeceasedPatientBannerTag: React.FC<DeceasedPatientBannerTagProps> = ({ patient }) => {
13
+ const { t } = useTranslation();
14
+ const isDeceased = Boolean(patient?.deceasedDateTime);
15
+ const { causeOfDeath, nonCodedCauseOfDeath } = useCauseOfDeath(patient?.id);
16
+
17
+ if (!isDeceased) {
18
+ return null;
19
+ }
20
+
21
+ return (
22
+ <DefinitionTooltip
23
+ align="bottom-left"
24
+ definition={
25
+ <div role="tooltip" className={styles.tooltipPadding}>
26
+ <h6 style={{ marginBottom: '0.5rem' }}>{t('deceased', 'Deceased')}</h6>
27
+ <span>
28
+ {formatDatetime(parseDate(patient?.deceasedDateTime))}
29
+ {nonCodedCauseOfDeath || causeOfDeath
30
+ ? ` ${t('from_lower', 'from')} ${nonCodedCauseOfDeath || causeOfDeath}`
31
+ : null}
32
+ </span>
33
+ </div>
34
+ }>
35
+ <Tag className={styles.tagOverride}>{t('deceased', 'Deceased')}</Tag>
36
+ </DefinitionTooltip>
37
+ );
38
+ };
39
+
40
+ export { DeceasedPatientBannerTagProps };
41
+ export default DeceasedPatientBannerTag;
@@ -0,0 +1,15 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .tooltipPadding {
5
+ padding: layout.$spacing-02;
6
+ }
7
+
8
+ .tooltipSmallText {
9
+ font-size: 80%;
10
+ }
11
+
12
+ .tagOverride {
13
+ background-color: colors.$gray-80;
14
+ color: colors.$gray-20;
15
+ }
@@ -0,0 +1,30 @@
1
+ import { useMemo } from 'react';
2
+ import useSWR from 'swr';
3
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
4
+
5
+ interface CauseOfDeathResponse {
6
+ data: {
7
+ causeOfDeath: {
8
+ display: string;
9
+ };
10
+ causeOfDeathNonCoded?: string;
11
+ };
12
+ }
13
+
14
+ export function useCauseOfDeath(patientUuid: string) {
15
+ const customRepresentation = 'custom:(causeOfDeath:(display),causeOfDeathNonCoded)';
16
+ const url = `${restBaseUrl}/person/${patientUuid}?v=${customRepresentation}`;
17
+
18
+ const { data, error } = useSWR<CauseOfDeathResponse, Error>(patientUuid ? url : null, openmrsFetch);
19
+
20
+ const results = useMemo(
21
+ () => ({
22
+ causeOfDeath: data?.data?.causeOfDeath?.display,
23
+ nonCodedCauseOfDeath: data?.data?.causeOfDeathNonCoded,
24
+ error,
25
+ }),
26
+ [data?.data?.causeOfDeath?.display, data?.data?.causeOfDeathNonCoded, error],
27
+ );
28
+
29
+ return results;
30
+ }
@@ -0,0 +1,43 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Tag, Toggletip, ToggletipButton, ToggletipContent } from '@carbon/react';
4
+ import { type Visit, formatDatetime, parseDate, useVisit } from '@openmrs/esm-framework';
5
+ import styles from './visit-tag.scss';
6
+
7
+ interface VisitTagProps {
8
+ patientUuid: string;
9
+ patient: fhir.Patient;
10
+ }
11
+
12
+ function VisitTag({ patientUuid, patient }: VisitTagProps) {
13
+ const { activeVisit, isLoading } = useVisit(patientUuid);
14
+ const isNotDeceased = !patient?.deceasedDateTime;
15
+ return !isLoading && activeVisit && isNotDeceased ? <ActiveVisitTag activeVisit={activeVisit} /> : null;
16
+ }
17
+
18
+ interface ActiveVisitTagProps {
19
+ activeVisit: Visit;
20
+ }
21
+
22
+ const ActiveVisitTag: React.FC<ActiveVisitTagProps> = ({ activeVisit }) => {
23
+ const { t } = useTranslation();
24
+ return (
25
+ <Toggletip align="bottom">
26
+ <ToggletipButton label={t('activeVisit', 'Active Visit')}>
27
+ <Tag type="blue">{t('activeVisit', 'Active Visit')}</Tag>
28
+ </ToggletipButton>
29
+ <ToggletipContent>
30
+ <div role="tooltip">
31
+ <h6 className={styles.heading}>{activeVisit?.visitType?.display}</h6>
32
+ <span>
33
+ <span className={styles.tooltipSmallText}>{t('started', 'Started')}: </span>
34
+ <span>{formatDatetime(parseDate(activeVisit?.startDatetime), { mode: 'wide' })}</span>
35
+ </span>
36
+ </div>
37
+ </ToggletipContent>
38
+ </Toggletip>
39
+ );
40
+ };
41
+
42
+ export { VisitTagProps };
43
+ export default VisitTag;
@@ -0,0 +1,19 @@
1
+ @use '@carbon/layout';
2
+
3
+ .tooltipBox {
4
+ padding: layout.$spacing-02;
5
+ }
6
+
7
+ .tooltipSmallText {
8
+ font-size: 80%;
9
+ }
10
+
11
+ .heading {
12
+ margin-bottom: layout.$spacing-03;
13
+ }
14
+
15
+ .definitionToolTip {
16
+ & > button {
17
+ border-bottom: none;
18
+ }
19
+ }
@@ -0,0 +1,449 @@
1
+ import {
2
+ type FetchResponse,
3
+ getConfig,
4
+ openmrsFetch,
5
+ queueSynchronizationItem,
6
+ restBaseUrl,
7
+ type Session,
8
+ showSnackbar,
9
+ type StyleguideConfigObject,
10
+ toOmrsIsoString,
11
+ } from '@openmrs/esm-framework';
12
+
13
+ import { type RegistrationConfig } from '../config-schema';
14
+ import {
15
+ addPatientIdentifier,
16
+ deletePatientIdentifier,
17
+ deletePersonName,
18
+ deleteRelationship,
19
+ generateIdentifier,
20
+ getDatetime,
21
+ saveEncounter,
22
+ savePatient,
23
+ savePatientPhoto,
24
+ saveRelationship,
25
+ updatePatientIdentifier,
26
+ updateRelationship,
27
+ } from './registration-form.resource';
28
+ import {
29
+ AttributeValue,
30
+ CapturePhotoProps,
31
+ Encounter,
32
+ FormValues,
33
+ Patient,
34
+ PatientIdentifier,
35
+ PatientRegistration,
36
+ PatientUuidMapType,
37
+ RelationshipValue,
38
+ } from '../type';
39
+ import { patientRegistration } from '../constant';
40
+
41
+ export type SavePatientForm = (
42
+ isNewPatient: boolean,
43
+ values: FormValues,
44
+ patientUuidMap: PatientUuidMapType,
45
+ initialAddressFieldValues: Record<string, any>,
46
+ capturePhotoProps: CapturePhotoProps,
47
+ currentLocation: string,
48
+ initialIdentifierValues: FormValues['identifiers'],
49
+ currentUser: Session,
50
+ config: RegistrationConfig,
51
+ savePatientTransactionManager: SavePatientTransactionManager,
52
+ abortController?: AbortController,
53
+ ) => Promise<string | void>;
54
+
55
+ export class FormManager {
56
+ static savePatientFormOffline: SavePatientForm = async (
57
+ isNewPatient,
58
+ values,
59
+ patientUuidMap,
60
+ initialAddressFieldValues,
61
+ capturePhotoProps,
62
+ currentLocation,
63
+ initialIdentifierValues,
64
+ currentUser,
65
+ config,
66
+ ) => {
67
+ const syncItem: PatientRegistration = {
68
+ fhirPatient: FormManager.mapPatientToFhirPatient(
69
+ FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, [], config),
70
+ ),
71
+ _patientRegistrationData: {
72
+ isNewPatient,
73
+ formValues: values,
74
+ patientUuidMap,
75
+ initialAddressFieldValues,
76
+ capturePhotoProps,
77
+ currentLocation,
78
+ initialIdentifierValues,
79
+ currentUser,
80
+ config,
81
+ savePatientTransactionManager: new SavePatientTransactionManager(),
82
+ },
83
+ };
84
+
85
+ await queueSynchronizationItem(patientRegistration, syncItem, {
86
+ id: values.patientUuid,
87
+ displayName: 'Patient registration',
88
+ patientUuid: syncItem.fhirPatient.id,
89
+ dependencies: [],
90
+ });
91
+
92
+ return null;
93
+ };
94
+
95
+ static savePatientFormOnline: SavePatientForm = async (
96
+ isNewPatient,
97
+ values,
98
+ patientUuidMap,
99
+ initialAddressFieldValues,
100
+ capturePhotoProps,
101
+ currentLocation,
102
+ initialIdentifierValues,
103
+ currentUser,
104
+ config,
105
+ savePatientTransactionManager,
106
+ abortController,
107
+ ) => {
108
+ const patientIdentifiers: Array<PatientIdentifier> = await FormManager.savePatientIdentifiers(
109
+ isNewPatient,
110
+ values.patientUuid,
111
+ values.identifiers,
112
+ initialIdentifierValues,
113
+ currentLocation,
114
+ );
115
+
116
+ const createdPatient = FormManager.getPatientToCreate(
117
+ isNewPatient,
118
+ values,
119
+ patientUuidMap,
120
+ initialAddressFieldValues,
121
+ patientIdentifiers,
122
+ config,
123
+ );
124
+
125
+ FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
126
+ await deletePersonName(name.nameUuid, name.personUuid);
127
+ });
128
+
129
+ const savePatientResponse = await savePatient(
130
+ createdPatient,
131
+ isNewPatient && !savePatientTransactionManager.patientSaved ? undefined : values.patientUuid,
132
+ );
133
+
134
+ if (savePatientResponse.ok) {
135
+ savePatientTransactionManager.patientSaved = true;
136
+
137
+ // Save relationships
138
+ await this.saveRelationships(values.relationships, savePatientResponse);
139
+
140
+ // Save observations and create encounter
141
+ await this.saveObservations(values.obs, savePatientResponse, currentLocation, currentUser, config);
142
+
143
+ // Save patient photo if available
144
+ const { patientPhotoConceptUuid } = await getConfig<StyleguideConfigObject>('@openmrs/esm-styleguide');
145
+
146
+ if (patientPhotoConceptUuid && capturePhotoProps?.imageData) {
147
+ await savePatientPhoto(
148
+ savePatientResponse.data.uuid,
149
+ capturePhotoProps.imageData,
150
+ `${restBaseUrl}/obs`,
151
+ capturePhotoProps.dateTime || new Date().toISOString(),
152
+ patientPhotoConceptUuid,
153
+ );
154
+ }
155
+ }
156
+
157
+ return savePatientResponse.data.uuid;
158
+ };
159
+
160
+ static async saveRelationships(relationships: Array<RelationshipValue>, savePatientResponse: FetchResponse) {
161
+ return Promise.all(
162
+ relationships
163
+ .filter((m) => m.relationshipType)
164
+ .filter((relationship) => !!relationship.action)
165
+ .map(({ relatedPersonUuid, relationshipType, uuid: relationshipUuid, action }) => {
166
+ const [type, direction] = relationshipType.split('/');
167
+ const thisPatientUuid = savePatientResponse.data.uuid;
168
+ const isAToB = direction === 'aIsToB';
169
+ const relationshipToSave = {
170
+ personA: isAToB ? relatedPersonUuid : thisPatientUuid,
171
+ personB: isAToB ? thisPatientUuid : relatedPersonUuid,
172
+ relationshipType: type,
173
+ };
174
+
175
+ switch (action) {
176
+ case 'ADD':
177
+ return saveRelationship(relationshipToSave);
178
+ case 'UPDATE':
179
+ return updateRelationship(relationshipUuid, relationshipToSave);
180
+ case 'DELETE':
181
+ return deleteRelationship(relationshipUuid);
182
+ }
183
+ }),
184
+ );
185
+ }
186
+
187
+ static async saveObservations(
188
+ obss: { [conceptUuid: string]: string },
189
+ savePatientResponse: FetchResponse,
190
+ currentLocation: string,
191
+ currentUser: Session,
192
+ config: RegistrationConfig,
193
+ ) {
194
+ if (!config.registrationObs?.encounterTypeUuid) {
195
+ return;
196
+ }
197
+
198
+ const encounterToSave: Encounter = {
199
+ patient: savePatientResponse.data.uuid,
200
+ encounterType: config.registrationObs.encounterTypeUuid,
201
+ location: currentLocation,
202
+ encounterProviders: [
203
+ {
204
+ provider: currentUser.currentProvider.uuid,
205
+ encounterRole: config.registrationObs.encounterProviderRoleUuid,
206
+ },
207
+ ],
208
+ form: config.registrationObs.registrationFormUuid,
209
+ obs:
210
+ obss && Object.keys(obss).length > 0
211
+ ? Object.entries(obss)
212
+ .filter(([, value]) => value !== '')
213
+ .map(([conceptUuid, value]) => ({ concept: conceptUuid, value }))
214
+ : [],
215
+ };
216
+
217
+ return saveEncounter(encounterToSave);
218
+ }
219
+
220
+ static async savePatientIdentifiers(
221
+ isNewPatient: boolean,
222
+ patientUuid: string,
223
+ patientIdentifiers: FormValues['identifiers'], // values.identifiers
224
+ initialIdentifierValues: FormValues['identifiers'], // Initial identifiers assigned to the patient
225
+ location: string,
226
+ ): Promise<Array<PatientIdentifier>> {
227
+ let identifierTypeRequests = Object.values(patientIdentifiers)
228
+ /* Since default identifier-types will be present on the form and are also in the not-required state,
229
+ therefore we might be running into situations when there's no value and no source associated,
230
+ hence filtering these fields out.
231
+ */
232
+ .filter(
233
+ ({ identifierValue, autoGeneration, selectedSource }) => identifierValue || (autoGeneration && selectedSource),
234
+ )
235
+ .map(async (patientIdentifier) => {
236
+ const {
237
+ identifierTypeUuid,
238
+ identifierValue,
239
+ identifierUuid,
240
+ selectedSource,
241
+ preferred,
242
+ autoGeneration,
243
+ initialValue,
244
+ } = patientIdentifier;
245
+
246
+ const autoGenerationManualEntry =
247
+ autoGeneration && selectedSource?.autoGenerationOption?.manualEntryEnabled && !!identifierValue;
248
+
249
+ const identifier =
250
+ !autoGeneration || autoGenerationManualEntry
251
+ ? identifierValue
252
+ : await (
253
+ await generateIdentifier(selectedSource.uuid)
254
+ ).data.identifier;
255
+
256
+ const identifierToCreate = {
257
+ uuid: identifierUuid,
258
+ identifier,
259
+ identifierType: identifierTypeUuid,
260
+ location,
261
+ preferred,
262
+ };
263
+
264
+ if (!isNewPatient) {
265
+ if (!initialValue) {
266
+ await addPatientIdentifier(patientUuid, identifierToCreate);
267
+ } else if (initialValue !== identifier) {
268
+ await updatePatientIdentifier(patientUuid, identifierUuid, identifierToCreate.identifier);
269
+ }
270
+ }
271
+
272
+ return identifierToCreate;
273
+ });
274
+
275
+ /*
276
+ If there was initially an identifier assigned to the patient,
277
+ which is now not present in the patientIdentifiers(values.identifiers),
278
+ this means that the identifier is meant to be deleted, hence we need
279
+ to delete the respective identifiers.
280
+ */
281
+
282
+ if (patientUuid) {
283
+ Object.keys(initialIdentifierValues)
284
+ .filter((identifierFieldName) => !patientIdentifiers[identifierFieldName])
285
+ .forEach(async (identifierFieldName) => {
286
+ await deletePatientIdentifier(patientUuid, initialIdentifierValues[identifierFieldName].identifierUuid);
287
+ });
288
+ }
289
+
290
+ return Promise.all(identifierTypeRequests);
291
+ }
292
+
293
+ static getDeletedNames(patientUuid: string, patientUuidMap: PatientUuidMapType) {
294
+ if (patientUuidMap?.additionalNameUuid) {
295
+ return [
296
+ {
297
+ nameUuid: patientUuidMap.additionalNameUuid,
298
+ personUuid: patientUuid,
299
+ },
300
+ ];
301
+ }
302
+ return [];
303
+ }
304
+
305
+ static getPatientToCreate(
306
+ isNewPatient: boolean,
307
+ values: FormValues,
308
+ patientUuidMap: PatientUuidMapType,
309
+ initialAddressFieldValues: Record<string, any>,
310
+ identifiers: Array<PatientIdentifier>,
311
+ config?: RegistrationConfig,
312
+ ): Patient {
313
+ let birthdate;
314
+ if (values.birthdate instanceof Date) {
315
+ birthdate = [values.birthdate.getFullYear(), values.birthdate.getMonth() + 1, values.birthdate.getDate()].join(
316
+ '-',
317
+ );
318
+ } else {
319
+ birthdate = values.birthdate;
320
+ }
321
+
322
+ return {
323
+ uuid: values.patientUuid,
324
+ person: {
325
+ uuid: values.patientUuid,
326
+ names: FormManager.getNames(values, patientUuidMap),
327
+ gender: values.gender.charAt(0).toUpperCase(),
328
+ birthdate,
329
+ birthdateEstimated: values.birthdateEstimated,
330
+ attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
331
+ addresses: [values.address],
332
+ ...FormManager.getPatientDeathInfo(values, config),
333
+ },
334
+ identifiers,
335
+ };
336
+ }
337
+
338
+ static getNames(values: FormValues, patientUuidMap: PatientUuidMapType) {
339
+ const names = [
340
+ {
341
+ uuid: patientUuidMap.preferredNameUuid,
342
+ preferred: true,
343
+ givenName: values.givenName,
344
+ middleName: values.middleName,
345
+ familyName: values.familyName,
346
+ },
347
+ ];
348
+
349
+ if (values.addNameInLocalLanguage) {
350
+ names.push({
351
+ uuid: patientUuidMap.additionalNameUuid,
352
+ preferred: false,
353
+ givenName: values.additionalGivenName,
354
+ middleName: values.additionalMiddleName,
355
+ familyName: values.additionalFamilyName,
356
+ });
357
+ }
358
+
359
+ return names;
360
+ }
361
+
362
+ static getPatientAttributes(isNewPatient: boolean, values: FormValues, patientUuidMap: PatientUuidMapType) {
363
+ const attributes: Array<AttributeValue> = [];
364
+ if (values.attributes) {
365
+ Object.entries(values.attributes)
366
+ .filter(([, value]) => !!value)
367
+ .forEach(([key, value]) => {
368
+ attributes.push({
369
+ attributeType: key,
370
+ value,
371
+ });
372
+ });
373
+
374
+ if (!isNewPatient && values.patientUuid) {
375
+ Object.entries(values.attributes)
376
+ .filter(([, value]) => !value)
377
+ .forEach(async ([key]) => {
378
+ const attributeUuid = patientUuidMap[`attribute.${key}`];
379
+ await openmrsFetch(`${restBaseUrl}/person/${values.patientUuid}/attribute/${attributeUuid}`, {
380
+ method: 'DELETE',
381
+ }).catch((err) => {
382
+ showSnackbar({
383
+ kind: 'error',
384
+ title: `Error deleting patient attribute: ${err.message}`,
385
+ });
386
+ });
387
+ });
388
+ }
389
+ }
390
+
391
+ return attributes;
392
+ }
393
+
394
+ static getPatientDeathInfo(values: FormValues, config?: RegistrationConfig) {
395
+ const { isDead, deathDate, deathTime, deathTimeFormat, deathCause, nonCodedCauseOfDeath } = values;
396
+
397
+ if (!isDead) {
398
+ return {
399
+ dead: false,
400
+ };
401
+ }
402
+ const dateTimeOfDeath = toOmrsIsoString(getDatetime(deathDate, deathTime, deathTimeFormat));
403
+
404
+ return {
405
+ dead: true,
406
+ deathDate: dateTimeOfDeath,
407
+ ...(deathCause === config?.freeTextFieldConceptUuid
408
+ ? { causeOfDeathNonCoded: nonCodedCauseOfDeath, causeOfDeath: null }
409
+ : { causeOfDeath: deathCause, causeOfDeathNonCoded: null }),
410
+ };
411
+ }
412
+
413
+ static mapPatientToFhirPatient(patient: Partial<Patient>): fhir.Patient {
414
+ // Important:
415
+ // When changing this code, ideally assume that `patient` can be missing any attribute.
416
+ // The `fhir.Patient` provides us with the benefit that all properties are nullable and thus
417
+ // not required (technically, at least). -> Even if we cannot map some props here, we still
418
+ // provide a valid fhir.Patient object. The various patient chart modules should be able to handle
419
+ // such missing props correctly (and should be updated if they don't).
420
+
421
+ // Mapping inspired by:
422
+ // https://github.com/openmrs/openmrs-module-fhir/blob/669b3c52220bb9abc622f815f4dc0d8523687a57/api/src/main/java/org/openmrs/module/fhir/api/util/FHIRPatientUtil.java#L36
423
+ // 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
424
+ // Update as required.
425
+ return {
426
+ id: patient.uuid,
427
+ gender: patient.person?.gender,
428
+ birthDate: patient.person?.birthdate,
429
+ deceasedBoolean: patient.person.dead,
430
+ deceasedDateTime: patient.person.deathDate,
431
+ name: patient.person?.names?.map((name) => ({
432
+ given: [name.givenName, name.middleName].filter(Boolean),
433
+ family: name.familyName,
434
+ })),
435
+ address: patient.person?.addresses.map((address) => ({
436
+ city: address.cityVillage,
437
+ country: address.country,
438
+ postalCode: address.postalCode,
439
+ state: address.stateProvince,
440
+ use: 'home',
441
+ })),
442
+ telecom: patient.person.attributes?.filter((attribute) => attribute.attributeType === 'Telephone Number'),
443
+ };
444
+ }
445
+ }
446
+
447
+ export class SavePatientTransactionManager {
448
+ patientSaved = false;
449
+ }