@kenyaemr/esm-patient-registration-app 8.0.1-pre.95 → 8.0.2

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 (134) hide show
  1. package/.turbo/turbo-build.log +23 -22
  2. package/dist/108.js +1 -0
  3. package/dist/108.js.map +1 -0
  4. package/dist/130.js +1 -1
  5. package/dist/130.js.LICENSE.txt +2 -0
  6. package/dist/130.js.map +1 -1
  7. package/dist/2.js +1 -0
  8. package/dist/2.js.map +1 -0
  9. package/dist/250.js +1 -0
  10. package/dist/250.js.map +1 -0
  11. package/dist/271.js +1 -1
  12. package/dist/319.js +1 -1
  13. package/dist/325.js +1 -0
  14. package/dist/325.js.map +1 -0
  15. package/dist/372.js +2 -0
  16. package/dist/372.js.map +1 -0
  17. package/dist/460.js +1 -1
  18. package/dist/574.js +1 -1
  19. package/dist/644.js +1 -1
  20. package/dist/66.js +1 -0
  21. package/dist/66.js.map +1 -0
  22. package/dist/662.js +1 -0
  23. package/dist/662.js.map +1 -0
  24. package/dist/757.js +1 -1
  25. package/dist/{59.js → 76.js} +1 -1
  26. package/dist/{59.js.map → 76.js.map} +1 -1
  27. package/dist/788.js +1 -1
  28. package/dist/807.js +1 -1
  29. package/dist/833.js +1 -1
  30. package/dist/895.js +2 -0
  31. package/dist/895.js.LICENSE.txt +34 -0
  32. package/dist/895.js.map +1 -0
  33. package/dist/kenyaemr-esm-patient-registration-app.js +1 -1
  34. package/dist/kenyaemr-esm-patient-registration-app.js.buildmanifest.json +161 -188
  35. package/dist/kenyaemr-esm-patient-registration-app.js.map +1 -1
  36. package/dist/main.js +1 -1
  37. package/dist/main.js.LICENSE.txt +10 -0
  38. package/dist/main.js.map +1 -1
  39. package/dist/routes.json +1 -1
  40. package/package-lock.json +6047 -0
  41. package/package.json +3 -4
  42. package/src/client-registry/client-registry.component.tsx +22 -0
  43. package/src/client-registry/hie-client-registry/hie-client-registry.component.tsx +134 -0
  44. package/src/client-registry/hie-client-registry/hie-client-registry.scss +53 -0
  45. package/src/client-registry/hie-client-registry/hie-resource.ts +162 -0
  46. package/src/client-registry/hie-client-registry/hie-types.ts +29 -0
  47. package/src/client-registry/hie-client-registry/modal/confirm-hie.modal.tsx +82 -0
  48. package/src/client-registry/hie-client-registry/modal/confirm-hie.scss +10 -0
  49. package/src/{patient-verification → client-registry/patient-verification}/patient-verification-hook.tsx +2 -2
  50. package/src/{patient-verification → client-registry/patient-verification}/patient-verification-utils.ts +1 -1
  51. package/src/{patient-verification → client-registry/patient-verification}/patient-verification.component.tsx +4 -1
  52. package/src/{patient-verification → client-registry/patient-verification}/patient-verification.scss +17 -1
  53. package/src/{patient-verification → client-registry/patient-verification}/verification-modal/empty-prompt.component.tsx +9 -6
  54. package/src/config-schema.ts +72 -2
  55. package/src/index.ts +6 -6
  56. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
  57. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +84 -0
  58. package/src/patient-registration/field/dob/dob.component.tsx +21 -7
  59. package/src/patient-registration/field/field.component.tsx +11 -5
  60. package/src/patient-registration/field/field.resource.ts +11 -4
  61. package/src/patient-registration/field/field.scss +44 -5
  62. package/src/patient-registration/field/gender/gender-field.component.tsx +2 -1
  63. package/src/patient-registration/field/gender/gender-field.test.tsx +1 -0
  64. package/src/patient-registration/field/id/id-field.component.tsx +8 -6
  65. package/src/patient-registration/field/id/id-field.test.tsx +27 -8
  66. package/src/patient-registration/field/name/name-field.component.tsx +5 -1
  67. package/src/patient-registration/field/obs/obs-field.component.tsx +1 -1
  68. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +1 -0
  69. package/src/patient-registration/field/person-attributes/custom-person-attribute-field.component.tsx +76 -27
  70. package/src/patient-registration/field/person-attributes/location-person-attribute-field.component.tsx +105 -0
  71. package/src/patient-registration/field/person-attributes/location-person-attribute-field.resource.tsx +48 -0
  72. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +12 -1
  73. package/src/patient-registration/field/person-attributes/useUpdateIdentifierRequirement.tsx +83 -0
  74. package/src/patient-registration/form-manager.test.ts +21 -0
  75. package/src/patient-registration/form-manager.ts +40 -20
  76. package/src/patient-registration/input/basic-input/input/input.component.tsx +5 -1
  77. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +18 -10
  78. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +166 -67
  79. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +3 -0
  80. package/src/patient-registration/input/input.scss +5 -0
  81. package/src/patient-registration/patient-registration-context.ts +4 -3
  82. package/src/patient-registration/patient-registration-hooks.ts +67 -9
  83. package/src/patient-registration/patient-registration-utils.ts +3 -7
  84. package/src/patient-registration/patient-registration.component.tsx +44 -30
  85. package/src/patient-registration/patient-registration.resource.ts +8 -0
  86. package/src/patient-registration/patient-registration.test.tsx +9 -3
  87. package/src/patient-registration/patient-registration.types.ts +4 -1
  88. package/src/patient-registration/section/death-info/death-info-section.component.tsx +22 -17
  89. package/src/patient-registration/section/death-info/death-info-section.test.tsx +4 -14
  90. package/src/patient-registration/section/section.component.tsx +1 -1
  91. package/src/patient-registration/section/section.scss +5 -0
  92. package/src/patient-registration/validation/{patient-registration-validation.test.tsx → patient-registration-validation.test.ts} +26 -4
  93. package/src/patient-registration/validation/patient-registration-validation.ts +126 -0
  94. package/src/routes.json +14 -17
  95. package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
  96. package/src/widgets/cancel-patient-edit.test.tsx +2 -3
  97. package/src/widgets/delete-identifier-confirmation.modal.tsx +22 -15
  98. package/src/widgets/delete-identifier-confirmation.test.tsx +2 -1
  99. package/translations/am.json +36 -25
  100. package/translations/ar.json +37 -26
  101. package/translations/en.json +51 -20
  102. package/translations/es.json +38 -26
  103. package/translations/fr.json +47 -35
  104. package/translations/he.json +37 -30
  105. package/translations/km.json +37 -30
  106. package/translations/zh.json +37 -20
  107. package/translations/zh_CN.json +37 -20
  108. package/dist/152.js +0 -1
  109. package/dist/152.js.map +0 -1
  110. package/dist/255.js +0 -2
  111. package/dist/255.js.map +0 -1
  112. package/dist/303.js +0 -1
  113. package/dist/303.js.map +0 -1
  114. package/dist/330.js +0 -1
  115. package/dist/330.js.map +0 -1
  116. package/dist/564.js +0 -1
  117. package/dist/564.js.map +0 -1
  118. package/dist/623.js +0 -1
  119. package/dist/623.js.map +0 -1
  120. package/dist/729.js +0 -1
  121. package/dist/729.js.map +0 -1
  122. package/dist/735.js +0 -1
  123. package/dist/735.js.map +0 -1
  124. package/dist/831.js +0 -2
  125. package/dist/831.js.LICENSE.txt +0 -14
  126. package/dist/831.js.map +0 -1
  127. package/src/patient-registration/validation/patient-registration-validation.tsx +0 -60
  128. package/src/widgets/cancel-patient-edit.component.tsx +0 -37
  129. package/src/widgets/delete-identifier-confirmation.scss +0 -34
  130. /package/dist/{255.js.LICENSE.txt → 372.js.LICENSE.txt} +0 -0
  131. /package/src/{patient-verification → client-registry/patient-verification}/assets/counties.json +0 -0
  132. /package/src/{patient-verification → client-registry/patient-verification}/assets/verification-assets.ts +0 -0
  133. /package/src/{patient-verification → client-registry/patient-verification}/verification-modal/confirm-prompt.component.tsx +0 -0
  134. /package/src/{patient-verification → client-registry/patient-verification}/verification-types.ts +0 -0
