@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,155 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import find from 'lodash-es/find';
|
|
3
|
+
import camelCase from 'lodash-es/camelCase';
|
|
4
|
+
import escapeRegExp from 'lodash-es/escapeRegExp';
|
|
5
|
+
import { getConfig, messageOmrsServiceWorker, openmrsFetch, type Session } from '@openmrs/esm-framework';
|
|
6
|
+
import type {
|
|
7
|
+
PatientIdentifierType,
|
|
8
|
+
FetchedPatientIdentifierType,
|
|
9
|
+
AddressTemplate,
|
|
10
|
+
} from './patient-registration/patient-registration.types';
|
|
11
|
+
import { cacheForOfflineHeaders, moduleName } from './constants';
|
|
12
|
+
|
|
13
|
+
export interface Resources {
|
|
14
|
+
addressTemplate: AddressTemplate;
|
|
15
|
+
currentSession: Session;
|
|
16
|
+
relationshipTypes: any;
|
|
17
|
+
identifierTypes: Array<PatientIdentifierType>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const ResourcesContext = React.createContext<Resources>(null);
|
|
21
|
+
|
|
22
|
+
export async function fetchCurrentSession(): Promise<Session> {
|
|
23
|
+
const { data } = await cacheAndFetch<Session>('/ws/rest/v1/session');
|
|
24
|
+
return data;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function fetchAddressTemplate() {
|
|
28
|
+
const { data } = await cacheAndFetch<AddressTemplate>('/ws/rest/v1/addresstemplate');
|
|
29
|
+
return data;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function fetchAllRelationshipTypes() {
|
|
33
|
+
const { data } = await cacheAndFetch('/ws/rest/v1/relationshiptype?v=default');
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export async function fetchAllFieldDefinitionTypes() {
|
|
38
|
+
const config = await getConfig(moduleName);
|
|
39
|
+
|
|
40
|
+
if (!config.fieldDefinitions) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const fieldDefinitionPromises = config.fieldDefinitions.map((def) => fetchFieldDefinitionType(def));
|
|
45
|
+
|
|
46
|
+
const fieldDefinitionResults = await Promise.all(fieldDefinitionPromises);
|
|
47
|
+
|
|
48
|
+
const mergedData = fieldDefinitionResults.reduce((merged, result) => {
|
|
49
|
+
if (result) {
|
|
50
|
+
merged.push(result);
|
|
51
|
+
}
|
|
52
|
+
return merged;
|
|
53
|
+
}, []);
|
|
54
|
+
|
|
55
|
+
return mergedData;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function fetchFieldDefinitionType(fieldDefinition) {
|
|
59
|
+
let apiUrl = '';
|
|
60
|
+
|
|
61
|
+
if (fieldDefinition.type === 'person attribute') {
|
|
62
|
+
apiUrl = `/ws/rest/v1/personattributetype/${fieldDefinition.uuid}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (fieldDefinition.answerConceptSetUuid) {
|
|
66
|
+
await cacheAndFetch(`/ws/rest/v1/concept/${fieldDefinition.answerConceptSetUuid}`);
|
|
67
|
+
}
|
|
68
|
+
const { data } = await cacheAndFetch(apiUrl);
|
|
69
|
+
return data;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function fetchPatientIdentifierTypesWithSources(): Promise<Array<PatientIdentifierType>> {
|
|
73
|
+
const patientIdentifierTypes = await fetchPatientIdentifierTypes();
|
|
74
|
+
|
|
75
|
+
// @ts-ignore Reason: The required props of the type are generated below.
|
|
76
|
+
const identifierTypes: Array<PatientIdentifierType> = patientIdentifierTypes.filter(Boolean);
|
|
77
|
+
|
|
78
|
+
const [autoGenOptions, ...allIdentifierSources] = await Promise.all([
|
|
79
|
+
fetchAutoGenerationOptions(),
|
|
80
|
+
...identifierTypes.map((identifierType) => fetchIdentifierSources(identifierType.uuid)),
|
|
81
|
+
]);
|
|
82
|
+
|
|
83
|
+
for (let i = 0; i < identifierTypes?.length; i++) {
|
|
84
|
+
identifierTypes[i].identifierSources = allIdentifierSources[i].data.results.map((source) => {
|
|
85
|
+
const option = find(autoGenOptions.data.results, { source: { uuid: source.uuid } });
|
|
86
|
+
source.autoGenerationOption = option;
|
|
87
|
+
return source;
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return identifierTypes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function fetchPatientIdentifierTypes(): Promise<Array<FetchedPatientIdentifierType>> {
|
|
95
|
+
const [patientIdentifierTypesResponse, primaryIdentifierTypeResponse] = await Promise.all([
|
|
96
|
+
cacheAndFetch('/ws/rest/v1/patientidentifiertype?v=custom:(display,uuid,name,format,required,uniquenessBehavior)'),
|
|
97
|
+
cacheAndFetch('/ws/rest/v1/metadatamapping/termmapping?v=full&code=emr.primaryIdentifierType'),
|
|
98
|
+
]);
|
|
99
|
+
|
|
100
|
+
if (patientIdentifierTypesResponse.ok) {
|
|
101
|
+
// Primary identifier type is to be kept at the top of the list.
|
|
102
|
+
const patientIdentifierTypes = patientIdentifierTypesResponse?.data?.results;
|
|
103
|
+
|
|
104
|
+
const primaryIdentifierTypeUuid = primaryIdentifierTypeResponse?.data?.results?.[0]?.metadataUuid;
|
|
105
|
+
|
|
106
|
+
let identifierTypes = primaryIdentifierTypeResponse?.ok
|
|
107
|
+
? [
|
|
108
|
+
mapPatientIdentifierType(
|
|
109
|
+
patientIdentifierTypes?.find((type) => type.uuid === primaryIdentifierTypeUuid),
|
|
110
|
+
true,
|
|
111
|
+
),
|
|
112
|
+
]
|
|
113
|
+
: [];
|
|
114
|
+
|
|
115
|
+
patientIdentifierTypes.forEach((type) => {
|
|
116
|
+
if (type.uuid !== primaryIdentifierTypeUuid) {
|
|
117
|
+
identifierTypes.push(mapPatientIdentifierType(type, false));
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
return identifierTypes;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return [];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function fetchIdentifierSources(identifierType: string) {
|
|
127
|
+
return await cacheAndFetch(`/ws/rest/v1/idgen/identifiersource?v=default&identifierType=${identifierType}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function fetchAutoGenerationOptions(abortController?: AbortController) {
|
|
131
|
+
return await cacheAndFetch(`/ws/rest/v1/idgen/autogenerationoption?v=full`);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async function cacheAndFetch<T = any>(url?: string) {
|
|
135
|
+
const abortController = new AbortController();
|
|
136
|
+
|
|
137
|
+
await messageOmrsServiceWorker({
|
|
138
|
+
type: 'registerDynamicRoute',
|
|
139
|
+
pattern: escapeRegExp(url),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
return await openmrsFetch<T>(url, { headers: cacheForOfflineHeaders, signal: abortController?.signal });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function mapPatientIdentifierType(patientIdentifierType, isPrimary) {
|
|
146
|
+
return {
|
|
147
|
+
name: patientIdentifierType.display,
|
|
148
|
+
fieldName: camelCase(patientIdentifierType.name),
|
|
149
|
+
required: patientIdentifierType.required,
|
|
150
|
+
uuid: patientIdentifierType.uuid,
|
|
151
|
+
format: patientIdentifierType.format,
|
|
152
|
+
isPrimary,
|
|
153
|
+
uniquenessBehavior: patientIdentifierType.uniquenessBehavior,
|
|
154
|
+
};
|
|
155
|
+
}
|
package/src/offline.ts
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeUrl,
|
|
3
|
+
messageOmrsServiceWorker,
|
|
4
|
+
navigate,
|
|
5
|
+
setupDynamicOfflineDataHandler,
|
|
6
|
+
setupOfflineSync,
|
|
7
|
+
type SyncProcessOptions,
|
|
8
|
+
} from '@openmrs/esm-framework';
|
|
9
|
+
import { patientRegistration, personRelationshipRepresentation } from './constants';
|
|
10
|
+
import {
|
|
11
|
+
fetchAddressTemplate,
|
|
12
|
+
fetchAllFieldDefinitionTypes,
|
|
13
|
+
fetchAllRelationshipTypes,
|
|
14
|
+
fetchCurrentSession,
|
|
15
|
+
fetchPatientIdentifierTypesWithSources,
|
|
16
|
+
} from './offline.resources';
|
|
17
|
+
import { FormManager } from './patient-registration/form-manager';
|
|
18
|
+
import { type PatientRegistration } from './patient-registration/patient-registration.types';
|
|
19
|
+
|
|
20
|
+
export function setupOffline() {
|
|
21
|
+
setupOfflineSync(patientRegistration, [], syncPatientRegistration, {
|
|
22
|
+
onBeginEditSyncItem(syncItem) {
|
|
23
|
+
navigate({ to: `\${openmrsSpaBase}/patient/${syncItem.content.fhirPatient.id}/edit` });
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
precacheStaticAssets();
|
|
28
|
+
|
|
29
|
+
setupDynamicOfflineDataHandler({
|
|
30
|
+
id: 'esm-patient-registration-app:patient',
|
|
31
|
+
type: 'patient',
|
|
32
|
+
displayName: 'Patient registration',
|
|
33
|
+
async isSynced(patientUuid) {
|
|
34
|
+
const expectedUrls = getPatientUrlsToBeCached(patientUuid);
|
|
35
|
+
const cache = await caches.open('omrs-spa-cache-v1');
|
|
36
|
+
const keys = (await cache.keys()).map((key) => key.url);
|
|
37
|
+
return expectedUrls.every((url) => keys.includes(url));
|
|
38
|
+
},
|
|
39
|
+
async sync(patientUuid) {
|
|
40
|
+
const urlsToCache = getPatientUrlsToBeCached(patientUuid);
|
|
41
|
+
await Promise.allSettled(
|
|
42
|
+
urlsToCache.map(async (url) => {
|
|
43
|
+
await messageOmrsServiceWorker({
|
|
44
|
+
type: 'registerDynamicRoute',
|
|
45
|
+
url,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await fetch(url);
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function getPatientUrlsToBeCached(patientUuid: string) {
|
|
56
|
+
return [
|
|
57
|
+
`/ws/fhir2/R4/Patient/${patientUuid}`,
|
|
58
|
+
`/ws/rest/v1/relationship?v=${personRelationshipRepresentation}&person=${patientUuid}`,
|
|
59
|
+
`/ws/rest/v1/person/${patientUuid}/attribute`,
|
|
60
|
+
`/ws/rest/v1/patient/${patientUuid}/identifier?v=custom:(uuid,identifier,identifierType:(uuid,required,name),preferred)`,
|
|
61
|
+
].map((url) => window.origin + makeUrl(url));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function precacheStaticAssets() {
|
|
65
|
+
await Promise.all([
|
|
66
|
+
fetchCurrentSession(),
|
|
67
|
+
fetchAddressTemplate(),
|
|
68
|
+
fetchAllRelationshipTypes(),
|
|
69
|
+
fetchAllFieldDefinitionTypes(),
|
|
70
|
+
fetchPatientIdentifierTypesWithSources(),
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export async function syncPatientRegistration(
|
|
75
|
+
queuedPatient: PatientRegistration,
|
|
76
|
+
options: SyncProcessOptions<PatientRegistration>,
|
|
77
|
+
) {
|
|
78
|
+
await FormManager.savePatientFormOnline(
|
|
79
|
+
queuedPatient._patientRegistrationData.isNewPatient,
|
|
80
|
+
queuedPatient._patientRegistrationData.formValues,
|
|
81
|
+
queuedPatient._patientRegistrationData.patientUuidMap,
|
|
82
|
+
queuedPatient._patientRegistrationData.initialAddressFieldValues,
|
|
83
|
+
queuedPatient._patientRegistrationData.capturePhotoProps,
|
|
84
|
+
queuedPatient._patientRegistrationData.currentLocation,
|
|
85
|
+
queuedPatient._patientRegistrationData.initialIdentifierValues,
|
|
86
|
+
queuedPatient._patientRegistrationData.currentUser,
|
|
87
|
+
queuedPatient._patientRegistrationData.config,
|
|
88
|
+
queuedPatient._patientRegistrationData.savePatientTransactionManager,
|
|
89
|
+
options.abort,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
3
|
+
import { showModal, navigate } from '@openmrs/esm-framework';
|
|
4
|
+
import { useTranslation } from 'react-i18next';
|
|
5
|
+
|
|
6
|
+
function getUrlWithoutPrefix(url: string) {
|
|
7
|
+
return url.split(window['getOpenmrsSpaBase']())?.[1];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
interface BeforeSavePromptProps {
|
|
11
|
+
when: boolean;
|
|
12
|
+
redirect?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const BeforeSavePrompt: React.FC<BeforeSavePromptProps> = ({ when, redirect }) => {
|
|
16
|
+
const { t } = useTranslation();
|
|
17
|
+
const ref = useRef<boolean>(false);
|
|
18
|
+
const [localTarget, setTarget] = useState<string | undefined>();
|
|
19
|
+
const target = localTarget || redirect;
|
|
20
|
+
const cancelUnload = useCallback(
|
|
21
|
+
(e: BeforeUnloadEvent) => {
|
|
22
|
+
const message = t(
|
|
23
|
+
'discardModalBody',
|
|
24
|
+
"The changes you made to this patient's details have not been saved. Discard changes?",
|
|
25
|
+
);
|
|
26
|
+
e.preventDefault();
|
|
27
|
+
e.returnValue = message;
|
|
28
|
+
return message;
|
|
29
|
+
},
|
|
30
|
+
[t],
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const cancelNavigation = useCallback((evt: CustomEvent) => {
|
|
34
|
+
if (!evt.detail.navigationIsCanceled && !ref.current) {
|
|
35
|
+
ref.current = true;
|
|
36
|
+
evt.detail.cancelNavigation();
|
|
37
|
+
const dispose = showModal(
|
|
38
|
+
'cancel-patient-edit-modal',
|
|
39
|
+
{
|
|
40
|
+
onConfirm: () => {
|
|
41
|
+
setTarget(evt.detail.newUrl);
|
|
42
|
+
dispose();
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
() => {
|
|
46
|
+
ref.current = false;
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}, []);
|
|
51
|
+
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
if (when && typeof target === 'undefined') {
|
|
54
|
+
window.addEventListener('single-spa:before-routing-event', cancelNavigation);
|
|
55
|
+
window.addEventListener('beforeunload', cancelUnload);
|
|
56
|
+
|
|
57
|
+
return () => {
|
|
58
|
+
window.removeEventListener('beforeunload', cancelUnload);
|
|
59
|
+
window.removeEventListener('single-spa:before-routing-event', cancelNavigation);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
}, [target, when, cancelUnload, cancelNavigation]);
|
|
63
|
+
|
|
64
|
+
useEffect(() => {
|
|
65
|
+
if (typeof target === 'string') {
|
|
66
|
+
navigate({ to: `\${openmrsSpaBase}/${getUrlWithoutPrefix(target)}` });
|
|
67
|
+
}
|
|
68
|
+
}, [target]);
|
|
69
|
+
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export default BeforeSavePrompt;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const generateFormatting = (order: Array<string>, separator: string) => {
|
|
2
|
+
const parse = (value: string) => {
|
|
3
|
+
const parts = value.split(separator);
|
|
4
|
+
const date = new Date(null);
|
|
5
|
+
|
|
6
|
+
order.forEach((key, index) => {
|
|
7
|
+
switch (key) {
|
|
8
|
+
case 'd':
|
|
9
|
+
date.setDate(parseInt(parts[index]));
|
|
10
|
+
break;
|
|
11
|
+
case 'm':
|
|
12
|
+
date.setMonth(parseInt(parts[index]) - 1);
|
|
13
|
+
break;
|
|
14
|
+
case 'Y':
|
|
15
|
+
date.setFullYear(parseInt(parts[index]));
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
return date;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const format = (date: Date) => {
|
|
23
|
+
if (date === null) {
|
|
24
|
+
return '';
|
|
25
|
+
} else if (!(date instanceof Date)) {
|
|
26
|
+
return date;
|
|
27
|
+
} else {
|
|
28
|
+
const parts = [];
|
|
29
|
+
|
|
30
|
+
order.forEach((key, index) => {
|
|
31
|
+
switch (key) {
|
|
32
|
+
case 'd':
|
|
33
|
+
parts[index] = date.getDate();
|
|
34
|
+
break;
|
|
35
|
+
case 'm':
|
|
36
|
+
parts[index] = date.getMonth() + 1;
|
|
37
|
+
break;
|
|
38
|
+
case 'Y':
|
|
39
|
+
parts[index] = date.getFullYear();
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return parts.join(separator);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const placeHolder = order.map((x) => (x === 'Y' ? 'YYYY' : x + x)).join(separator);
|
|
49
|
+
const dateFormat = order.join(separator);
|
|
50
|
+
|
|
51
|
+
return { parse, format, placeHolder, dateFormat };
|
|
52
|
+
};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { type ConceptResponse } from '../../patient-registration.types';
|
|
2
|
+
|
|
3
|
+
export const useConcept = jest.fn(function mockUseConceptImplementation(uuid: string): {
|
|
4
|
+
data: ConceptResponse;
|
|
5
|
+
isLoading: boolean;
|
|
6
|
+
} {
|
|
7
|
+
let data;
|
|
8
|
+
if (uuid == 'weight-uuid') {
|
|
9
|
+
data = {
|
|
10
|
+
uuid: 'weight-uuid',
|
|
11
|
+
display: 'Weight (kg)',
|
|
12
|
+
datatype: { display: 'Numeric', uuid: 'num' },
|
|
13
|
+
answers: [],
|
|
14
|
+
setMembers: [],
|
|
15
|
+
};
|
|
16
|
+
} else if (uuid == 'chief-complaint-uuid') {
|
|
17
|
+
data = {
|
|
18
|
+
uuid: 'chief-complaint-uuid',
|
|
19
|
+
display: 'Chief Complaint',
|
|
20
|
+
datatype: { display: 'Text', uuid: 'txt' },
|
|
21
|
+
answers: [],
|
|
22
|
+
setMembers: [],
|
|
23
|
+
};
|
|
24
|
+
} else if (uuid == 'nationality-uuid') {
|
|
25
|
+
data = {
|
|
26
|
+
uuid: 'nationality-uuid',
|
|
27
|
+
display: 'Nationality',
|
|
28
|
+
datatype: { display: 'Coded', uuid: 'cdd' },
|
|
29
|
+
answers: [
|
|
30
|
+
{ display: 'USA', uuid: 'usa' },
|
|
31
|
+
{ display: 'Mexico', uuid: 'mex' },
|
|
32
|
+
],
|
|
33
|
+
setMembers: [],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
data: data ?? null,
|
|
38
|
+
isLoading: !data,
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
export const useConceptAnswers = jest.fn((uuid: string) => {
|
|
43
|
+
if (uuid == 'nationality-uuid') {
|
|
44
|
+
return {
|
|
45
|
+
data: [
|
|
46
|
+
{ display: 'USA', uuid: 'usa' },
|
|
47
|
+
{ display: 'Mexico', uuid: 'mex' },
|
|
48
|
+
],
|
|
49
|
+
isLoading: false,
|
|
50
|
+
};
|
|
51
|
+
} else if (uuid == 'other-countries-uuid') {
|
|
52
|
+
return {
|
|
53
|
+
data: [
|
|
54
|
+
{ display: 'Kenya', uuid: 'ke' },
|
|
55
|
+
{ display: 'Uganda', uuid: 'ug' },
|
|
56
|
+
],
|
|
57
|
+
isLoading: false,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
});
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import React, { useEffect, useState, useContext, useMemo } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ResourcesContext } from '../../../offline.resources';
|
|
4
|
+
import { SkeletonText, InlineNotification } from '@carbon/react';
|
|
5
|
+
import { Input } from '../../input/basic-input/input/input.component';
|
|
6
|
+
import { useConfig, useConnectivity } from '@openmrs/esm-framework';
|
|
7
|
+
import { PatientRegistrationContext } from '../../patient-registration-context';
|
|
8
|
+
import { useOrderedAddressHierarchyLevels } from './address-hierarchy.resource';
|
|
9
|
+
import AddressHierarchyLevels from './address-hierarchy-levels.component';
|
|
10
|
+
import AddressSearchComponent from './address-search.component';
|
|
11
|
+
import styles from '../field.scss';
|
|
12
|
+
|
|
13
|
+
function parseString(xmlDockAsString: string) {
|
|
14
|
+
const parser = new DOMParser();
|
|
15
|
+
return parser.parseFromString(xmlDockAsString, 'text/xml');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const AddressComponent: React.FC = () => {
|
|
19
|
+
const [selected, setSelected] = useState('');
|
|
20
|
+
const { addressTemplate } = useContext(ResourcesContext);
|
|
21
|
+
const addressLayout = useMemo(() => {
|
|
22
|
+
if (!addressTemplate?.lines) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const allFields = addressTemplate?.lines?.flat();
|
|
27
|
+
const fields = allFields?.filter(({ isToken }) => isToken === 'IS_ADDR_TOKEN');
|
|
28
|
+
const allRequiredFields = Object.fromEntries(addressTemplate?.requiredElements?.map((curr) => [curr, curr]) || []);
|
|
29
|
+
return fields.map(({ displayText, codeName }) => {
|
|
30
|
+
return {
|
|
31
|
+
id: codeName,
|
|
32
|
+
name: codeName,
|
|
33
|
+
label: displayText,
|
|
34
|
+
required: Boolean(allRequiredFields[codeName]),
|
|
35
|
+
};
|
|
36
|
+
});
|
|
37
|
+
}, [addressTemplate]);
|
|
38
|
+
|
|
39
|
+
const { t } = useTranslation();
|
|
40
|
+
const config = useConfig();
|
|
41
|
+
const isOnline = useConnectivity();
|
|
42
|
+
const {
|
|
43
|
+
fieldConfigurations: {
|
|
44
|
+
address: {
|
|
45
|
+
useAddressHierarchy: { enabled: addressHierarchyEnabled, useQuickSearch, searchAddressByLevel },
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
} = config;
|
|
49
|
+
|
|
50
|
+
const { setFieldValue } = useContext(PatientRegistrationContext);
|
|
51
|
+
const { orderedFields, isLoadingFieldOrder, errorFetchingFieldOrder } = useOrderedAddressHierarchyLevels();
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
if (addressTemplate?.elementDefaults) {
|
|
55
|
+
Object.entries(addressTemplate.elementDefaults).forEach(([name, defaultValue]) => {
|
|
56
|
+
setFieldValue(`address.${name}`, defaultValue);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}, [addressTemplate, setFieldValue]);
|
|
60
|
+
|
|
61
|
+
const orderedAddressFields = useMemo(() => {
|
|
62
|
+
if (isLoadingFieldOrder || errorFetchingFieldOrder) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const orderMap = Object.fromEntries(orderedFields.map((field, indx) => [field, indx]));
|
|
67
|
+
|
|
68
|
+
return [...addressLayout].sort(
|
|
69
|
+
(existingField1, existingField2) => orderMap[existingField1.name] - orderMap[existingField2.name],
|
|
70
|
+
);
|
|
71
|
+
}, [isLoadingFieldOrder, errorFetchingFieldOrder, orderedFields, addressLayout]);
|
|
72
|
+
|
|
73
|
+
if (!addressTemplate) {
|
|
74
|
+
return (
|
|
75
|
+
<AddressComponentContainer>
|
|
76
|
+
<SkeletonText />
|
|
77
|
+
</AddressComponentContainer>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!addressHierarchyEnabled || !isOnline) {
|
|
82
|
+
return (
|
|
83
|
+
<AddressComponentContainer>
|
|
84
|
+
{addressLayout.map((attributes, index) => (
|
|
85
|
+
<Input
|
|
86
|
+
key={`combo_input_${index}`}
|
|
87
|
+
name={`address.${attributes.name}`}
|
|
88
|
+
labelText={t(attributes.label)}
|
|
89
|
+
id={attributes.name}
|
|
90
|
+
value={selected}
|
|
91
|
+
required={attributes.required}
|
|
92
|
+
/>
|
|
93
|
+
))}
|
|
94
|
+
</AddressComponentContainer>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (isLoadingFieldOrder) {
|
|
99
|
+
return (
|
|
100
|
+
<AddressComponentContainer>
|
|
101
|
+
<SkeletonText />
|
|
102
|
+
</AddressComponentContainer>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (errorFetchingFieldOrder) {
|
|
107
|
+
return (
|
|
108
|
+
<AddressComponentContainer>
|
|
109
|
+
<InlineNotification
|
|
110
|
+
style={{ margin: '0', minWidth: '100%' }}
|
|
111
|
+
kind="error"
|
|
112
|
+
lowContrast={true}
|
|
113
|
+
title={t('errorFetchingOrderedFields', 'Error occured fetching ordered fields for address hierarchy')}
|
|
114
|
+
/>
|
|
115
|
+
</AddressComponentContainer>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<AddressComponentContainer>
|
|
121
|
+
{useQuickSearch && <AddressSearchComponent addressLayout={orderedAddressFields} />}
|
|
122
|
+
{searchAddressByLevel ? (
|
|
123
|
+
<AddressHierarchyLevels orderedAddressFields={orderedAddressFields} />
|
|
124
|
+
) : (
|
|
125
|
+
orderedAddressFields.map((attributes, index) => (
|
|
126
|
+
<Input
|
|
127
|
+
key={`combo_input_${index}`}
|
|
128
|
+
name={`address.${attributes.name}`}
|
|
129
|
+
labelText={t(attributes.label)}
|
|
130
|
+
id={attributes.name}
|
|
131
|
+
value={selected}
|
|
132
|
+
required={attributes.required}
|
|
133
|
+
/>
|
|
134
|
+
))
|
|
135
|
+
)}
|
|
136
|
+
</AddressComponentContainer>
|
|
137
|
+
);
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const AddressComponentContainer = ({ children }) => {
|
|
141
|
+
const { t } = useTranslation();
|
|
142
|
+
return (
|
|
143
|
+
<div>
|
|
144
|
+
<h4 className={styles.productiveHeading02Light}>{t('addressHeader', 'Address')}</h4>
|
|
145
|
+
<div
|
|
146
|
+
style={{
|
|
147
|
+
paddingBottom: '5%',
|
|
148
|
+
}}>
|
|
149
|
+
{children}
|
|
150
|
+
</div>
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import React, { useCallback } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { useAddressEntries, useAddressEntryFetchConfig } from './address-hierarchy.resource';
|
|
4
|
+
import { useField } from 'formik';
|
|
5
|
+
import ComboInput from '../../input/combo-input/combo-input.component';
|
|
6
|
+
|
|
7
|
+
interface AddressHierarchyLevelsProps {
|
|
8
|
+
orderedAddressFields: Array<any>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const AddressHierarchyLevels: React.FC<AddressHierarchyLevelsProps> = ({ orderedAddressFields }) => {
|
|
12
|
+
const { t } = useTranslation();
|
|
13
|
+
|
|
14
|
+
return (
|
|
15
|
+
<>
|
|
16
|
+
{orderedAddressFields.map((attribute) => (
|
|
17
|
+
<AddressComboBox key={attribute.id} attribute={attribute} />
|
|
18
|
+
))}
|
|
19
|
+
</>
|
|
20
|
+
);
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default AddressHierarchyLevels;
|
|
24
|
+
|
|
25
|
+
interface AddressComboBoxProps {
|
|
26
|
+
attribute: {
|
|
27
|
+
id: string;
|
|
28
|
+
name: string;
|
|
29
|
+
value: string;
|
|
30
|
+
label: string;
|
|
31
|
+
required?: boolean;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const AddressComboBox: React.FC<AddressComboBoxProps> = ({ attribute }) => {
|
|
36
|
+
const { t } = useTranslation();
|
|
37
|
+
const [field, meta, { setValue }] = useField(`address.${attribute.name}`);
|
|
38
|
+
const { fetchEntriesForField, searchString, updateChildElements } = useAddressEntryFetchConfig(attribute.name);
|
|
39
|
+
const { entries } = useAddressEntries(fetchEntriesForField, searchString);
|
|
40
|
+
const label = t(attribute.label) + (attribute?.required ? '' : ` (${t('optional', 'optional')})`);
|
|
41
|
+
|
|
42
|
+
const handleInputChange = useCallback(
|
|
43
|
+
(newValue) => {
|
|
44
|
+
setValue(newValue);
|
|
45
|
+
},
|
|
46
|
+
[setValue],
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const handleSelection = useCallback(
|
|
50
|
+
(selectedItem) => {
|
|
51
|
+
if (meta.value !== selectedItem) {
|
|
52
|
+
setValue(selectedItem);
|
|
53
|
+
updateChildElements();
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
[updateChildElements, meta.value, setValue],
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<ComboInput
|
|
61
|
+
entries={entries}
|
|
62
|
+
handleSelection={handleSelection}
|
|
63
|
+
name={`address.${attribute.name}`}
|
|
64
|
+
fieldProps={{
|
|
65
|
+
...field,
|
|
66
|
+
id: attribute.name,
|
|
67
|
+
labelText: label,
|
|
68
|
+
required: attribute?.required,
|
|
69
|
+
}}
|
|
70
|
+
handleInputChange={handleInputChange}
|
|
71
|
+
/>
|
|
72
|
+
);
|
|
73
|
+
};
|