@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.
Files changed (176) hide show
  1. package/.turbo/turbo-build.log +41 -0
  2. package/README.md +7 -0
  3. package/dist/130.js +2 -0
  4. package/dist/130.js.LICENSE.txt +3 -0
  5. package/dist/130.js.map +1 -0
  6. package/dist/152.js +1 -0
  7. package/dist/152.js.map +1 -0
  8. package/dist/249.js +2 -0
  9. package/dist/249.js.LICENSE.txt +46 -0
  10. package/dist/249.js.map +1 -0
  11. package/dist/255.js +2 -0
  12. package/dist/255.js.LICENSE.txt +9 -0
  13. package/dist/255.js.map +1 -0
  14. package/dist/271.js +1 -0
  15. package/dist/303.js +1 -0
  16. package/dist/303.js.map +1 -0
  17. package/dist/319.js +1 -0
  18. package/dist/365.js +1 -0
  19. package/dist/365.js.map +1 -0
  20. package/dist/460.js +1 -0
  21. package/dist/525.js +1 -0
  22. package/dist/525.js.map +1 -0
  23. package/dist/537.js +1 -0
  24. package/dist/537.js.map +1 -0
  25. package/dist/574.js +1 -0
  26. package/dist/591.js +2 -0
  27. package/dist/591.js.LICENSE.txt +32 -0
  28. package/dist/591.js.map +1 -0
  29. package/dist/621.js +1 -0
  30. package/dist/621.js.map +1 -0
  31. package/dist/644.js +1 -0
  32. package/dist/729.js +1 -0
  33. package/dist/729.js.map +1 -0
  34. package/dist/735.js +1 -0
  35. package/dist/735.js.map +1 -0
  36. package/dist/757.js +1 -0
  37. package/dist/784.js +2 -0
  38. package/dist/784.js.LICENSE.txt +9 -0
  39. package/dist/784.js.map +1 -0
  40. package/dist/788.js +1 -0
  41. package/dist/807.js +1 -0
  42. package/dist/833.js +1 -0
  43. package/dist/879.js +1 -0
  44. package/dist/879.js.map +1 -0
  45. package/dist/ampath-esm-patient-registration-app.js +1 -0
  46. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +649 -0
  47. package/dist/ampath-esm-patient-registration-app.js.map +1 -0
  48. package/dist/main.js +2 -0
  49. package/dist/main.js.LICENSE.txt +56 -0
  50. package/dist/main.js.map +1 -0
  51. package/dist/routes.json +1 -0
  52. package/docs/images/patient-registration-hierarchy.png +0 -0
  53. package/jest.config.js +3 -0
  54. package/package.json +61 -0
  55. package/src/add-patient-link.scss +3 -0
  56. package/src/add-patient-link.test.tsx +20 -0
  57. package/src/add-patient-link.tsx +21 -0
  58. package/src/config-schema.ts +410 -0
  59. package/src/constants.ts +14 -0
  60. package/src/declarations.d.ts +6 -0
  61. package/src/index.ts +71 -0
  62. package/src/nav-link.test.tsx +13 -0
  63. package/src/nav-link.tsx +10 -0
  64. package/src/offline.resources.ts +155 -0
  65. package/src/offline.ts +91 -0
  66. package/src/patient-registration/before-save-prompt.tsx +73 -0
  67. package/src/patient-registration/date-util.ts +52 -0
  68. package/src/patient-registration/field/__mocks__/field.resource.ts +60 -0
  69. package/src/patient-registration/field/address/address-field.component.tsx +153 -0
  70. package/src/patient-registration/field/address/address-hierarchy-levels.component.tsx +73 -0
  71. package/src/patient-registration/field/address/address-hierarchy.resource.tsx +157 -0
  72. package/src/patient-registration/field/address/address-search.component.tsx +85 -0
  73. package/src/patient-registration/field/address/address-search.scss +53 -0
  74. package/src/patient-registration/field/address/custom-address-field.component.tsx +31 -0
  75. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +214 -0
  76. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +135 -0
  77. package/src/patient-registration/field/custom-field.component.tsx +25 -0
  78. package/src/patient-registration/field/dob/dob.component.tsx +159 -0
  79. package/src/patient-registration/field/dob/dob.test.tsx +75 -0
  80. package/src/patient-registration/field/field.component.tsx +47 -0
  81. package/src/patient-registration/field/field.resource.ts +35 -0
  82. package/src/patient-registration/field/field.scss +127 -0
  83. package/src/patient-registration/field/field.test.tsx +294 -0
  84. package/src/patient-registration/field/gender/gender-field.component.tsx +49 -0
  85. package/src/patient-registration/field/gender/gender-field.test.tsx +59 -0
  86. package/src/patient-registration/field/id/id-field.component.tsx +144 -0
  87. package/src/patient-registration/field/id/id-field.test.tsx +107 -0
  88. package/src/patient-registration/field/id/identifier-selection-overlay.component.tsx +198 -0
  89. package/src/patient-registration/field/id/identifier-selection.scss +37 -0
  90. package/src/patient-registration/field/name/name-field.component.tsx +142 -0
  91. package/src/patient-registration/field/obs/obs-field.component.tsx +204 -0
  92. package/src/patient-registration/field/obs/obs-field.test.tsx +205 -0
  93. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +60 -0
  94. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +116 -0
  95. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +127 -0
  96. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +88 -0
  97. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +187 -0
  98. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +20 -0
  99. package/src/patient-registration/field/person-attributes/text-person-attribute-field.component.tsx +58 -0
  100. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +88 -0
  101. package/src/patient-registration/field/phone/phone-field.component.tsx +16 -0
  102. package/src/patient-registration/form-manager.test.ts +67 -0
  103. package/src/patient-registration/form-manager.ts +414 -0
  104. package/src/patient-registration/input/basic-input/input/input.component.tsx +179 -0
  105. package/src/patient-registration/input/basic-input/input/input.test.tsx +72 -0
  106. package/src/patient-registration/input/basic-input/select/select-input.component.tsx +32 -0
  107. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +49 -0
  108. package/src/patient-registration/input/combo-input/combo-input.component.tsx +128 -0
  109. package/src/patient-registration/input/combo-input/selection-tick.component.tsx +20 -0
  110. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.component.tsx +187 -0
  111. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.scss +62 -0
  112. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +132 -0
  113. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +156 -0
  114. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +107 -0
  115. package/src/patient-registration/input/custom-input/identifier/utils.test.ts +81 -0
  116. package/src/patient-registration/input/custom-input/identifier/utils.ts +19 -0
  117. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +53 -0
  118. package/src/patient-registration/input/dummy-data/dummy-data-input.test.tsx +43 -0
  119. package/src/patient-registration/input/input.scss +118 -0
  120. package/src/patient-registration/patient-registration-context.ts +24 -0
  121. package/src/patient-registration/patient-registration-hooks.ts +287 -0
  122. package/src/patient-registration/patient-registration-utils.ts +216 -0
  123. package/src/patient-registration/patient-registration.component.tsx +240 -0
  124. package/src/patient-registration/patient-registration.resource.test.tsx +26 -0
  125. package/src/patient-registration/patient-registration.resource.ts +250 -0
  126. package/src/patient-registration/patient-registration.scss +122 -0
  127. package/src/patient-registration/patient-registration.test.tsx +471 -0
  128. package/src/patient-registration/patient-registration.types.ts +318 -0
  129. package/src/patient-registration/section/death-info/death-info-section.component.tsx +31 -0
  130. package/src/patient-registration/section/death-info/death-info-section.test.tsx +64 -0
  131. package/src/patient-registration/section/demographics/demographics-section.component.tsx +30 -0
  132. package/src/patient-registration/section/demographics/demographics-section.test.tsx +83 -0
  133. package/src/patient-registration/section/generic-section.component.tsx +17 -0
  134. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +235 -0
  135. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +100 -0
  136. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +78 -0
  137. package/src/patient-registration/section/patient-relationships/relationships.scss +35 -0
  138. package/src/patient-registration/section/section-wrapper.component.tsx +40 -0
  139. package/src/patient-registration/section/section.component.tsx +23 -0
  140. package/src/patient-registration/section/section.scss +1 -0
  141. package/src/patient-registration/ui-components/overlay/overlay.component.tsx +51 -0
  142. package/src/patient-registration/ui-components/overlay/overlay.scss +63 -0
  143. package/src/patient-registration/validation/patient-registration-validation.test.tsx +157 -0
  144. package/src/patient-registration/validation/patient-registration-validation.tsx +60 -0
  145. package/src/patient-verification/client-registry-constants.ts +13 -0
  146. package/src/patient-verification/client-registry.component.tsx +66 -0
  147. package/src/patient-verification/client-registry.scss +1 -0
  148. package/src/patient-verification/utils.tsx +56 -0
  149. package/src/patient-verification/verification-modal.scss +20 -0
  150. package/src/patient-verification/verification.component.tsx +48 -0
  151. package/src/resource.ts +12 -0
  152. package/src/root.component.tsx +63 -0
  153. package/src/root.scss +7 -0
  154. package/src/root.test.tsx +32 -0
  155. package/src/routes.json +66 -0
  156. package/src/widgets/cancel-patient-edit.component.tsx +37 -0
  157. package/src/widgets/cancel-patient-edit.test.tsx +27 -0
  158. package/src/widgets/delete-identifier-confirmation-modal.test.tsx +34 -0
  159. package/src/widgets/delete-identifier-confirmation-modal.tsx +41 -0
  160. package/src/widgets/delete-identifier-modal.scss +34 -0
  161. package/src/widgets/display-photo.component.tsx +30 -0
  162. package/src/widgets/display-photo.test.tsx +37 -0
  163. package/src/widgets/edit-patient-details-button.component.tsx +34 -0
  164. package/src/widgets/edit-patient-details-button.scss +3 -0
  165. package/src/widgets/edit-patient-details-button.test.tsx +41 -0
  166. package/translations/am.json +97 -0
  167. package/translations/ar.json +97 -0
  168. package/translations/en.json +103 -0
  169. package/translations/es.json +97 -0
  170. package/translations/fr.json +97 -0
  171. package/translations/he.json +97 -0
  172. package/translations/km.json +97 -0
  173. package/translations/zh.json +89 -0
  174. package/translations/zh_CN.json +89 -0
  175. package/tsconfig.json +5 -0
  176. package/webpack.config.js +1 -0