@@ -1,32 +1,34 @@
1
- import React, { useState, useEffect, useContext, useMemo, useRef } from 'react';
1
+ import React, { useContext, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import classNames from 'classnames';
3
3
  import { Button, Link, InlineLoading } from '@carbon/react';
4
4
  import { XAxis, ShareKnowledge } from '@carbon/react/icons';
5
5
  import { useLocation, useParams } from 'react-router-dom';
6
6
  import { useTranslation } from 'react-i18next';
7
- import { Formik, Form, type FormikHelpers } from 'formik';
7
+ import { Form, Formik, type FormikHelpers } from 'formik';
8
8
  import {
9
9
  createErrorHandler,
10
+ interpolateUrl,
10
11
  showSnackbar,
11
12
  useConfig,
12
- interpolateUrl,
13
13
  usePatient,
14
14
  usePatientPhoto,
15
+ useFeatureFlag,
15
16
  } from '@openmrs/esm-framework';
16
17
  import { getValidationSchema } from './validation/patient-registration-validation';
17
- import { type FormValues, type CapturePhotoProps } from './patient-registration.types';
18
+ import { type CapturePhotoProps, type FormValues } from './patient-registration.types';
18
19
  import { PatientRegistrationContext } from './patient-registration-context';
19
20
  import { type SavePatientForm, SavePatientTransactionManager } from './form-manager';
20
21
  import { DummyDataInput } from './input/dummy-data/dummy-data-input.component';
21
22
  import { cancelRegistration, filterOutUndefinedPatientIdentifiers, scrollIntoView } from './patient-registration-utils';
22
- import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap,usePatientObs } from './patient-registration-hooks';
23
+ import { useInitialAddressFieldValues, useInitialFormValues, usePatientUuidMap } from './patient-registration-hooks';
23
24
  import { ResourcesContext } from '../offline.resources';
