@kenyaemr/esm-patient-registration-app 4.3.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.
- package/README.md +7 -0
- package/__mocks__/autogenerationoptions.mock.ts +34 -0
- package/__mocks__/react-i18next.js +49 -0
- package/dist/144.js +2 -0
- package/dist/144.js.LICENSE.txt +27 -0
- package/dist/144.js.map +1 -0
- package/dist/207.js +1 -0
- package/dist/207.js.map +1 -0
- package/dist/317.js +2 -0
- package/dist/317.js.LICENSE.txt +6 -0
- package/dist/317.js.map +1 -0
- package/dist/330.js +1 -0
- package/dist/330.js.map +1 -0
- package/dist/574.js +1 -0
- package/dist/59.js +1 -0
- package/dist/59.js.map +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/62.js +1 -0
- package/dist/62.js.map +1 -0
- package/dist/635.js +1 -0
- package/dist/635.js.map +1 -0
- package/dist/68.js +1 -0
- package/dist/68.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/805.js +1 -0
- package/dist/805.js.map +1 -0
- package/dist/807.js +1 -0
- package/dist/821.js +1 -0
- package/dist/821.js.map +1 -0
- package/dist/822.js +1 -0
- package/dist/822.js.map +1 -0
- package/dist/858.js +2 -0
- package/dist/858.js.LICENSE.txt +3 -0
- package/dist/858.js.map +1 -0
- package/dist/887.js +1 -0
- package/dist/887.js.map +1 -0
- package/dist/9.js +2 -0
- package/dist/9.js.LICENSE.txt +9 -0
- package/dist/9.js.map +1 -0
- package/dist/975.js +1 -0
- package/dist/975.js.map +1 -0
- package/dist/main.js +2 -0
- package/dist/main.js.LICENSE.txt +9 -0
- package/dist/main.js.map +1 -0
- package/dist/openmrs-esm-patient-registration-app.js +1 -0
- package/dist/openmrs-esm-patient-registration-app.js.buildmanifest.json +623 -0
- package/dist/openmrs-esm-patient-registration-app.js.map +1 -0
- package/dist/openmrs-esm-patient-registration-app.old +1 -0
- package/docs/images/patient-registration-hierarchy.png +0 -0
- package/package.json +55 -0
- package/src/add-patient-link.scss +3 -0
- package/src/add-patient-link.tsx +21 -0
- package/src/config-schema.ts +405 -0
- package/src/constants.ts +14 -0
- package/src/declarations.d.tsx +4 -0
- package/src/index.ts +131 -0
- package/src/nav-link.tsx +10 -0
- package/src/offline.resources.ts +109 -0
- package/src/offline.ts +90 -0
- package/src/patient-registration/before-save-prompt.tsx +72 -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 +31 -0
- package/src/patient-registration/field/address/address-hierarchy.component.tsx +143 -0
- package/src/patient-registration/field/address/address-hierarchy.test.tsx +181 -0
- package/src/patient-registration/field/address/address-search.component.tsx +98 -0
- package/src/patient-registration/field/address/address-search.scss +53 -0
- package/src/patient-registration/field/custom-field.component.tsx +25 -0
- package/src/patient-registration/field/dob/dob.component.tsx +143 -0
- package/src/patient-registration/field/dob/dob.test.tsx +73 -0
- package/src/patient-registration/field/field.component.tsx +44 -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/gender/gender-field.component.tsx +49 -0
- package/src/patient-registration/field/gender/gender-field.test.tsx +66 -0
- package/src/patient-registration/field/id/id-field.component.tsx +142 -0
- package/src/patient-registration/field/id/identifier-selection-overlay.tsx +194 -0
- package/src/patient-registration/field/id/identifier-selection.scss +37 -0
- package/src/patient-registration/field/name/name-field.component.tsx +109 -0
- package/src/patient-registration/field/obs/obs-field.component.tsx +185 -0
- package/src/patient-registration/field/obs/obs-field.test.tsx +127 -0
- package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +59 -0
- package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +68 -0
- package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +81 -0
- package/src/patient-registration/field/person-attributes/person-attributes.resource.tsx +20 -0
- package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +57 -0
- package/src/patient-registration/form-manager.test.ts +68 -0
- package/src/patient-registration/form-manager.ts +413 -0
- package/src/patient-registration/input/basic-input/input/input.component.tsx +59 -0
- package/src/patient-registration/input/basic-input/input/input.test.tsx +170 -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 +32 -0
- package/src/patient-registration/input/combo-input/combo-input.component.tsx +76 -0
- package/src/patient-registration/input/combo-input/combo-input.test.tsx +43 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +84 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +53 -0
- package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +109 -0
- package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.component.tsx +32 -0
- package/src/patient-registration/input/custom-input/estimated-age/estimated-age-input.test.tsx +36 -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 +110 -0
- package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
- package/src/patient-registration/input/custom-input/unidentified-patient/unidentified-patient-input.component.tsx +24 -0
- package/src/patient-registration/input/custom-input/unidentified-patient/unidentified-patient-input.test.tsx +39 -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 +108 -0
- package/src/patient-registration/patient-registration-context.ts +24 -0
- package/src/patient-registration/patient-registration-hooks.ts +320 -0
- package/src/patient-registration/patient-registration-types.tsx +271 -0
- package/src/patient-registration/patient-registration-utils.ts +219 -0
- package/src/patient-registration/patient-registration.component.tsx +250 -0
- package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
- package/src/patient-registration/patient-registration.resource.tsx +296 -0
- package/src/patient-registration/patient-registration.scss +94 -0
- package/src/patient-registration/patient-registration.test.tsx +436 -0
- package/src/patient-registration/section/death-info/death-info-section.component.tsx +30 -0
- package/src/patient-registration/section/death-info/death-info-section.test.tsx +73 -0
- package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
- package/src/patient-registration/section/demographics/demographics-section.test.tsx +84 -0
- package/src/patient-registration/section/generic-section.component.tsx +17 -0
- package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +226 -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/index.tsx +51 -0
- package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
- package/src/patient-registration/validation/patient-registration-validation.test.tsx +129 -0
- package/src/patient-registration/validation/patient-registration-validation.tsx +46 -0
- package/src/patient-verification/assets/counties.json +236 -0
- package/src/patient-verification/assets/verification-assets.ts +11 -0
- package/src/patient-verification/patient-verification-hook.tsx +156 -0
- package/src/patient-verification/patient-verification-utils.ts +173 -0
- package/src/patient-verification/patient-verification.component.tsx +118 -0
- package/src/patient-verification/patient-verification.scss +30 -0
- package/src/patient-verification/verification-modal/confirm-prompt.component.tsx +69 -0
- package/src/patient-verification/verification-modal/empty-prompt.component.tsx +35 -0
- package/src/patient-verification/verification-types.ts +50 -0
- package/src/resource.ts +12 -0
- package/src/root.component.tsx +66 -0
- package/src/root.scss +7 -0
- package/src/root.test.tsx +32 -0
- package/src/widgets/cancel-patient-edit.component.tsx +37 -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/edit-patient-details-button.component.tsx +34 -0
- package/src/widgets/edit-patient-details-button.scss +3 -0
- package/translations/en.json +108 -0
- package/translations/fr.json +89 -0
- package/translations/km.json +89 -0
- package/tsconfig.json +5 -0
- package/webpack.config.js +1 -0
|
@@ -0,0 +1,109 @@
|
|
|
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 { FetchResponse, messageOmrsServiceWorker, openmrsFetch, Session } from '@openmrs/esm-framework';
|
|
6
|
+
import { PatientIdentifierType, FetchedPatientIdentifierType } from './patient-registration/patient-registration-types';
|
|
7
|
+
import { cacheForOfflineHeaders } from './constants';
|
|
8
|
+
|
|
9
|
+
export interface Resources {
|
|
10
|
+
addressTemplate: any;
|
|
11
|
+
currentSession: Session;
|
|
12
|
+
relationshipTypes: any;
|
|
13
|
+
identifierTypes: Array<PatientIdentifierType>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const ResourcesContext = React.createContext<Resources>(null);
|
|
17
|
+
|
|
18
|
+
export async function fetchCurrentSession(): Promise<Session> {
|
|
19
|
+
const { data } = await cacheAndFetch<Session>('/ws/rest/v1/session');
|
|
20
|
+
return data;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function fetchAddressTemplate() {
|
|
24
|
+
const { data } = await cacheAndFetch('/ws/rest/v1/systemsetting?q=layout.address.format&v=custom:(value)');
|
|
25
|
+
return data;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function fetchAllRelationshipTypes() {
|
|
29
|
+
const { data } = await cacheAndFetch('/ws/rest/v1/relationshiptype?v=default');
|
|
30
|
+
return data;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function fetchPatientIdentifierTypesWithSources(): Promise<Array<PatientIdentifierType>> {
|
|
34
|
+
const patientIdentifierTypes = await fetchPatientIdentifierTypes();
|
|
35
|
+
|
|
36
|
+
// @ts-ignore Reason: The required props of the type are generated below.
|
|
37
|
+
const identifierTypes: Array<PatientIdentifierType> = patientIdentifierTypes.filter(Boolean);
|
|
38
|
+
|
|
39
|
+
const [autoGenOptions, ...allIdentifierSources] = await Promise.all([
|
|
40
|
+
fetchAutoGenerationOptions(),
|
|
41
|
+
...identifierTypes.map((identifierType) => fetchIdentifierSources(identifierType.uuid)),
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < identifierTypes?.length; i++) {
|
|
45
|
+
identifierTypes[i].identifierSources = allIdentifierSources[i].data.results.map((source) => {
|
|
46
|
+
const option = find(autoGenOptions.data.results, { source: { uuid: source.uuid } });
|
|
47
|
+
source.autoGenerationOption = option;
|
|
48
|
+
return source;
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return identifierTypes;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function fetchPatientIdentifierTypes(): Promise<Array<FetchedPatientIdentifierType>> {
|
|
56
|
+
const [patientIdentifierTypesResponse, primaryIdentifierTypeResponse] = await Promise.all([
|
|
57
|
+
cacheAndFetch('/ws/rest/v1/patientidentifiertype?v=custom:(display,uuid,name,format,required,uniquenessBehavior)'),
|
|
58
|
+
cacheAndFetch('/ws/rest/v1/metadatamapping/termmapping?v=full&code=emr.primaryIdentifierType'),
|
|
59
|
+
]);
|
|
60
|
+
|
|
61
|
+
if (patientIdentifierTypesResponse.ok) {
|
|
62
|
+
// Primary identifier type is to be kept at the top of the list.
|
|
63
|
+
const patientIdentifierTypes = patientIdentifierTypesResponse?.data?.results;
|
|
64
|
+
|
|
65
|
+
const primaryIdentifierTypeUuid = primaryIdentifierTypeResponse?.data?.results?.[0]?.metadataUuid;
|
|
66
|
+
|
|
67
|
+
let identifierTypes = [];
|
|
68
|
+
|
|
69
|
+
patientIdentifierTypes.forEach((type) => {
|
|
70
|
+
if (type.uuid !== primaryIdentifierTypeUuid) {
|
|
71
|
+
identifierTypes.push(mapPatientIdentifierType(type, false));
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
return identifierTypes;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return [];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function fetchIdentifierSources(identifierType: string) {
|
|
81
|
+
return await cacheAndFetch(`/ws/rest/v1/idgen/identifiersource?v=default&identifierType=${identifierType}`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async function fetchAutoGenerationOptions(abortController?: AbortController) {
|
|
85
|
+
return await cacheAndFetch(`/ws/rest/v1/idgen/autogenerationoption?v=full`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function cacheAndFetch<T = any>(url?: string) {
|
|
89
|
+
const abortController = new AbortController();
|
|
90
|
+
|
|
91
|
+
await messageOmrsServiceWorker({
|
|
92
|
+
type: 'registerDynamicRoute',
|
|
93
|
+
pattern: escapeRegExp(url),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return await openmrsFetch<T>(url, { headers: cacheForOfflineHeaders, signal: abortController?.signal });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function mapPatientIdentifierType(patientIdentifierType, isPrimary) {
|
|
100
|
+
return {
|
|
101
|
+
name: patientIdentifierType.display,
|
|
102
|
+
fieldName: camelCase(patientIdentifierType.name),
|
|
103
|
+
required: patientIdentifierType.required,
|
|
104
|
+
uuid: patientIdentifierType.uuid,
|
|
105
|
+
format: patientIdentifierType.format,
|
|
106
|
+
isPrimary,
|
|
107
|
+
uniquenessBehavior: patientIdentifierType.uniquenessBehavior,
|
|
108
|
+
};
|
|
109
|
+
}
|
package/src/offline.ts
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
makeUrl,
|
|
3
|
+
messageOmrsServiceWorker,
|
|
4
|
+
navigate,
|
|
5
|
+
setupDynamicOfflineDataHandler,
|
|
6
|
+
setupOfflineSync,
|
|
7
|
+
subscribePrecacheStaticDependencies,
|
|
8
|
+
SyncProcessOptions,
|
|
9
|
+
} from '@openmrs/esm-framework';
|
|
10
|
+
import { patientRegistration, personRelationshipRepresentation } from './constants';
|
|
11
|
+
import {
|
|
12
|
+
fetchAddressTemplate,
|
|
13
|
+
fetchAllRelationshipTypes,
|
|
14
|
+
fetchCurrentSession,
|
|
15
|
+
fetchPatientIdentifierTypesWithSources,
|
|
16
|
+
} from './offline.resources';
|
|
17
|
+
import { FormManager } from './patient-registration/form-manager';
|
|
18
|
+
import { 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
|
+
subscribePrecacheStaticDependencies(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
|
+
fetchPatientIdentifierTypesWithSources(),
|
|
70
|
+
]);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function syncPatientRegistration(
|
|
74
|
+
queuedPatient: PatientRegistration,
|
|
75
|
+
options: SyncProcessOptions<PatientRegistration>,
|
|
76
|
+
) {
|
|
77
|
+
await FormManager.savePatientFormOnline(
|
|
78
|
+
queuedPatient._patientRegistrationData.isNewPatient,
|
|
79
|
+
queuedPatient._patientRegistrationData.formValues,
|
|
80
|
+
queuedPatient._patientRegistrationData.patientUuidMap,
|
|
81
|
+
queuedPatient._patientRegistrationData.initialAddressFieldValues,
|
|
82
|
+
queuedPatient._patientRegistrationData.capturePhotoProps,
|
|
83
|
+
queuedPatient._patientRegistrationData.currentLocation,
|
|
84
|
+
queuedPatient._patientRegistrationData.initialIdentifierValues,
|
|
85
|
+
queuedPatient._patientRegistrationData.currentUser,
|
|
86
|
+
queuedPatient._patientRegistrationData.config,
|
|
87
|
+
queuedPatient._patientRegistrationData.savePatientTransactionManager,
|
|
88
|
+
options.abort,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { showModal, navigate } from '@openmrs/esm-framework';
|
|
3
|
+
import { useTranslation } from 'react-i18next';
|
|
4
|
+
|
|
5
|
+
function getUrlWithoutPrefix(url: string) {
|
|
6
|
+
return url.split(window['getOpenmrsSpaBase']())?.[1];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface BeforeSavePromptProps {
|
|
10
|
+
when: boolean;
|
|
11
|
+
redirect?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const BeforeSavePrompt: React.FC<BeforeSavePromptProps> = ({ when, redirect }) => {
|
|
15
|
+
const { t } = useTranslation();
|
|
16
|
+
const ref = useRef<boolean>(false);
|
|
17
|
+
const [localTarget, setTarget] = useState<string | undefined>();
|
|
18
|
+
const target = localTarget || redirect;
|
|
19
|
+
const cancelUnload = useCallback(
|
|
20
|
+
(e: BeforeUnloadEvent) => {
|
|
21
|
+
const message = t(
|
|
22
|
+
'discardModalBody',
|
|
23
|
+
"The changes you made to this patient's details have not been saved. Discard changes?",
|
|
24
|
+
);
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
e.returnValue = message;
|
|
27
|
+
return message;
|
|
28
|
+
},
|
|
29
|
+
[t],
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const cancelNavigation = useCallback((evt: CustomEvent) => {
|
|
33
|
+
if (!evt.detail.navigationIsCanceled && !ref.current) {
|
|
34
|
+
ref.current = true;
|
|
35
|
+
evt.detail.cancelNavigation();
|
|
36
|
+
const dispose = showModal(
|
|
37
|
+
'cancel-patient-edit-modal',
|
|
38
|
+
{
|
|
39
|
+
onConfirm: () => {
|
|
40
|
+
setTarget(evt.detail.newUrl);
|
|
41
|
+
dispose();
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
() => {
|
|
45
|
+
ref.current = false;
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
}, []);
|
|
50
|
+
|
|
51
|
+
useEffect(() => {
|
|
52
|
+
if (when && typeof target === 'undefined') {
|
|
53
|
+
window.addEventListener('single-spa:before-routing-event', cancelNavigation);
|
|
54
|
+
window.addEventListener('beforeunload', cancelUnload);
|
|
55
|
+
|
|
56
|
+
return () => {
|
|
57
|
+
window.removeEventListener('beforeunload', cancelUnload);
|
|
58
|
+
window.removeEventListener('single-spa:before-routing-event', cancelNavigation);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
}, [target, when, cancelUnload, cancelNavigation]);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
if (typeof target === 'string') {
|
|
65
|
+
navigate({ to: `\${openmrsSpaBase}/${getUrlWithoutPrefix(target)}` });
|
|
66
|
+
}
|
|
67
|
+
}, [target]);
|
|
68
|
+
|
|
69
|
+
return null;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
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 { 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,31 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { Input } from '../../input/basic-input/input/input.component';
|
|
4
|
+
import styles from '../field.scss';
|
|
5
|
+
import { FieldDefinition } from '../../../config-schema';
|
|
6
|
+
import { Field } from 'formik';
|
|
7
|
+
|
|
8
|
+
export interface AddressFieldProps {
|
|
9
|
+
fieldDefinition: FieldDefinition;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const AddressField: React.FC<AddressFieldProps> = ({ fieldDefinition }) => {
|
|
13
|
+
const { t } = useTranslation();
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<div className={`${styles.customField} ${styles.halfWidthInDesktopView}`}>
|
|
17
|
+
<Field name={fieldDefinition.id}>
|
|
18
|
+
{({ field, form: { touched, errors }, meta }) => {
|
|
19
|
+
return (
|
|
20
|
+
<Input
|
|
21
|
+
id={fieldDefinition.id}
|
|
22
|
+
labelText={t(`${fieldDefinition.label}`, `${fieldDefinition.label}`)}
|
|
23
|
+
{...field}
|
|
24
|
+
required={fieldDefinition.validation.required}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}}
|
|
28
|
+
</Field>
|
|
29
|
+
</div>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import React, { useEffect, useState, useContext } from 'react';
|
|
2
|
+
import { useTranslation } from 'react-i18next';
|
|
3
|
+
import { ResourcesContext } from '../../../offline.resources';
|
|
4
|
+
import { ComboInput } from '../../input/combo-input/combo-input.component';
|
|
5
|
+
import { SkeletonText } from '@carbon/react';
|
|
6
|
+
import styles from '../field.scss';
|
|
7
|
+
import { Input } from '../../input/basic-input/input/input.component';
|
|
8
|
+
import { useConfig } from '@openmrs/esm-framework';
|
|
9
|
+
import AddressSearchComponent from './address-search.component';
|
|
10
|
+
import { PatientRegistrationContext } from '../../patient-registration-context';
|
|
11
|
+
|
|
12
|
+
function parseString(xmlDockAsString: string) {
|
|
13
|
+
const parser = new DOMParser();
|
|
14
|
+
return parser.parseFromString(xmlDockAsString, 'text/xml');
|
|
15
|
+
}
|
|
16
|
+
function getTagAsDocument(tagName: string, template: XMLDocument) {
|
|
17
|
+
const tmp = template.getElementsByTagName(tagName)[0];
|
|
18
|
+
return tmp ? parseString(tmp.outerHTML) : parseString('');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const AddressHierarchy: React.FC = () => {
|
|
22
|
+
const [selected, setSelected] = useState('');
|
|
23
|
+
const [addressLayout, setAddressLayout] = useState([]);
|
|
24
|
+
const { t } = useTranslation();
|
|
25
|
+
const { addressTemplate } = useContext(ResourcesContext);
|
|
26
|
+
const addressTemplateXml = addressTemplate?.results[0].value;
|
|
27
|
+
const setSelectedValue = (value: string) => {
|
|
28
|
+
setSelected(value);
|
|
29
|
+
};
|
|
30
|
+
const config = useConfig();
|
|
31
|
+
const {
|
|
32
|
+
fieldConfigurations: {
|
|
33
|
+
address: {
|
|
34
|
+
useAddressHierarchy: { enabled, useQuickSearch, searchAddressByLevel },
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
} = config;
|
|
38
|
+
|
|
39
|
+
const { setFieldValue, values } = useContext(PatientRegistrationContext);
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
const templateXmlDoc = parseString(addressTemplateXml);
|
|
43
|
+
const elementDefaults = getTagAsDocument('elementDefaults', templateXmlDoc);
|
|
44
|
+
const defaultValuesEntries = elementDefaults.getElementsByTagName('entry');
|
|
45
|
+
const defaultValues = Object.fromEntries(
|
|
46
|
+
Array.prototype.map.call(defaultValuesEntries, (entry: Element) => {
|
|
47
|
+
const [name, value] = Array.from(entry.getElementsByTagName('string'));
|
|
48
|
+
return [name.innerHTML, value.innerHTML];
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
const nameMappings = getTagAsDocument('nameMappings', templateXmlDoc);
|
|
52
|
+
const properties =
|
|
53
|
+
Array.from(nameMappings.getElementsByTagName('property')).length > 0
|
|
54
|
+
? nameMappings.getElementsByTagName('property')
|
|
55
|
+
: nameMappings.getElementsByTagName('entry');
|
|
56
|
+
|
|
57
|
+
const propertiesObj = Array.prototype.map.call(properties, (property: Element) => {
|
|
58
|
+
const name = property.getAttribute('name') ?? property.getElementsByTagName('string')[0].innerHTML;
|
|
59
|
+
const label = property.getAttribute('value') ?? property.getElementsByTagName('string')[1].innerHTML;
|
|
60
|
+
/*
|
|
61
|
+
DO NOT REMOVE THIS COMMENT UNLESS YOU UNDERSTAND WHY IT IS HERE
|
|
62
|
+
|
|
63
|
+
t('postalCode', 'Postal code')
|
|
64
|
+
t('address1', 'Address line 1')
|
|
65
|
+
t('address2', 'Address line 2')
|
|
66
|
+
t('countyDistrict', 'District')
|
|
67
|
+
t('stateProvince', 'State')
|
|
68
|
+
t('cityVillage', 'city')
|
|
69
|
+
t('country', 'Country')
|
|
70
|
+
t('countyDistrict', 'District')
|
|
71
|
+
*/
|
|
72
|
+
const value = defaultValues[name];
|
|
73
|
+
if (!values?.address?.[name] && value) {
|
|
74
|
+
setFieldValue(`address.${name}`, value);
|
|
75
|
+
}
|
|
76
|
+
return {
|
|
77
|
+
id: name,
|
|
78
|
+
name,
|
|
79
|
+
value,
|
|
80
|
+
label,
|
|
81
|
+
};
|
|
82
|
+
});
|
|
83
|
+
setAddressLayout(propertiesObj);
|
|
84
|
+
}, [t, addressTemplateXml, setFieldValue, values]);
|
|
85
|
+
|
|
86
|
+
if (!addressTemplate) {
|
|
87
|
+
return (
|
|
88
|
+
<div>
|
|
89
|
+
<h4 className={styles.productiveHeading02Light}>{t('addressHeader', 'Address')}</h4>
|
|
90
|
+
<SkeletonText />
|
|
91
|
+
</div>
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<div>
|
|
97
|
+
<h4 className={styles.productiveHeading02Light}>{t('addressHeader', 'Address')}</h4>
|
|
98
|
+
<div
|
|
99
|
+
style={{
|
|
100
|
+
paddingBottom: '5%',
|
|
101
|
+
}}>
|
|
102
|
+
{enabled ? (
|
|
103
|
+
<>
|
|
104
|
+
{useQuickSearch && <AddressSearchComponent addressLayout={addressLayout} />}
|
|
105
|
+
{addressLayout.map((attributes, index) =>
|
|
106
|
+
searchAddressByLevel ? (
|
|
107
|
+
<ComboInput
|
|
108
|
+
key={`combo_input_${index}`}
|
|
109
|
+
textFieldName={attributes.name}
|
|
110
|
+
name={`address.${attributes.name}`}
|
|
111
|
+
labelText={t(attributes.label)}
|
|
112
|
+
id={attributes.name}
|
|
113
|
+
setSelectedValue={setSelectedValue}
|
|
114
|
+
selected={selected}
|
|
115
|
+
/>
|
|
116
|
+
) : (
|
|
117
|
+
<Input
|
|
118
|
+
key={`combo_input_${index}`}
|
|
119
|
+
name={`address.${attributes.name}`}
|
|
120
|
+
labelText={t(attributes.label)}
|
|
121
|
+
id={attributes.name}
|
|
122
|
+
setSelectedValue={setSelectedValue}
|
|
123
|
+
selected={selected}
|
|
124
|
+
/>
|
|
125
|
+
),
|
|
126
|
+
)}
|
|
127
|
+
</>
|
|
128
|
+
) : (
|
|
129
|
+
addressLayout.map((attributes, index) => (
|
|
130
|
+
<Input
|
|
131
|
+
key={`combo_input_${index}`}
|
|
132
|
+
name={`address.${attributes.name}`}
|
|
133
|
+
labelText={t(attributes.label)}
|
|
134
|
+
id={attributes.name}
|
|
135
|
+
setSelectedValue={setSelectedValue}
|
|
136
|
+
selected={selected}
|
|
137
|
+
/>
|
|
138
|
+
))
|
|
139
|
+
)}
|
|
140
|
+
</div>
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
};
|