@@ -0,0 +1,240 @@
1
+ import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Button, Link, InlineLoading, Dropdown } from '@carbon/react';
4
+ import { XAxis } from '@carbon/react/icons';
5
+ import { useLocation, useParams } from 'react-router-dom';
6
+ import { useTranslation } from 'react-i18next';
7
+ import { Formik, Form, type FormikHelpers } from 'formik';
8
+ import {
9
+ createErrorHandler,
10
+ showSnackbar,
11
+ useConfig,
12
+ interpolateUrl,
13
+ usePatient,
14
+ showModal,
15
+ } from '@openmrs/esm-framework';
16
+ import { getValidationSchema } from './validation/patient-registration-validation';
17
+ import { type FormValues, type CapturePhotoProps } from './patient-registration.types';
18
+ import { PatientRegistrationContext } from './patient-registration-context';
19
+ import { type SavePatientForm, SavePatientTransactionManager } from './form-manager';
20
+ import { fetchPatientRecordFromClientRegistry, usePatientPhoto, fetchPerson } from './patient-registration.resource';
21
+ import { DummyDataInput } from './input/dummy-data/dummy-data-input.component';
22
+ import { cancelRegistration, filterUndefinedPatientIdenfier, scrollIntoView } from './patient-registration-utils';
23
+ import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap } from './patient-registration-hooks';
24
+ import { ResourcesContext } from '../offline.resources';
25
+ import { builtInSections, type RegistrationConfig, type SectionDefinition } from '../config-schema';
26
+ import { SectionWrapper } from './section/section-wrapper.component';
27
+ import BeforeSavePrompt from './before-save-prompt';
28
+ import styles from './patient-registration.scss';
29
+ import { TextInput } from '@carbon/react';
30
+ import { ClientRegistry } from '../patient-verification/client-registry.component';
31
+
32
+ let exportedInitialFormValuesForTesting = {} as FormValues;
33
+
34
+ export interface PatientRegistrationProps {
35
+ savePatientForm: SavePatientForm;
36
+ isOffline: boolean;
37
+ }
38
+
39
+ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePatientForm, isOffline }) => {
40
+ const { currentSession, addressTemplate, identifierTypes } = useContext(ResourcesContext);
41
+ const { search } = useLocation();
42
+ const config = useConfig() as RegistrationConfig;
43
+ const [target, setTarget] = useState<undefined | string>();
44
+ const { patientUuid: uuidOfPatientToEdit } = useParams();
45
+ const { isLoading: isLoadingPatientToEdit, patient: patientToEdit } = usePatient(uuidOfPatientToEdit);
46
+ const { t } = useTranslation();
47
+ const [capturePhotoProps, setCapturePhotoProps] = useState<CapturePhotoProps | null>(null);
48
+ const [initialFormValues, setInitialFormValues] = useInitialFormValues(uuidOfPatientToEdit);
49
+ const [initialAddressFieldValues] = useInitialAddressFieldValues(uuidOfPatientToEdit);
50
+ const [patientUuidMap] = usePatientUuidMap(uuidOfPatientToEdit);
51
+ const location = currentSession?.sessionLocation?.uuid;
52
+ const inEditMode = isLoadingPatientToEdit ? undefined : !!(uuidOfPatientToEdit && patientToEdit);
53
+ const showDummyData = useMemo(() => localStorage.getItem('openmrs:devtools') === 'true' && !inEditMode, [inEditMode]);
54
+ const { data: photo } = usePatientPhoto(patientToEdit?.id);
55
+ const savePatientTransactionManager = useRef(new SavePatientTransactionManager());
56
+ const fieldDefinition = config?.fieldDefinitions?.filter((def) => def.type === 'address');
57
+ const validationSchema = getValidationSchema(config);
58
+
59
+ useEffect(() => {
60
+ exportedInitialFormValuesForTesting = initialFormValues;
61
+ }, [initialFormValues]);
62
+
63
+ const sections: Array<SectionDefinition> = useMemo(() => {
64
+ return config.sections
65
+ .map(
66
+ (sectionName) =>
67
+ config.sectionDefinitions.filter((s) => s.id == sectionName)[0] ??
68
+ builtInSections.filter((s) => s.id == sectionName)[0],
69
+ )
70
+ .filter((s) => s);
71
+ }, [config.sections, config.sectionDefinitions]);
72
+
73
+ const onFormSubmit = async (values: FormValues, helpers: FormikHelpers<FormValues>) => {
74
+ const abortController = new AbortController();
75
+ helpers.setSubmitting(true);
76
+
77
+ const updatedFormValues = { ...values, identifiers: filterUndefinedPatientIdenfier(values.identifiers) };
78
+ try {
79
+ await savePatientForm(
80
+ !inEditMode,
81
+ updatedFormValues,
82
+ patientUuidMap,
83
+ initialAddressFieldValues,
84
+ capturePhotoProps,
85
+ location,
86
+ initialFormValues['identifiers'],
87
+ currentSession,
88
+ config,
89
+ savePatientTransactionManager.current,
90
+ abortController,
91
+ );
92
+
93
+ showSnackbar({
94
+ subtitle: inEditMode
95
+ ? t('updatePatientSuccessSnackbarSubtitle', "The patient's information has been successfully updated")
96
+ : t(
97
+ 'registerPatientSuccessSnackbarSubtitle',
98
+ 'The patient can now be found by searching for them using their name or ID number',
99
+ ),
100
+ title: inEditMode
101
+ ? t('updatePatientSuccessSnackbarTitle', 'Patient Details Updated')
102
+ : t('registerPatientSuccessSnackbarTitle', 'New Patient Created'),
103
+ kind: 'success',
104
+ isLowContrast: true,
105
+ });
106
+
107
+ const afterUrl = new URLSearchParams(search).get('afterUrl');
108
+ const redirectUrl = interpolateUrl(afterUrl || config.links.submitButton, { patientUuid: values.patientUuid });
109
+
110
+ setTarget(redirectUrl);
111
+ } catch (error) {
112
+ if (error.responseBody?.error?.globalErrors) {
113
+ error.responseBody.error.globalErrors.forEach((error) => {
114
+ showSnackbar({
115
+ title: inEditMode
116
+ ? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
117
+ : t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
118
+ subtitle: error.message,
119
+ kind: 'error',
120
+ });
121
+ });
122
+ } else if (error.responseBody?.error?.message) {
123
+ showSnackbar({
124
+ title: inEditMode
125
+ ? t('updatePatientErrorSnackbarTitle', 'Patient Details Update Failed')
126
+ : t('registrationErrorSnackbarTitle', 'Patient Registration Failed'),
127
+ subtitle: error.responseBody.error.message,
128
+ kind: 'error',
129
+ });
130
+ } else {
131
+ createErrorHandler()(error);
132
+ }
133
+
134
+ helpers.setSubmitting(false);
135
+ }
136
+ };
137
+
138
+ const displayErrors = (errors) => {
139
+ if (errors && typeof errors === 'object' && !!Object.keys(errors).length) {
140
+ Object.keys(errors).forEach((error) => {
141
+ showSnackbar({
142
+ subtitle: t(`${error}LabelText`, error),
143
+ title: t('incompleteForm', 'The following field has errors:'),
144
+ kind: 'warning',
145
+ isLowContrast: true,
146
+ timeoutInMs: 5000,
147
+ });
148
+ });
149
+ }
150
+ };
151
+
152
+ return (
153
+ <Formik
154
+ enableReinitialize
155
+ initialValues={initialFormValues}
156
+ validationSchema={validationSchema}
157
+ onSubmit={onFormSubmit}>
158
+ {(props) => (
159
+ <Form className={styles.form}>
160
+ <BeforeSavePrompt when={props.dirty} redirect={target} />
161
+ <div className={styles.formContainer}>
162
+ <div>
163
+ <div className={styles.stickyColumn}>
164
+ <h4>
165
+ {inEditMode ? t('edit', 'Edit') : t('createNew', 'Create New')} {t('patient', 'Patient')}
166
+ </h4>
167
+ {showDummyData && <DummyDataInput setValues={props.setValues} />}
168
+ <p className={styles.label01}>{t('jumpTo', 'Jump to')}</p>
169
+ {sections.map((section) => (
170
+ <div className={classNames(styles.space05, styles.touchTarget)} key={section.name}>
171
+ <Link className={styles.linkName} onClick={() => scrollIntoView(section.id)}>
172
+ <XAxis size={16} /> {t(`${section.id}Section`, section.name)}
173
+ </Link>
174
+ </div>
175
+ ))}
176
+ <Button
177
+ className={styles.submitButton}
178
+ type="submit"
179
+ onClick={() => props.validateForm().then((errors) => displayErrors(errors))}
180
+ // Current session and identifiers are required for patient registration.
181
+ // If currentSession or identifierTypes are not available, then the
182
+ // user should be blocked to register the patient.
183
+ disabled={!currentSession || !identifierTypes || props.isSubmitting}>
184
+ {props.isSubmitting ? (
185
+ <InlineLoading
186
+ className={styles.spinner}
187
+ description={`${t('submitting', 'Submitting')} ...`}
188
+ iconDescription="submitting"
189
+ status="active"
190
+ />
191
+ ) : inEditMode ? (
192
+ t('updatePatient', 'Update Patient')
193
+ ) : (
194
+ t('registerPatient', 'Register Patient')
195
+ )}
196
+ </Button>
197
+ <Button className={styles.cancelButton} kind="tertiary" onClick={cancelRegistration}>
198
+ {t('cancel', 'Cancel')}
199
+ </Button>
200
+ </div>
201
+ </div>
202
+ <div className={styles.infoGrid}>
203
+ <PatientRegistrationContext.Provider
204
+ value={{
205
+ identifierTypes: identifierTypes,
206
+ validationSchema,
207
+ values: props.values,
208
+ inEditMode,
209
+ setFieldValue: props.setFieldValue,
210
+ setCapturePhotoProps,
211
+ currentPhoto: photo?.imageSrc,
212
+ isOffline,
213
+ initialFormValues: props.initialValues,
214
+ setInitialFormValues,
215
+ }}>
216
+ <ClientRegistry initialFormValues={initialFormValues} setInitialFormValues={setInitialFormValues} />
217
+ {sections.map((section, index) => (
218
+ <SectionWrapper
219
+ key={`registration-section-${section.id}`}
220
+ sectionDefinition={section}
221
+ index={index}
222
+ />
223
+ ))}
224
+ </PatientRegistrationContext.Provider>
225
+ </div>
226
+ </div>
227
+ </Form>
228
+ )}
229
+ </Formik>
230
+ );
231
+ };
232
+
233
+ /**
234
+ * @internal
235
+ * Just exported for testing
236
+ */
237
+ export { exportedInitialFormValuesForTesting as initialFormValues };
238
+ function dispose() {
239
+ throw new Error('Function not implemented.');
240
+ }
@@ -0,0 +1,26 @@
1
+ import { openmrsFetch } from '@openmrs/esm-framework';
2
+ import { savePatient } from './patient-registration.resource';
3
+
4
+ const mockOpenmrsFetch = openmrsFetch as jest.Mock;
5
+
6
+ jest.mock('@openmrs/esm-framework', () => ({
7
+ openmrsFetch: jest.fn(),
8
+ }));
9
+
10
+ describe('savePatient', () => {
11
+ afterEach(() => {
12
+ jest.resetAllMocks();
13
+ });
14
+
15
+ it('appends patient uuid in url if provided', () => {
16
+ mockOpenmrsFetch.mockImplementationOnce((url) => url);
17
+ savePatient(null, '1234');
18
+ expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual('/ws/rest/v1/patient/1234');
19
+ });
20
+
21
+ it('does not append patient uuid in url', () => {
22
+ mockOpenmrsFetch.mockImplementationOnce(() => {});
23
+ savePatient(null);
24
+ expect(mockOpenmrsFetch.mock.calls[0][0]).toEqual('/ws/rest/v1/patient/');
25
+ });
26
+ });
@@ -0,0 +1,250 @@
1
+ import useSWR from 'swr';
2
+ import { openmrsFetch, useConfig } from '@openmrs/esm-framework';
3
+ import { type Patient, type Relationship, type PatientIdentifier, type Encounter } from './patient-registration.types';
4
+
5
+ export const uuidIdentifier = '05a29f94-c0ed-11e2-94be-8c13b969e334';
6
+ export const uuidTelephoneNumber = '14d4f066-15f5-102d-96e4-000c29c2a5d7';
7
+
8
+ function dataURItoFile(dataURI: string) {
9
+ const byteString = atob(dataURI.split(',')[1]);
10
+ const mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
11
+ // write the bytes of the string to a typed array
12
+ const buffer = new Uint8Array(byteString.length);
13
+
14
+ for (let i = 0; i < byteString.length; i++) {
15
+ buffer[i] = byteString.charCodeAt(i);
16
+ }
17
+
18
+ const blob = new Blob([buffer], { type: mimeString });
19
+ return new File([blob], 'patient-photo.png');
20
+ }
21
+
22
+ export function savePatient(patient: Patient | null, updatePatientUuid?: string) {
23
+ const abortController = new AbortController();
24
+
25
+ return openmrsFetch(`/ws/rest/v1/patient/${updatePatientUuid || ''}`, {
26
+ headers: {
27
+ 'Content-Type': 'application/json',
28
+ },
29
+ method: 'POST',
30
+ body: patient,
31
+ signal: abortController.signal,
32
+ });
33
+ }
34
+
35
+ export function saveEncounter(encounter: Encounter) {
36
+ const abortController = new AbortController();
37
+
38
+ return openmrsFetch(`/ws/rest/v1/encounter`, {
39
+ headers: {
40
+ 'Content-Type': 'application/json',
41
+ },
42
+ method: 'POST',
43
+ body: encounter,
44
+ signal: abortController.signal,
45
+ });
46
+ }
47
+
48
+ export function generateIdentifier(source: string) {
49
+ const abortController = new AbortController();
50
+
51
+ return openmrsFetch(`/ws/rest/v1/idgen/identifiersource/${source}/identifier`, {
52
+ headers: {
53
+ 'Content-Type': 'application/json',
54
+ },
55
+ method: 'POST',
56
+ body: {},
57
+ signal: abortController.signal,
58
+ });
59
+ }
60
+
61
+ export function deletePersonName(nameUuid: string, personUuid: string) {
62
+ const abortController = new AbortController();
63
+
64
+ return openmrsFetch(`/ws/rest/v1/person/${personUuid}/name/${nameUuid}`, {
65
+ method: 'DELETE',
66
+ signal: abortController.signal,
67
+ });
68
+ }
69
+
70
+ export function saveRelationship(relationship: Relationship) {
71
+ const abortController = new AbortController();
72
+
73
+ return openmrsFetch('/ws/rest/v1/relationship', {
74
+ headers: {
75
+ 'Content-Type': 'application/json',
76
+ },
77
+ method: 'POST',
78
+ body: relationship,
79
+ signal: abortController.signal,
80
+ });
81
+ }
82
+
83
+ export function updateRelationship(relationshipUuid, relationship: { relationshipType: string }) {
84
+ const abortController = new AbortController();
85
+
86
+ return openmrsFetch(`/ws/rest/v1/relationship/${relationshipUuid}`, {
87
+ headers: {
88
+ 'Content-Type': 'application/json',
89
+ },
90
+ method: 'POST',
91
+ body: { relationshipType: relationship.relationshipType },
92
+ signal: abortController.signal,
93
+ });
94
+ }
95
+
96
+ export function deleteRelationship(relationshipUuid) {
97
+ const abortController = new AbortController();
98
+
99
+ return openmrsFetch(`/ws/rest/v1/relationship/${relationshipUuid}`, {
100
+ headers: {
101
+ 'Content-Type': 'application/json',
102
+ },
103
+ method: 'DELETE',
104
+ signal: abortController.signal,
105
+ });
106
+ }
107
+
108
+ export async function savePatientPhoto(
109
+ patientUuid: string,
110
+ content: string,
111
+ url: string,
112
+ date: string,
113
+ conceptUuid: string,
114
+ ) {
115
+ const abortController = new AbortController();
116
+
117
+ const formData = new FormData();
118
+ formData.append('patient', patientUuid);
119
+ formData.append('file', dataURItoFile(content));
120
+ formData.append(
121
+ 'json',
122
+ JSON.stringify({
123
+ person: patientUuid,
124
+ concept: conceptUuid,
125
+ groupMembers: [],
126
+ obsDatetime: date,
127
+ }),
128
+ );
129
+
130
+ return openmrsFetch(url, {
131
+ method: 'POST',
132
+ signal: abortController.signal,
133
+ body: formData,
134
+ });
135
+ }
136
+
137
+ interface ObsFetchResponse {
138
+ results: Array<PhotoObs>;
139
+ }
140
+
141
+ interface PhotoObs {
142
+ display: string;
143
+ obsDatetime: string;
144
+ uuid: string;
145
+ value: {
146
+ display: string;
147
+ links: {
148
+ rel: string;
149
+ uri: string;
150
+ };
151
+ };
152
+ }
153
+
154
+ interface UsePatientPhotoResult {
155
+ data: { dateTime: string; imageSrc: string } | null;
156
+ isError: Error;
157
+ isLoading: boolean;
158
+ }
159
+
160
+ export function usePatientPhoto(patientUuid: string): UsePatientPhotoResult {
161
+ const {
162
+ concepts: { patientPhotoUuid },
163
+ } = useConfig();
164
+ const url = `/ws/rest/v1/obs?patient=${patientUuid}&concept=${patientPhotoUuid}&v=full`;
165
+
166
+ const { data, error, isLoading } = useSWR<{ data: ObsFetchResponse }, Error>(patientUuid ? url : null, openmrsFetch);
167
+
168
+ const item = data?.data?.results[0];
169
+
170
+ return {
171
+ data: item
172
+ ? {
173
+ dateTime: item?.obsDatetime,
174
+ imageSrc: item?.value?.links?.uri,
175
+ }
176
+ : null,
177
+ isError: error,
178
+ isLoading,
179
+ };
180
+ }
181
+
182
+ export async function fetchPerson(query: string, abortController: AbortController) {
183
+ const [patientsRes, personsRes] = await Promise.all([
184
+ openmrsFetch(`/ws/rest/v1/patient?q=${query}`, {
185
+ signal: abortController.signal,
186
+ }),
187
+ openmrsFetch(`/ws/rest/v1/person?q=${query}`, {
188
+ signal: abortController.signal,
189
+ }),
190
+ ]);
191
+
192
+ const results = [...patientsRes.data.results];
193
+
194
+ personsRes.data.results.forEach((person) => {
195
+ if (!results.some((patient) => patient.uuid === person.uuid)) {
196
+ results.push(person);
197
+ }
198
+ });
199
+
200
+ return results;
201
+ }
202
+
203
+ export async function addPatientIdentifier(patientUuid: string, patientIdentifier: PatientIdentifier) {
204
+ const abortController = new AbortController();
205
+ return openmrsFetch(`/ws/rest/v1/patient/${patientUuid}/identifier/`, {
206
+ method: 'POST',
207
+ headers: {
208
+ 'Content-Type': 'application/json',
209
+ },
210
+ signal: abortController.signal,
211
+ body: patientIdentifier,
212
+ });
213
+ }
214
+
215
+ export async function updatePatientIdentifier(patientUuid: string, identifierUuid: string, identifier: string) {
216
+ const abortController = new AbortController();
217
+ return openmrsFetch(`/ws/rest/v1/patient/${patientUuid}/identifier/${identifierUuid}`, {
218
+ method: 'POST',
219
+ headers: {
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ signal: abortController.signal,
223
+ body: { identifier },
224
+ });
225
+ }
226
+
227
+ export async function deletePatientIdentifier(patientUuid: string, patientIdentifierUuid: string) {
228
+ const abortController = new AbortController();
229
+ return openmrsFetch(`/ws/rest/v1/patient/${patientUuid}/identifier/${patientIdentifierUuid}?purge`, {
230
+ method: 'DELETE',
231
+ headers: {
232
+ 'Content-Type': 'application/json',
233
+ },
234
+ signal: abortController.signal,
235
+ });
236
+ }
237
+ export const fetchPatientRecordFromClientRegistry = (
238
+ patientIdentifier: string,
239
+ identifierType: string,
240
+ country: string,
241
+ ) => {
242
+ const url = `
243
+ https://ngx.ampath.or.ke/registry/api/uno?uno=${patientIdentifier}&idType=${identifierType}&countryCode=${country}`;
244
+
245
+ return fetch(url)
246
+ .then((response) => {
247
+ return response.json();
248
+ })
249
+ .then((data) => data);
250
+ };
@@ -0,0 +1,122 @@
1
+ @use '@carbon/styles/scss/spacing';
2
+ @use '@carbon/styles/scss/type';
3
+ @import '~@openmrs/esm-styleguide/src/vars';
4
+
5
+ .title {
6
+ color: var(--omrs-color-brand-teal);
7
+ }
8
+
9
+ .submit {
10
+ width: 250px;
11
+ }
12
+
13
+ .submit:hover {
14
+ cursor: pointer;
15
+ }
16
+
17
+ .cancelButton {
18
+ width: 11.688rem;
19
+ }
20
+
21
+ .submitButton {
22
+ margin-bottom: spacing.$spacing-05;
23
+ width: 11.688rem;
24
+ display: block;
25
+ }
26
+
27
+ .infoGrid {
28
+ width: 100%;
29
+ padding-left: spacing.$spacing-07;
30
+ margin-bottom: 40vh;
31
+ margin-top: spacing.$spacing-05;
32
+ max-width: 50rem;
33
+ }
34
+
35
+ .label01 {
36
+ @include type.type-style('label-01');
37
+ margin-top: spacing.$spacing-05;
38
+ margin-bottom: spacing.$spacing-05;
39
+ color: $ui-04;
40
+ }
41
+
42
+ .productiveHeading02 {
43
+ @include type.type-style('heading-compact-02');
44
+ color: $ui-04;
45
+ cursor: pointer;
46
+ }
47
+
48
+ .space05 {
49
+ margin: spacing.$spacing-05 0 spacing.$spacing-05 0;
50
+ }
51
+
52
+ .formContainer {
53
+ display: flex;
54
+ width: 100%;
55
+ }
56
+
57
+ .stickyColumn {
58
+ position: sticky;
59
+ margin-top: spacing.$spacing-05;
60
+ // 3rem for the nav height and 1rem for top margin
61
+ top: 4rem;
62
+ }
63
+
64
+ .touchTarget a:active {
65
+ color: $color-gray-100;
66
+ }
67
+
68
+ .linkName {
69
+ color: $color-gray-70;
70
+ line-height: 1.38;
71
+ font-size: spacing.$spacing-05;
72
+ font-weight: 600;
73
+
74
+ &:active {
75
+ text-decoration: none;
76
+ color: $color-gray-100;
77
+ }
78
+
79
+ &:hover {
80
+ text-decoration: none;
81
+ color: $color-gray-100;
82
+ cursor: pointer;
83
+ }
84
+ }
85
+
86
+ .main {
87
+ background-color: white;
88
+ }
89
+
90
+ :global(.omrs-breakpoint-lt-desktop) {
91
+ .infoGrid {
92
+ max-width: unset;
93
+ }
94
+ }
95
+
96
+ .spinner {
97
+ &:global(.cds--inline-loading) {
98
+ min-height: 1rem;
99
+ }
100
+ }
101
+
102
+ // Overriding styles for RTL support
103
+ html[dir='rtl'] {
104
+ .linkName {
105
+ & > svg {
106
+ transform: scale(-1, 1);
107
+ }
108
+ }
109
+
110
+ .infoGrid {
111
+ padding-left: unset;
112
+ padding-right: spacing.$spacing-07;
113
+ }
114
+ }
115
+ .patientVerification {
116
+ & > * {
117
+ margin-top: spacing.$spacing-03;
118
+ }
119
+ & > :last-child {
120
+ margin-top: spacing.$spacing-03;
121
+ }
122
+ }