24
25
  import { builtInSections, type RegistrationConfig, type SectionDefinition } from '../config-schema';
25
26
  import { SectionWrapper } from './section/section-wrapper.component';
26
27
  import BeforeSavePrompt from './before-save-prompt';
27
28
  import styles from './patient-registration.scss';
28
- import PatientVerification from '../patient-verification/patient-verification.component';
29
- import { handleSavePatientToClientRegistry } from '../patient-verification/patient-verification-hook';
29
+ import PatientVerification from '../client-registry/patient-verification/patient-verification.component';
30
+ import { handleSavePatientToClientRegistry } from '../client-registry/patient-verification/patient-verification-hook';
31
+ import ClientRegistry from '../client-registry/client-registry.component';
30
32
 
31
33
  let exportedInitialFormValuesForTesting = {} as FormValues;
32
34
 
@@ -36,6 +38,7 @@ export interface PatientRegistrationProps {
36
38
  }
37
39
 
38
40
  export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePatientForm, isOffline }) => {
41
+ const healthInformationExchangeFlag = useFeatureFlag('healthInformationExchange');
39
42
  const { currentSession, identifierTypes } = useContext(ResourcesContext);
40
43
  const { search } = useLocation();
41
44
  const config = useConfig() as RegistrationConfig;
@@ -158,16 +161,24 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
158
161
  }
159
162
  };
160
163
 
