@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.
- package/README.md +4 -0
- package/dist/117.js +1 -0
- package/dist/117.js.map +1 -0
- package/dist/131.js +1 -0
- package/dist/131.js.map +1 -0
- package/dist/132.js +1 -0
- package/dist/132.js.map +1 -0
- package/dist/152.js +1 -0
- package/dist/152.js.map +1 -0
- package/dist/209.js +1 -0
- package/dist/209.js.map +1 -0
- package/dist/247.js +1 -0
- package/dist/247.js.map +1 -0
- package/dist/28.js +1 -0
- package/dist/28.js.map +1 -0
- package/dist/317.js +1 -0
- package/dist/317.js.map +1 -0
- package/dist/349.js +1 -0
- package/dist/349.js.map +1 -0
- package/dist/371.js +1 -0
- package/dist/371.js.map +1 -0
- package/dist/378.js +1 -0
- package/dist/378.js.map +1 -0
- package/dist/442.js +1 -0
- package/dist/442.js.map +1 -0
- package/dist/45.js +1 -0
- package/dist/45.js.map +1 -0
- package/dist/459.js +12 -0
- package/dist/459.js.map +1 -0
- package/dist/466.js +1 -0
- package/dist/466.js.map +1 -0
- package/dist/508.js +1 -0
- package/dist/508.js.map +1 -0
- package/dist/61.js +1 -0
- package/dist/61.js.map +1 -0
- package/dist/640.js +1 -0
- package/dist/640.js.map +1 -0
- package/dist/689.js +1 -0
- package/dist/689.js.map +1 -0
- package/dist/709.js +1 -0
- package/dist/709.js.map +1 -0
- package/dist/712.js +1 -0
- package/dist/712.js.map +1 -0
- package/dist/720.js +1 -0
- package/dist/720.js.map +1 -0
- package/dist/742.js +1 -0
- package/dist/742.js.map +1 -0
- package/dist/771.js +1 -0
- package/dist/771.js.map +1 -0
- package/dist/806.js +1 -0
- package/dist/806.js.map +1 -0
- package/dist/912.js +1 -0
- package/dist/912.js.map +1 -0
- package/dist/913.js +1 -0
- package/dist/913.js.map +1 -0
- package/dist/940.js +1 -0
- package/dist/940.js.map +1 -0
- package/dist/957.js +15 -0
- package/dist/957.js.map +1 -0
- package/dist/digihmis-esm-patient-registration-app.js +6 -0
- package/dist/digihmis-esm-patient-registration-app.js.buildmanifest.json +827 -0
- package/dist/digihmis-esm-patient-registration-app.js.map +1 -0
- package/dist/main.js +17 -0
- package/dist/main.js.map +1 -0
- package/dist/routes.json +1 -0
- package/jest.config.js +26 -0
- package/package.json +51 -0
- package/rspack.config.js +1 -0
- package/src/active-visit-table/active-visit-table.component.tsx +240 -0
- package/src/active-visit-table/active-visits.resource.ts +338 -0
- package/src/active-visit-table/active-visits.scss +211 -0
- package/src/active-visit-table/empty-data-illustration.component.tsx +39 -0
- package/src/checkin-form/base-visit-type.component.tsx +102 -0
- package/src/checkin-form/base-visit-type.scss +75 -0
- package/src/checkin-form/checkin-form.scss +167 -0
- package/src/checkin-form/checkin-form.workspace.tsx +703 -0
- package/src/checkin-form/hooks/useDefaultFacilityLocation.ts +15 -0
- package/src/checkin-form/hooks/useDefaultVisitLocation.ts +24 -0
- package/src/checkin-form/hooks/useOfflineVisitType.ts +18 -0
- package/src/checkin-form/hooks/usePatientHistory.ts +40 -0
- package/src/checkin-form/hooks/useRecommendedVisitTypes.ts +34 -0
- package/src/checkin-form/hooks/useVisitAttributeType.ts +102 -0
- package/src/checkin-form/location-selector.component.tsx +86 -0
- package/src/checkin-form/recommended-visit-type.component.tsx +32 -0
- package/src/checkin-form/visit-attribute-type.component.tsx +256 -0
- package/src/checkin-form/visit-attribute-type.scss +5 -0
- package/src/checkin-form/visit-date-time.component.tsx +201 -0
- package/src/checkin-form/visit-form.resource.ts +308 -0
- package/src/components/custom-card/compact-patient-banner.component.tsx +70 -0
- package/src/components/custom-card/compact-patient-banner.scss +34 -0
- package/src/components/custom-card/custom-loader.component.tsx +22 -0
- package/src/components/custom-card/patient-search-context.ts +30 -0
- package/src/components/custom-input/autosuggest/autosuggest.component.tsx +213 -0
- package/src/components/custom-input/autosuggest/autosuggest.scss +158 -0
- package/src/components/custom-input/autosuggest/autosuggest.test.tsx +168 -0
- package/src/components/custom-patient-banner/custom-patient-banner.component.tsx +39 -0
- package/src/components/custom-patient-banner/custom-patient-banner.resource.ts +32 -0
- package/src/components/custom-patient-banner/custom-patient-banner.scss +26 -0
- package/src/config-schema.ts +511 -0
- package/src/constant.ts +6 -0
- package/src/constants/index.ts +1 -0
- package/src/dashboard.meta.ts +8 -0
- package/src/declarations.d.ts +6 -0
- package/src/empty-svg/empty-svg.component.tsx +565 -0
- package/src/icon-wrapper/icon-wrapper.extension.tsx +17 -0
- package/src/identifier-workspace/identifier-configure-form.workspace.tsx +278 -0
- package/src/identifier-workspace/identifier-configure.scss +106 -0
- package/src/identifier-workspace/identifier.resource.ts +90 -0
- package/src/index.ts +33 -0
- package/src/left-panel/left-panel.component.tsx +54 -0
- package/src/left-panel/left-panel.scss +13 -0
- package/src/page-header/page-header.component.tsx +35 -0
- package/src/page-header/page-header.scss +76 -0
- package/src/patient-tags/deceased-patient-tag.extension.tsx +41 -0
- package/src/patient-tags/deceased-patient-tag.scss +15 -0
- package/src/patient-tags/patient-tags.resource.ts +30 -0
- package/src/patient-tags/visit-tag.extension.tsx +43 -0
- package/src/patient-tags/visit-tag.scss +19 -0
- package/src/registration-form/form-manager.ts +449 -0
- package/src/registration-form/registration-form.component.tsx +1565 -0
- package/src/registration-form/registration-form.resource.ts +264 -0
- package/src/registration-form/registration-form.scss +282 -0
- package/src/routes.json +62 -0
- package/src/search-bar/search-bar.component.tsx +285 -0
- package/src/search-bar/search-bar.resource.ts +274 -0
- package/src/search-bar/search-bar.scss +427 -0
- package/src/search-dashboard/search-dashboard.component.tsx +17 -0
- package/src/search-dashboard/search-dashboard.scss +13 -0
- package/src/setup-tests.ts +1 -0
- package/src/type/index.ts +463 -0
- package/src/utils/fhir-mapper.ts +73 -0
- package/src/visit-summary/visit-summary.component.tsx +32 -0
- package/src/visit-summary/visit-summary.resource.ts +56 -0
- package/src/visit-summary/visit-summary.scss +74 -0
- package/translations/am.json +112 -0
- package/translations/ar.json +120 -0
- package/translations/en.json +112 -0
- package/translations/es.json +114 -0
- package/translations/fr.json +114 -0
- package/translations/he.json +114 -0
- package/translations/km.json +111 -0
- package/translations/pt.json +114 -0
- package/translations/sw.json +112 -0
- package/translations/vi.json +111 -0
- package/translations/zh.json +111 -0
- 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,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
|
+
}
|