164
+ const getDescription = (errors) => {
165
+ return (
166
+ <ul style={{ listStyle: 'inside' }}>
167
+ {Object.keys(errors).map((error, index) => {
168
+ return <li key={index}>{t(`${error}LabelText`, error)}</li>;
169
+ })}
170
+ </ul>
171
+ );
172
+ };
173
+ const enableRegistryButton = healthInformationExchangeFlag ? false : !enableClientRegistry;
174
+
161
175
  const displayErrors = (errors) => {
162
176
  if (errors && typeof errors === 'object' && !!Object.keys(errors).length) {
163
- Object.keys(errors).forEach((error) => {
164
- showSnackbar({
165
- subtitle: t(`${error}LabelText`, error),
166
- title: t('incompleteForm', 'The following field has errors:'),
167
- kind: 'warning',
168
- isLowContrast: true,
169
- timeoutInMs: 5000,
170
- });
177
+ showSnackbar({
178
+ isLowContrast: true,
179
+ kind: 'warning',
180
+ title: t('fieldsWithErrors', 'The following fields have errors:'),
181
+ subtitle: <>{getDescription(errors)}</>,
171
182
  });
172
183
  }
173
184
  };
@@ -180,7 +191,7 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
180
191
  onSubmit={onFormSubmit}>
181
192
  {(props) => (
182
193
  <Form className={styles.form}>
183
- <BeforeSavePrompt when={props.dirty} redirect={target} />
194
+ <BeforeSavePrompt when={Object.keys(props.touched).length > 0} redirect={target} />
184
195
  <div className={styles.formContainer}>
185
196
  <div>
186
197
  <div className={styles.stickyColumn}>
@@ -198,18 +209,20 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
198
209
  </Link>
199
210
  </div>
200
211
  ))}
201
- <Button
202
- renderIcon={ShareKnowledge}
203
- disabled={!currentSession || !identifierTypes}
204
- onClick={() => {
205
- setEnableClientRegistry(true);
206
- props.isValid
207
- ? handleSavePatientToClientRegistry(props.values, props.setValues, inEditMode)
208
- : props.validateForm().then((errors) => displayErrors(errors));
209
- }}
210
- className={styles.submitButton}>
211
- {t('postToRegistry', 'Post to registry')}
212
- </Button>
212
+ {!healthInformationExchangeFlag && (
213
+ <Button
214
+ renderIcon={ShareKnowledge}
215
+ disabled={!currentSession || !identifierTypes}
216
+ onClick={() => {
217
+ setEnableClientRegistry(true);
218
+ props.isValid
219
+ ? handleSavePatientToClientRegistry(props.values, props.setValues, inEditMode)
220
+ : props.validateForm().then((errors) => displayErrors(errors));
221
+ }}
222
+ className={styles.submitButton}>
223
+ {t('postToRegistry', 'Post to registry')}
224
+ </Button>
225
+ )}
213
226
  <Button
214
227
  className={styles.submitButton}
215
228
  type="submit"
@@ -217,7 +230,7 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
217
230
  // Current session and identifiers are required for patient registration.
218
231
  // If currentSession or identifierTypes are not available, then the
219
232
  // user should be blocked to register the patient.
220
- disabled={!currentSession || !identifierTypes || props.isSubmitting || !enableClientRegistry}>
233
+ disabled={!currentSession || !identifierTypes || props.isSubmitting || enableRegistryButton}>
221
234
  {props.isSubmitting ? (
222
235
  <InlineLoading
223
236
  className={styles.spinner}
@@ -243,13 +256,14 @@ export const PatientRegistration: React.FC<PatientRegistrationProps> = ({ savePa
243
256
  values: props.values,
244
257
  inEditMode,
245
258
  setFieldValue: props.setFieldValue,
259
+ setFieldTouched: props.setFieldTouched,
246
260
  setCapturePhotoProps,
247
261
  currentPhoto: photo?.imageSrc,
248
262
  isOffline,
249
263
  initialFormValues: props.initialValues,
250
264
  setInitialFormValues,
251
265
  }}>
252
- <PatientVerification props={props} setInitialFormValues={setInitialFormValues} />
266
+ <ClientRegistry props={props} setInitialFormValues={setInitialFormValues} />
253
267
  {sections.map((section, index) => (
254
268
  <SectionWrapper
255
269
  key={`registration-section-${section.id}`}
@@ -1,5 +1,6 @@
1
1
  import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
2
  import { type Patient, type Relationship, type PatientIdentifier, type Encounter } from './patient-registration.types';
3
+ import dayjs from 'dayjs';
3
4
 
4
5
  export const uuidIdentifier = '05a29f94-c0ed-11e2-94be-8c13b969e334';
5
6
  export const uuidTelephoneNumber = '14d4f066-15f5-102d-96e4-000c29c2a5d7';
@@ -188,3 +189,10 @@ export async function deletePatientIdentifier(patientUuid: string, patientIdenti
188
189
  signal: abortController.signal,
189
190
  });
190
191
  }
192
+
193
+ export function getDatetime(date: Date | string, time: string, timeFormat: 'AM' | 'PM') {
194
+ const datetime = new Date(date);
195
+ const [hours, minutes] = time.split(':').map(Number);
196
+ const fullHours = timeFormat === 'PM' ? (hours % 12) + 12 : hours % 12;
197
+ return dayjs(datetime).hour(fullHours).minute(minutes).second(0).millisecond(0).toDate();
198
+ }
@@ -14,7 +14,7 @@ import {
14
14
  import { mockedAddressTemplate } from '__mocks__';
15
15
  import { mockPatient } from 'tools';
16
16
  import { saveEncounter, savePatient } from './patient-registration.resource';
17
- import { type RegistrationConfig, esmPatientRegistrationSchema } from '../config-schema';
17
+ import { esmPatientRegistrationSchema, type RegistrationConfig } from '../config-schema';
18
18
  import type { AddressTemplate, Encounter } from './patient-registration.types';
19
19
  import { ResourcesContext } from '../offline.resources';
20
20
  import { FormManager } from './form-manager';
@@ -167,6 +167,9 @@ let mockOpenmrsConfig: RegistrationConfig = {
167
167
  searchAddressByLevel: true,
168
168
  },
169
169
  },
170
+ causeOfDeath: {
171
+ conceptUuid: 'cause-of-death-concept-uuid',
172
+ },
170
173
  },
171
174
  links: {
172
175
  submitButton: '#',
@@ -407,7 +410,7 @@ describe('Updating an existing patient record', () => {
407
410
  const givenNameInput: HTMLInputElement = screen.getByLabelText(/First Name/);
408
411
  const familyNameInput: HTMLInputElement = screen.getByLabelText(/Family Name/);
409
412
  const middleNameInput: HTMLInputElement = screen.getByLabelText(/Middle Name/);
410
- const dateOfBirthInput: HTMLInputElement = screen.getByLabelText('Date of Birth');
413
+ const dateOfBirthInput: HTMLInputElement = screen.getByLabelText(/Date of Birth/i);
411
414
  const genderInput: HTMLInputElement = screen.getByLabelText(/Male/);
412
415
 
413
416
  // assert initial values
@@ -443,7 +446,10 @@ describe('Updating an existing patient record', () => {
443
446
  birthdate: new Date('1972-04-04T00:00:00.000Z'),
444
447
  birthdateEstimated: false,
445
448
  deathCause: '',
446
- deathDate: '',
449
+ nonCodedCauseOfDeath: '',
450
+ deathDate: undefined,
451
+ deathTime: undefined,
452
+ deathTimeFormat: 'AM',
447
453
  familyName: 'Smith',
448
454
  gender: expect.stringMatching(/male/i),
449
455
  givenName: 'Eric',
@@ -167,7 +167,9 @@ export interface FormValues {
167
167
  birthdate: Date | string;
168
168
  birthdateEstimated: boolean;
169
169
  deathCause: string;
170
- deathDate: string;
170
+ deathDate: string | Date;
171
+ deathTime: string;
172
+ deathTimeFormat: 'AM' | 'PM';
171
173
  familyName: string;
172
174
  gender: string;
173
175
  givenName: string;
@@ -177,6 +179,7 @@ export interface FormValues {
177
179
  isDead: boolean;
178
180
  middleName: string;
179
181
  monthsEstimated: number;
182
+ nonCodedCauseOfDeath: string;
180
183
  obs?: {
181
184
  [conceptUuid: string]: string;
182
185
  };
@@ -1,30 +1,35 @@
1
- import React from 'react';
2
- import classNames from 'classnames';
1
+ import React, { useContext } from 'react';
3
2
  import { useTranslation } from 'react-i18next';
4
- import { Input } from '../../input/basic-input/input/input.component';
5
- import { SelectInput } from '../../input/basic-input/select/select-input.component';
3
+ import { Checkbox, Layer } from '@carbon/react';
4
+ import { useField } from 'formik';
5
+ import { Field } from '../../field/field.component';
6
6
  import { PatientRegistrationContext } from '../../patient-registration-context';
7
7
  import styles from './../section.scss';
8
8
 
9
- export const DeathInfoSection = () => {
10
- const { values } = React.useContext(PatientRegistrationContext);
9
+ export interface DeathInfoSectionProps {
10
+ fields: Array<string>;
11
+ }
12
+
13
+ export const DeathInfoSection: React.FC<DeathInfoSectionProps> = ({ fields }) => {
11
14
  const { t } = useTranslation();
15
+ const { values, setFieldValue } = useContext(PatientRegistrationContext);
16
+ const [deathDate, deathDateMeta] = useField('deathDate');
17
+ const today = new Date();
12
18
 
13
19
  return (
14
20
  <section className={styles.formSection} aria-label="Death Info Section">
15
- <h5 className={classNames('omrs-type-title-5', styles.formSectionTitle)}>Death Info</h5>
16
21
  <section className={styles.fieldGroup}>
17
- <Input labelText={t('isDeadInputLabel', 'Is Dead')} name="isDead" id="isDead" />
18
- {values.isDead && (
19
- <>
20
- <Input labelText={t('deathDateInputLabel', 'Date of Death')} name="deathDate" id="deathDate" />
21
- <SelectInput
22
- options={[t('unknown', 'Unknown'), t('stroke', 'Stroke')]}
23
- label={t('causeOfDeathInputLabel', 'Cause of Death')}
24
- name="deathCause"
22
+ <Layer>
23
+ <div className={styles.isDeadFieldContainer}>
24
+ <Checkbox
25
+ checked={values.isDead}
26
+ id="isDead"
27
+ labelText={t('isDeadInputLabel', 'Is dead')}
28
+ onChange={(event, { checked, id }) => setFieldValue(id, checked)}
25
29
  />
26
- </>
27
- )}
30
+ </div>
31
+ </Layer>
32
+ {values.isDead ? fields.map((field) => <Field key={`death-info-${field}`} name={field} />) : null}
28
33
  </section>
29
34
  </section>
30
35
  );
@@ -1,10 +1,10 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
- import { Formik, Form } from 'formik';
3
+ import { Form, Formik } from 'formik';
4
4
  import { initialFormValues } from '../../patient-registration.component';
5
- import { DeathInfoSection } from './death-info-section.component';
6
5
  import { type FormValues } from '../../patient-registration.types';
7
6
  import { PatientRegistrationContext } from '../../patient-registration-context';
7
+ import { DeathInfoSection } from './death-info-section.component';
8
8
 
9
9
  const initialContextValues = {
10
10
  currentPhoto: 'data:image/png;base64,1234567890',
@@ -29,7 +29,7 @@ describe('Death info section', () => {
29
29
  <PatientRegistrationContext.Provider value={initialContextValues}>
30
30
  <Formik initialValues={initialFormValues} onSubmit={jest.fn()}>
31
31
  <Form>
32
- <DeathInfoSection />
32
+ <DeathInfoSection fields={[]} />
33
33
  </Form>
34
34
  </Formik>
35
35
  </PatientRegistrationContext.Provider>,
@@ -40,16 +40,6 @@ describe('Death info section', () => {
40
40
  renderDeathInfoSection(true);
41
41
 
42
42
  expect(screen.getByRole('region', { name: /death info section/i })).toBeInTheDocument();
43
- expect(screen.getByRole('heading', { name: /death info/i })).toBeInTheDocument();
44
- expect(screen.getByRole('textbox', { name: /is dead \(optional\)/i })).toBeInTheDocument();
45
- expect(screen.getByRole('textbox', { name: /date of death \(optional\)/i })).toBeInTheDocument();
46
- expect(screen.getByRole('combobox', { name: /cause of death \(optional\)/i })).toBeInTheDocument();
47
- });
48
-
49
- it('has the correct number of inputs if is dead is not checked', async () => {
50
- renderDeathInfoSection(false);
51
-
52
- expect(screen.queryByRole('textbox', { name: /date of death \(optional\)/i })).not.toBeInTheDocument();
53
- expect(screen.queryByRole('combobox', { name: /cause of death \(optional\)/i })).not.toBeInTheDocument();
43
+ expect(screen.getByRole('checkbox', { name: /is dead/i })).toBeInTheDocument();
54
44
  });
55
45
  });
@@ -14,7 +14,7 @@ export function Section({ sectionDefinition }: SectionProps) {
14
14
  case 'demographics':
15
15
  return <DemographicsSection fields={sectionDefinition.fields} />;
16
16
  case 'death':
17
- return <DeathInfoSection />;
17
+ return <DeathInfoSection fields={sectionDefinition.fields} />;
18
18
  case 'relationships':
19
19
  return <RelationshipsSection />;
20
20
  default: // includes 'contact'
@@ -1,3 +1,4 @@
1
+ @use '@carbon/colors';
1
2
  @use '@carbon/layout';
2
3
  @use '@carbon/type';
3
4
  @use '@openmrs/esm-styleguide/src/vars' as *;
@@ -14,3 +15,7 @@
14
15
  color: $ui-04;
15
16
  cursor: pointer;
16
17
  }
18
+
19
+ .isDeadFieldContainer {
20
+ margin-bottom: layout.$spacing-05;
21
+ }
@@ -1,6 +1,7 @@
1
1
  import { getConfig } from '@openmrs/esm-framework';
2
2
  import { type RegistrationConfig } from '../../config-schema';
3
3
  import { getValidationSchema } from './patient-registration-validation';
4
+ import dayjs from 'dayjs';
4
5
 
5
6
  const mockGetConfig = jest.mocked(getConfig);
6
7
 
@@ -35,6 +36,8 @@ describe('Patient registration validation', () => {
35
36
  additionalGivenName: '',
36
37
  birthdate: new Date('1990-01-01'),
37
38
  birthdateEstimated: false,
39
+ isDead: false,
40
+ causeOfDeath: null,
38
41
  deathDate: null,
39
42
  email: 'john.doe@example.com',
40
43
  familyName: 'Doe',
@@ -142,7 +145,7 @@ describe('Patient registration validation', () => {
142
145
  expect(validationError).toBeFalsy();
143
146
  });
144
147
 
145
- it('should throw error when date of birth is a future date', async () => {
148
+ it('should throw an error when date of birth is a future date', async () => {
146
149
  const invalidFormValues = {
147
150
  ...validFormValues,
148
151
  birthdate: new Date('2100-01-01'),
@@ -151,6 +154,15 @@ describe('Patient registration validation', () => {
151
154
  expect(validationError.errors).toContain('birthdayNotInTheFuture');
152
155
  });
153
156
 
157
+ it('should throw an error when date of birth is more than 140 years ago', async () => {
158
+ const invalidFormValues = {
159
+ ...validFormValues,
160
+ birthdate: dayjs().subtract(141, 'years').toDate(),
161
+ };
162
+ const validationError = await validateFormValues(invalidFormValues);
163
+ expect(validationError.errors).toContain('birthdayNotOver140YearsAgo');
164
+ });
165
+
154
166
  it('should require yearsEstimated when birthdateEstimated is true', async () => {
155
167
  const invalidFormValues = {
156
168
  ...validFormValues,
@@ -160,7 +172,7 @@ describe('Patient registration validation', () => {
160
172
  expect(validationError.errors).toContain('yearsEstimateRequired');
161
173
  });
162
174
 
163
- it('should throw error when monthEstimated is negative', async () => {
175
+ it('should throw an error when monthEstimated is negative', async () => {
164
176
  const invalidFormValues = {
165
177
  ...validFormValues,
166
178
  birthdateEstimated: true,
@@ -171,12 +183,22 @@ describe('Patient registration validation', () => {
171
183
  expect(validationError.errors).toContain('negativeMonths');
172
184
  });
173
185
 
174
- it('should throw error when deathDate is in future', async () => {
186
+ it('should throw an error when yearsEstimated is more than 140', async () => {
187
+ const invalidFormValues = {
188
+ ...validFormValues,
189
+ birthdateEstimated: true,
190
+ yearsEstimated: 141,
191
+ };
192
+ const validationError = await validateFormValues(invalidFormValues);
193
+ expect(validationError.errors).toContain('nonsensicalYears');
194
+ });
195
+
196
+ it('should throw an error when deathDate is in future', async () => {
175
197
  const invalidFormValues = {
176
198
  ...validFormValues,
177
199
  deathDate: new Date('2100-01-01'),
178
200
  };
179
201
  const validationError = await validateFormValues(invalidFormValues);
180
- expect(validationError.errors).toContain('deathdayNotInTheFuture');
202
+ expect(validationError.errors).toContain('deathDateInFuture');
181
203
  });
182
204
  });
@@ -0,0 +1,126 @@
1
+ import dayjs from 'dayjs';
2
+ import * as Yup from 'yup';
3
+ import mapValues from 'lodash/mapValues';
4
+ import { translateFrom } from '@openmrs/esm-framework';
5
+ import { type RegistrationConfig } from '../../config-schema';
6
+ import { type FormValues } from '../patient-registration.types';
7
+ import { getDatetime } from '../patient-registration.resource';
8
+
9
+ const t = (key: string, value: string) => translateFrom('@openmrs/esm-framework', key, value);
10
+
11
+ export function getValidationSchema(config: RegistrationConfig) {
12
+ return Yup.object({
13
+ givenName: Yup.string().required(t('givenNameRequired', 'Given name is required')),
14
+ familyName: Yup.string().required(t('familyNameRequired', 'Family name is required')),
15
+ additionalGivenName: Yup.string().when('addNameInLocalLanguage', {
16
+ is: true,
17
+ then: Yup.string().required(t('givenNameRequired', 'Given name is required')),
18
+ otherwise: Yup.string().notRequired(),
19
+ }),
20
+ additionalFamilyName: Yup.string().when('addNameInLocalLanguage', {
21
+ is: true,
22
+ then: Yup.string().required(t('familyNameRequired', 'Family name is required')),
23
+ otherwise: Yup.string().notRequired(),
24
+ }),
25
+ gender: Yup.string()
26
+ .oneOf(
27
+ config.fieldConfigurations.gender.map((g) => g.value),
28
+ t('genderUnspecified', 'Gender unspecified'),
29
+ )
30
+ .required(t('genderRequired', 'Gender is required')),
31
+ birthdate: Yup.date().when('birthdateEstimated', {
32
+ is: false,
33
+ then: Yup.date()
34
+ .required(t('birthdayRequired', 'Birthday is required'))
35
+ .max(Date(), t('birthdayNotInTheFuture', 'Birthday cannot be in future'))
36
+ .min(
37
+ dayjs().subtract(140, 'years').toDate(),
38
+ t('birthdayNotOver140YearsAgo', 'Birthday cannot be more than 140 years ago'),
39
+ )
40
+ .nullable(),
41
+ otherwise: Yup.date().nullable(),
42
+ }),
43
+ yearsEstimated: Yup.number().when('birthdateEstimated', {
44
+ is: true,
45
+ then: Yup.number()
46
+ .required(t('yearsEstimateRequired', 'Estimated years required'))
47
+ .min(0, t('negativeYears', 'Estimated years cannot be negative'))
48
+ .max(140, t('nonsensicalYears', 'Estimated years cannot be more than 140')),
49
+ otherwise: Yup.number().nullable(),
50
+ }),
51
+ monthsEstimated: Yup.number().min(0, t('negativeMonths', 'Estimated months cannot be negative')),
52
+ isDead: Yup.boolean(),
53
+ deathDate: Yup.date()
54
+ .when('isDead', {
55
+ is: true,
56
+ then: Yup.date().required(t('deathDateRequired', 'Death date is required')),
57
+ otherwise: Yup.date().nullable(),
58
+ })
59
+ .max(new Date(), 'deathDateInFuture')
60
+ .test(
61
+ 'deathDate-after-birthdate',
62
+ t('deathdayInvalidDate', 'Death date and time cannot be before the birthday'),
63
+ function (value) {
64
+ const { birthdate } = this.parent;
65
+ if (birthdate && value) {
66
+ return dayjs(value).isAfter(birthdate);
67
+ }
68
+ return true;
69
+ },
70
+ )
71
+ .test('deathDate-before-today', t('deathDateInFuture', 'Death date cannot be in future'), function (value) {
72
+ const { deathTime, deathTimeFormat } = this.parent;
73
+ if (value && deathTime && deathTimeFormat && /^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/.test(deathTime)) {
74
+ return dayjs(getDatetime(value, deathTime, deathTimeFormat)).isBefore(dayjs());
75
+ }
76
+ return true;
77
+ }),
78
+ deathTime: Yup.string()
79
+ .when('isDead', {
80
+ is: true,
81
+ then: Yup.string().required(t('deathTimeRequired', 'Death time is required')),
82
+ otherwise: Yup.string().nullable(),
83
+ })
84
+ .matches(/^(1[0-2]|0?[1-9]):([0-5]?[0-9])$/, t('deathTimeInvalid', "Time doesn't match the format 'hh:mm'")),
85
+
86
+ deathTimeFormat: Yup.string()
87
+ .when('isDead', {
88
+ is: true,
89
+ then: Yup.string().required(t('deathTimeFormatRequired', 'Time format is required')),
90
+ otherwise: Yup.string().nullable(),
91
+ })
92
+ .oneOf(['AM', 'PM'], t('deathTimeFormatInvalid', 'Time format is invalid')),
93
+
94
+ deathCause: Yup.string().when('isDead', {
95
+ is: true,
96
+ then: Yup.string().required(t('deathCauseRequired', 'Cause of death is required')),
97
+ otherwise: Yup.string().nullable(),
98
+ }),
99
+ nonCodedCauseOfDeath: Yup.string().when(['isDead', 'deathCause'], {
100
+ is: (isDead, deathCause) => isDead && deathCause === config.freeTextFieldConceptUuid,
101
+ then: Yup.string().required(t('nonCodedCauseOfDeathRequired', 'Cause of death is required')),
102
+ otherwise: Yup.string().nullable(),
103
+ }),
104
+ email: Yup.string().optional().email(t('invalidEmail', 'Invalid email')),
105
+ identifiers: Yup.lazy((obj: FormValues['identifiers']) =>
106
+ Yup.object(
107
+ mapValues(obj, () =>
108
+ Yup.object({
109
+ required: Yup.bool(),
110
+ identifierValue: Yup.string().when('required', {
111
+ is: true,
112
+ then: Yup.string().required(t('identifierValueRequired', 'Identifier value is required')),
113
+ otherwise: Yup.string().notRequired(),
114
+ }),
115
+ }),
116
+ ),
117
+ ),
118
+ ),
119
+ relationships: Yup.array().of(
120
+ Yup.object().shape({
121
+ relatedPersonUuid: Yup.string().required(),
122
+ relationshipType: Yup.string().required(),
123
+ }),
124
+ ),
125
+ });
126
+ }
package/src/routes.json CHANGED
@@ -25,12 +25,6 @@
25
25
  "online": true,
26
26
  "offline": true
27
27
  },
28
- {
29
- "component": "cancelPatientEditModal",
30
- "name": "cancel-patient-edit-modal",
31
- "online": true,
32
- "offline": true
33
- },
34
28
  {
35
29
  "component": "patientPhotoExtension",
36
30
  "name": "patient-photo-widget",
@@ -51,25 +45,28 @@
51
45
  "slot": "patient-search-actions-slot",
52
46
  "online": true,
53
47
  "offline": true
48
+ }
49
+ ],
50
+ "modals": [
51
+ {
52
+ "name": "cancel-patient-edit-modal",
53
+ "component": "cancelPatientEditModal"
54
54
  },
55
55
  {
56
- "component": "deleteIdentifierConfirmationModal",
57
56
  "name": "delete-identifier-confirmation-modal",
58
- "online": true,
59
- "offline": true
57
+ "component": "deleteIdentifierConfirmationModal"
60
58
  },
61
59
  {
62
60
  "component": "emptyClientRegistryModal",
63
- "name": "empty-client-registry-modal",
64
- "online": true,
65
- "offline": true
61
+ "name": "empty-client-registry-modal"
66
62
  },
67
63
  {
68
64
  "component": "confirmClientRegistryModal",
69
- "name": "confirm-client-registry-modal",
70
- "online": true,
71
- "offline": true
65
+ "name": "confirm-client-registry-modal"
66
+ },
67
+ {
68
+ "component": "hieConfirmationModal",
69
+ "name": "hie-confirmation-modal"
72
70
  }
73
71
  ]
74
- }
75
-
72
+ }
@@ -0,0 +1,33 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button, ModalBody, ModalFooter, ModalHeader } from '@carbon/react';
4
+
5
+ interface CancelPatientEditPropsModal {
6
+ close(): void;
7
+ onConfirm(): void;
8
+ }
9
+
10
+ const CancelPatientEditModal: React.FC<CancelPatientEditPropsModal> = ({ close, onConfirm }) => {
11
+ const { t } = useTranslation();
12
+ return (
13
+ <>
14
+ <ModalHeader
15
+ closeModal={close}
16
+ title={t('confirmDiscardChangesTitle', 'Are you sure you want to discard these changes?')}
17
+ />
18
+ <ModalBody>
19
+ <p>{t('confirmDiscardChangesBody', 'Your unsaved changes will be lost if you proceed to discard the form')}.</p>
20
+ </ModalBody>
21
+ <ModalFooter>
22
+ <Button kind="secondary" onClick={close}>
23
+ {t('cancel', 'Cancel')}
24
+ </Button>
25
+ <Button kind="danger" onClick={onConfirm}>
26
+ {t('discard', 'Discard')}
27
+ </Button>
28
+ </ModalFooter>
29
+ </>
30
+ );
31
+ };
32
+
33
+ export default CancelPatientEditModal;
@@ -1,15 +1,14 @@
1
1
  import React from 'react';
2
2
  import userEvent from '@testing-library/user-event';
3
3
  import { screen, render } from '@testing-library/react';
4
- import CancelPatientEdit from './cancel-patient-edit.component';
4
+ import CancelPatientEdit from './cancel-patient-edit.modal';
5
5
 
6
- describe('CancelPatientEdit component', () => {
6
+ describe('CancelPatientEdit modal', () => {
7
7
  const mockClose = jest.fn();
8
8
  const mockOnConfirm = jest.fn();
9
9
 
10
10
  it('renders the modal and triggers close and onConfirm functions', async () => {
11
11
  const user = userEvent.setup();
12
-
13
12
  render(<CancelPatientEdit close={mockClose} onConfirm={mockOnConfirm} />);
14
13
 
15
14
  const cancelButton = screen.getByRole('button', { name: /Cancel/i });