@kenyaemr/esm-patient-registration-app 8.1.1-pre.114 → 8.1.1-pre.116

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 (85) hide show
  1. package/.turbo/turbo-build.log +20 -20
  2. package/dist/108.js +1 -1
  3. package/dist/130.js +1 -1
  4. package/dist/130.js.map +1 -1
  5. package/dist/250.js +1 -0
  6. package/dist/250.js.map +1 -0
  7. package/dist/271.js +1 -1
  8. package/dist/319.js +1 -1
  9. package/dist/460.js +1 -1
  10. package/dist/471.js +1 -0
  11. package/dist/471.js.map +1 -0
  12. package/dist/574.js +1 -1
  13. package/dist/644.js +1 -1
  14. package/dist/662.js +1 -0
  15. package/dist/662.js.map +1 -0
  16. package/dist/757.js +1 -1
  17. package/dist/76.js +1 -1
  18. package/dist/788.js +1 -1
  19. package/dist/807.js +1 -1
  20. package/dist/833.js +1 -1
  21. package/dist/895.js +2 -0
  22. package/dist/895.js.map +1 -0
  23. package/dist/kenyaemr-esm-patient-registration-app.js +1 -1
  24. package/dist/kenyaemr-esm-patient-registration-app.js.buildmanifest.json +115 -115
  25. package/dist/kenyaemr-esm-patient-registration-app.js.map +1 -1
  26. package/dist/main.js +1 -1
  27. package/dist/main.js.map +1 -1
  28. package/dist/routes.json +1 -1
  29. package/package.json +2 -2
  30. package/src/config-schema.ts +28 -2
  31. package/src/index.ts +1 -4
  32. package/src/patient-registration/field/cause-of-death/cause-of-death.component.tsx +98 -0
  33. package/src/patient-registration/field/date-and-time-of-death/date-and-time-of-death.component.tsx +84 -0
  34. package/src/patient-registration/field/dob/dob.component.tsx +21 -7
  35. package/src/patient-registration/field/field.component.tsx +11 -5
  36. package/src/patient-registration/field/field.resource.ts +11 -4
  37. package/src/patient-registration/field/field.scss +23 -1
  38. package/src/patient-registration/field/gender/gender-field.component.tsx +2 -1
  39. package/src/patient-registration/field/gender/gender-field.test.tsx +1 -0
  40. package/src/patient-registration/field/name/name-field.component.tsx +5 -1
  41. package/src/patient-registration/form-manager.test.ts +3 -0
  42. package/src/patient-registration/form-manager.ts +30 -15
  43. package/src/patient-registration/input/basic-input/input/input.component.tsx +5 -1
  44. package/src/patient-registration/input/custom-input/identifier/identifier-input.component.tsx +2 -2
  45. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +122 -71
  46. package/src/patient-registration/input/dummy-data/dummy-data-input.component.tsx +3 -0
  47. package/src/patient-registration/patient-registration-context.ts +4 -3
  48. package/src/patient-registration/patient-registration-hooks.ts +54 -5
  49. package/src/patient-registration/patient-registration-utils.ts +3 -7
  50. package/src/patient-registration/patient-registration.component.tsx +20 -13
  51. package/src/patient-registration/patient-registration.resource.ts +8 -0
  52. package/src/patient-registration/patient-registration.test.tsx +9 -3
  53. package/src/patient-registration/patient-registration.types.ts +4 -1
  54. package/src/patient-registration/section/death-info/death-info-section.component.tsx +22 -17
  55. package/src/patient-registration/section/death-info/death-info-section.test.tsx +4 -14
  56. package/src/patient-registration/section/section.component.tsx +1 -1
  57. package/src/patient-registration/section/section.scss +5 -0
  58. package/src/patient-registration/validation/{patient-registration-validation.test.tsx → patient-registration-validation.test.ts} +26 -4
  59. package/src/patient-registration/validation/patient-registration-validation.ts +126 -0
  60. package/src/routes.json +10 -18
  61. package/src/widgets/cancel-patient-edit.modal.tsx +33 -0
  62. package/src/widgets/cancel-patient-edit.test.tsx +2 -3
  63. package/src/widgets/delete-identifier-confirmation.modal.tsx +22 -15
  64. package/src/widgets/delete-identifier-confirmation.test.tsx +2 -1
  65. package/translations/am.json +36 -25
  66. package/translations/ar.json +37 -26
  67. package/translations/en.json +37 -20
  68. package/translations/es.json +38 -26
  69. package/translations/fr.json +47 -35
  70. package/translations/he.json +37 -30
  71. package/translations/km.json +37 -30
  72. package/translations/zh.json +37 -20
  73. package/translations/zh_CN.json +37 -20
  74. package/dist/623.js +0 -1
  75. package/dist/623.js.map +0 -1
  76. package/dist/709.js +0 -2
  77. package/dist/709.js.map +0 -1
  78. package/dist/735.js +0 -1
  79. package/dist/735.js.map +0 -1
  80. package/dist/830.js +0 -1
  81. package/dist/830.js.map +0 -1
  82. package/src/patient-registration/validation/patient-registration-validation.tsx +0 -60
  83. package/src/widgets/cancel-patient-edit.component.tsx +0 -37
  84. package/src/widgets/delete-identifier-confirmation.scss +0 -34
  85. /package/dist/{709.js.LICENSE.txt → 895.js.LICENSE.txt} +0 -0
package/dist/routes.json CHANGED
@@ -1 +1 @@
1
- {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.24.0"},"pages":[{"component":"root","route":"patient-registration","online":true,"offline":true},{"component":"editPatient","routeRegex":"patient\\/([a-zA-Z0-9\\-]+)\\/edit","online":true,"offline":true}],"extensions":[{"component":"addPatientLink","name":"add-patient-action","slot":"top-nav-actions-slot","online":true,"offline":true},{"component":"cancelPatientEditModal","name":"cancel-patient-edit-modal","online":true,"offline":true},{"component":"patientPhotoExtension","name":"patient-photo-widget","slot":"patient-photo-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-actions-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-search-actions-slot","online":true,"offline":true},{"component":"deleteIdentifierConfirmationModal","name":"delete-identifier-confirmation-modal","online":true,"offline":true},{"component":"emptyClientRegistryModal","name":"empty-client-registry-modal","online":true,"offline":true},{"component":"confirmClientRegistryModal","name":"confirm-client-registry-modal","online":true,"offline":true}],"modals":[{"component":"hieConfirmationModal","name":"hie-confirmation-modal"}],"version":"8.1.1-pre.114"}
1
+ {"$schema":"https://json.openmrs.org/routes.schema.json","backendDependencies":{"webservices.rest":"^2.24.0"},"pages":[{"component":"root","route":"patient-registration","online":true,"offline":true},{"component":"editPatient","routeRegex":"patient\\/([a-zA-Z0-9\\-]+)\\/edit","online":true,"offline":true}],"extensions":[{"component":"addPatientLink","name":"add-patient-action","slot":"top-nav-actions-slot","online":true,"offline":true},{"component":"patientPhotoExtension","name":"patient-photo-widget","slot":"patient-photo-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-actions-slot","online":true,"offline":true},{"component":"editPatientDetailsButton","name":"edit-patient-details-button","slot":"patient-search-actions-slot","online":true,"offline":true}],"modals":[{"name":"cancel-patient-edit-modal","component":"cancelPatientEditModal"},{"name":"delete-identifier-confirmation-modal","component":"deleteIdentifierConfirmationModal"},{"component":"emptyClientRegistryModal","name":"empty-client-registry-modal"},{"component":"confirmClientRegistryModal","name":"confirm-client-registry-modal"},{"component":"hieConfirmationModal","name":"hie-confirmation-modal"}],"version":"8.1.1-pre.116"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-patient-registration-app",
3
- "version": "8.1.1-pre.114",
3
+ "version": "8.1.1-pre.116",
4
4
  "description": "Patient registration microfrontend for the OpenMRS SPA",
5
5
  "browser": "dist/kenyaemr-esm-patient-registration-app.js",
6
6
  "main": "src/index.ts",
@@ -18,7 +18,7 @@
18
18
  "test:watch": "cross-env TZ=UTC jest --watch --config jest.config.js --color",
19
19
  "coverage": "yarn test --coverage",
20
20
  "typescript": "tsc",
21
- "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.extension.tsx' 'src/**/*modal.tsx' 'src/**/*.workspace.tsx' 'src/index.ts' --config ../../tools/i18next-parser.config.js"
21
+ "extract-translations": "i18next 'src/**/*.component.tsx' 'src/**/*.extension.tsx' 'src/**/*modal.tsx' 'src/**/*.workspace.tsx' 'src/index.ts' 'src/patient-registration/validation/patient-registration-validation.ts' --config ../../tools/i18next-parser.config.js"
22
22
  },
23
23
  "browserslist": [
24
24
  "extends browserslist-config-openmrs"
@@ -42,6 +42,10 @@ export interface RegistrationConfig {
42
42
  sectionDefinitions: Array<SectionDefinition>;
43
43
  fieldDefinitions: Array<FieldDefinition>;
44
44
  fieldConfigurations: {
45
+ causeOfDeath: {
46
+ conceptUuid: string;
47
+ required?: boolean;
48
+ };
45
49
  name: {
46
50
  displayMiddleName: boolean;
47
51
  allowUnidentifiedPatients: boolean;
@@ -83,6 +87,7 @@ export interface RegistrationConfig {
83
87
  encounterProviderRoleUuid: string;
84
88
  registrationFormUuid: string | null;
85
89
  };
90
+ freeTextFieldConceptUuid: string;
86
91
  hieClientRegistry: {
87
92
  identifierTypes: Array<{ identifierType: string; identifierValue: string }>;
88
93
  baseUrl: string;
@@ -97,12 +102,21 @@ export const builtInSections: Array<SectionDefinition> = [
97
102
  fields: ['name', 'gender', 'dob', 'id'],
98
103
  },
99
104
  { id: 'contact', name: 'Contact Details', fields: ['address', 'phone'] },
100
- { id: 'death', name: 'Death Info', fields: [] },
105
+ { id: 'death', name: 'Death Info', fields: ['dateAndTimeOfDeath', 'causeOfDeath'] },
101
106
  { id: 'relationships', name: 'Relationships', fields: [] },
102
107
  ];
103
108
 
104
109
  // These fields are handled specially in field.component.tsx
105
- export const builtInFields = ['name', 'gender', 'dob', 'id', 'address', 'phone'] as const;
110
+ export const builtInFields = [
111
+ 'name',
112
+ 'gender',
113
+ 'dob',
114
+ 'id',
115
+ 'address',
116
+ 'phone',
117
+ 'causeOfDeath',
118
+ 'dateAndTimeOfDeath',
119
+ ] as const;
106
120
 
107
121
  export const esmPatientRegistrationSchema = {
108
122
  sections: {
@@ -209,6 +223,14 @@ export const esmPatientRegistrationSchema = {
209
223
  'Definitions for custom fields that can be used in sectionDefinitions. Can also be used to override built-in fields.',
210
224
  },
211
225
  fieldConfigurations: {
226
+ causeOfDeath: {
227
+ conceptUuid: {
228
+ _type: Type.ConceptUuid,
229
+ _description: 'The concept UUID to get cause of death answers',
230
+ _default: '9272a14b-7260-4353-9e5b-5787b5dead9d',
231
+ },
232
+ required: { _type: Type.Boolean, _default: false },
233
+ },
212
234
  name: {
213
235
  displayMiddleName: { _type: Type.Boolean, _default: true },
214
236
  allowUnidentifiedPatients: {
@@ -369,6 +391,10 @@ export const esmPatientRegistrationSchema = {
369
391
  'The form UUID to associate with the registration encounter. By default no form will be associated.',
370
392
  },
371
393
  },
394
+ freeTextFieldConceptUuid: {
395
+ _default: '5622AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
396
+ _type: Type.ConceptUuid,
397
+ },
372
398
  hieClientRegistry: {
373
399
  identifierTypes: {
374
400
  _type: Type.Array,
package/src/index.ts CHANGED
@@ -51,10 +51,7 @@ export const editPatient = getSyncLifecycle(rootComponent, {
51
51
 
52
52
  export const addPatientLink = getSyncLifecycle(addPatientLinkComponent, options);
53
53
 
54
- export const cancelPatientEditModal = getAsyncLifecycle(
55
- () => import('./widgets/cancel-patient-edit.component'),
56
- options,
57
- );
54
+ export const cancelPatientEditModal = getAsyncLifecycle(() => import('./widgets/cancel-patient-edit.modal'), options);
58
55
 
59
56
  export const patientPhotoExtension = getSyncLifecycle(PatientPhotoExtension, options);
60
57
 
@@ -0,0 +1,98 @@
1
+ import React, { useMemo } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Field, useField } from 'formik';
4
+ import { useTranslation } from 'react-i18next';
5
+ import { InlineNotification, Layer, Select, SelectItem, SelectSkeleton, TextInput } from '@carbon/react';
6
+ import { useConfig } from '@openmrs/esm-framework';
7
+ import { type RegistrationConfig } from '../../../config-schema';
8
+ import { useConceptAnswers } from '../field.resource';
9
+ import styles from '../field.scss';
10
+
11
+ export const CauseOfDeathField: React.FC = () => {
12
+ const { t } = useTranslation();
13
+ const { fieldConfigurations, freeTextFieldConceptUuid } = useConfig<RegistrationConfig>();
14
+ const [deathCause, deathCauseMeta] = useField('deathCause');
15
+
16
+ const conceptUuid = fieldConfigurations?.causeOfDeath?.conceptUuid;
17
+ const required = fieldConfigurations?.causeOfDeath?.required;
18
+
19
+ const {
20
+ data: conceptAnswers,
21
+ isLoading: isLoadingConceptAnswers,
22
+ error: errorLoadingConceptAnswers,
23
+ } = useConceptAnswers(conceptUuid);
24
+
25
+ const answers = useMemo(() => {
26
+ if (!isLoadingConceptAnswers && conceptAnswers) {
27
+ return conceptAnswers.map((answer) => ({ ...answer, label: answer.display }));
28
+ }
29
+ return [];
30
+ }, [conceptAnswers, isLoadingConceptAnswers]);
31
+
32
+ if (isLoadingConceptAnswers) {
33
+ return (
34
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
35
+ <h4 className={styles.productiveHeading02Light}>{t('causeOfDeathInputLabel', 'Cause of death')}</h4>
36
+ <SelectSkeleton />
37
+ </div>
38
+ );
39
+ }
40
+
41
+ return (
42
+ <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
43
+ <h4 className={styles.productiveHeading02Light}>{t('causeOfDeathInputLabel', 'Cause of death')}</h4>
44
+ {errorLoadingConceptAnswers || !conceptUuid ? (
45
+ <InlineNotification
46
+ hideCloseButton
47
+ kind="error"
48
+ title={t('errorFetchingCodedCausesOfDeath', 'Error fetching coded causes of death')}
49
+ subtitle={t('refreshOrContactAdmin', 'Try refreshing the page or contact your system administrator')}
50
+ />
51
+ ) : (
52
+ <>
53
+ <Field name="deathCause">
54
+ {({ field, form: { touched, errors }, meta }) => {
55
+ return (
56
+ <Layer>
57
+ <Select
58
+ {...field}
59
+ id="deathCause"
60
+ invalid={errors.deathCause && touched.deathCause}
61
+ invalidText={errors.deathCause?.message}
62
+ labelText={t('causeOfDeathInputLabel', 'Cause of Death')}
63
+ name="deathCause"
64
+ required={required}>
65
+ <SelectItem id="empty-default-option" value={null} text={t('selectAnOption', 'Select an option')} />
66
+ {answers.map((answer) => (
67
+ <SelectItem id={answer.uuid} key={answer.uuid} text={answer.label} value={answer.uuid} />
68
+ ))}
69
+ </Select>
70
+ </Layer>
71
+ );
72
+ }}
73
+ </Field>
74
+ {deathCause.value === freeTextFieldConceptUuid && (
75
+ <div className={styles.nonCodedCauseOfDeath}>
76
+ <Field name="nonCodedCauseOfDeath">
77
+ {({ field, form: { touched, errors }, meta }) => {
78
+ return (
79
+ <Layer>
80
+ <TextInput
81
+ {...field}
82
+ id="nonCodedCauseOfDeath"
83
+ invalid={errors?.nonCodedCauseOfDeath && touched.nonCodedCauseOfDeath}
84
+ invalidText={errors?.nonCodedCauseOfDeath?.message}
85
+ labelText={t('nonCodedCauseOfDeath', 'Non-coded cause of death')}
86
+ placeholder={t('enterNonCodedCauseOfDeath', 'Enter non-coded cause of death')}
87
+ />
88
+ </Layer>
89
+ );
90
+ }}
91
+ </Field>
92
+ </div>
93
+ )}
94
+ </>
95
+ )}
96
+ </div>
97
+ );
98
+ };
@@ -0,0 +1,84 @@
1
+ import React, { useCallback, useContext } from 'react';
2
+ import classNames from 'classnames';
3
+ import dayjs from 'dayjs';
4
+ import { Layer, SelectItem, TimePicker, TimePickerSelect } from '@carbon/react';
5
+ import { useTranslation } from 'react-i18next';
6
+ import { useField } from 'formik';
7
+ import { OpenmrsDatePicker } from '@openmrs/esm-framework';
8
+ import { PatientRegistrationContext } from '../../patient-registration-context';
9
+ import type { FormValues } from '../../patient-registration.types';
10
+ import styles from '../field.scss';
11
+
12
+ export const DateAndTimeOfDeathField: React.FC = () => {
13
+ const { t } = useTranslation();
14
+
15
+ return (
16
+ <div className={classNames(styles.dodField, styles.halfWidthInDesktopView)}>
17
+ <h4 className={styles.productiveHeading02Light}>{t('deathDateInputLabel', 'Date of Death')}</h4>
18
+ <span>
19
+ <DeathDateField />
20
+ <DeathTimeField />
21
+ </span>
22
+ </div>
23
+ );
24
+ };
25
+
26
+ function DeathDateField() {
27
+ const { values, setFieldValue } = useContext(PatientRegistrationContext);
28
+ const [deathDate, deathDateMeta] = useField<keyof FormValues>('deathDate');
29
+ const { t } = useTranslation();
30
+ const today = dayjs().hour(23).minute(59).second(59).toDate();
31
+ const onDateChange = useCallback(
32
+ (selectedDate: Date) => {
33
+ setFieldValue(
34
+ 'deathDate',
35
+ selectedDate ? dayjs(selectedDate).hour(0).minute(0).second(0).millisecond(0).toDate() : undefined,
36
+ );
37
+ },
38
+ [deathDate],
39
+ );
40
+
41
+ return (
42
+ <Layer>
43
+ <OpenmrsDatePicker
44
+ {...deathDate}
45
+ id="deathDate"
46
+ invalidText={t(deathDateMeta.error)}
47
+ invalid={!!(deathDateMeta.touched && deathDateMeta.error)}
48
+ isRequired={values.isDead}
49
+ labelText={t('deathDateInputLabel', 'Date of death')}
50
+ maxDate={today}
51
+ onChange={onDateChange}
52
+ />
53
+ </Layer>
54
+ );
55
+ }
56
+
57
+ function DeathTimeField() {
58
+ const { t } = useTranslation();
59
+ const [deathTimeField, deathTimeMeta] = useField<keyof FormValues>('deathTime');
60
+ const [deathTimeFormatField, deathTimeFormatMeta] = useField<keyof FormValues>('deathTimeFormat');
61
+
62
+ return (
63
+ <Layer>
64
+ <TimePicker
65
+ {...deathTimeField}
66
+ id="time-picker"
67
+ labelText={t('timeOfDeathInputLabel', 'Time of death (hh:mm)')}
68
+ className={styles.timeOfDeathField}
69
+ pattern="^(1[0-2]|0?[1-9]):([0-5]?[0-9])$"
70
+ invalid={!!(deathTimeMeta.touched && deathTimeMeta.error)}
71
+ invalidText={t(deathTimeMeta.error)}>
72
+ <TimePickerSelect
73
+ {...deathTimeFormatField}
74
+ id="time-format-picker"
75
+ aria-label={t('timeFormat', 'Time Format')}
76
+ invalid={!!deathTimeFormatMeta.touched && deathTimeFormatMeta.error}
77
+ invalidText={t(deathTimeFormatMeta.error)}>
78
+ <SelectItem value="AM" text="AM" />
79
+ <SelectItem value="PM" text="PM" />
80
+ </TimePickerSelect>
81
+ </TimePicker>
82
+ </Layer>
83
+ );
84
+ }
@@ -30,7 +30,7 @@ export const DobField: React.FC = () => {
30
30
  const [birthdate, birthdateMeta] = useField('birthdate');
31
31
  const [yearsEstimated, yearsEstimateMeta] = useField('yearsEstimated');
32
32
  const [monthsEstimated, monthsEstimateMeta] = useField('monthsEstimated');
33
- const { setFieldValue } = useContext(PatientRegistrationContext);
33
+ const { setFieldValue, setFieldTouched } = useContext(PatientRegistrationContext);
34
34
  const today = new Date();
35
35
 
36
36
  const onToggle = useCallback(
@@ -39,6 +39,7 @@ export const DobField: React.FC = () => {
39
39
  setFieldValue('birthdate', '');
40
40
  setFieldValue('yearsEstimated', 0);
41
41
  setFieldValue('monthsEstimated', '');
42
+ setFieldTouched('birthdateEstimated', true, false);
42
43
  },
43
44
  [setFieldValue],
44
45
  );
@@ -46,8 +47,9 @@ export const DobField: React.FC = () => {
46
47
  const onDateChange = useCallback(
47
48
  (birthdate: Date) => {
48
49
  setFieldValue('birthdate', birthdate);
50
+ setFieldTouched('birthdate', true, false);
49
51
  },
50
- [setFieldValue],
52
+ [setFieldValue, setFieldTouched],
51
53
  );
52
54
 
53
55
  const onEstimatedYearsChange = useCallback(
@@ -80,7 +82,10 @@ export const DobField: React.FC = () => {
80
82
  setFieldValue('yearsEstimated', years);
81
83
  setFieldValue('monthsEstimated', months > 0 ? months : '');
82
84
  setFieldValue('birthdate', calcBirthdate(years, months, dateOfBirth));
83
- }, [setFieldValue, monthsEstimateMeta, yearsEstimateMeta, dateOfBirth]);
85
+ setFieldTouched('yearsEstimated', true, false);
86
+ setFieldTouched('monthsEstimated', true, false);
87
+ setFieldTouched('birthdate', true, false);
88
+ }, [setFieldValue, setFieldTouched, monthsEstimateMeta, yearsEstimateMeta, dateOfBirth]);
84
89
 
85
90
  return (
86
91
  <div className={styles.halfWidthInDesktopView}>
@@ -88,7 +93,7 @@ export const DobField: React.FC = () => {
88
93
  {(allowEstimatedBirthDate || dobUnknown) && (
89
94
  <div className={styles.dobField}>
90
95
  <div className={styles.dobContentSwitcherLabel}>
91
- <span className={styles.label01}>{t('dobToggleLabelText', 'Date of Birth Known?')}</span>
96
+ <span className={styles.label01}>{t('dobToggleLabelText', 'Date of birth known?')}</span>
92
97
  </div>
93
98
  <ContentSwitcher onChange={onToggle} selectedIndex={dobUnknown ? 1 : 0}>
94
99
  <Switch name="known" text={t('yes', 'Yes')} />
@@ -103,8 +108,9 @@ export const DobField: React.FC = () => {
103
108
  id="birthdate"
104
109
  {...birthdate}
105
110
  onChange={onDateChange}
111
+ onBlur={() => setFieldTouched('birthdate', true, false)}
106
112
  maxDate={today}
107
- labelText={t('dateOfBirthLabelText', 'Date of Birth')}
113
+ labelText={t('dateOfBirthLabelText', 'Date of birth')}
108
114
  isInvalid={!!(birthdateMeta.touched && birthdateMeta.error)}
109
115
  invalidText={t(birthdateMeta.error)}
110
116
  value={birthdate.value}
@@ -125,7 +131,11 @@ export const DobField: React.FC = () => {
125
131
  min={0}
126
132
  required
127
133
  {...yearsEstimated}
128
- onBlur={updateBirthdate}
134
+ onBlur={(e) => {
135
+ yearsEstimated.onBlur(e);
136
+ setFieldTouched('yearsEstimated', true, false);
137
+ updateBirthdate();
138
+ }}
129
139
  />
130
140
  </div>
131
141
  <div className={styles.dobField}>
@@ -141,7 +151,11 @@ export const DobField: React.FC = () => {
141
151
  min={0}
142
152
  {...monthsEstimated}
143
153
  required={!yearsEstimateMeta.value}
144
- onBlur={updateBirthdate}
154
+ onBlur={(e) => {
155
+ monthsEstimated.onBlur(e);
156
+ setFieldTouched('monthsEstimated', true, false);
157
+ updateBirthdate();
158
+ }}
145
159
  />
146
160
  </div>
147
161
  </div>
@@ -1,12 +1,14 @@
1
1
  import React from 'react';
2
- import { NameField } from './name/name-field.component';
3
- import { GenderField } from './gender/gender-field.component';
4
- import { Identifiers } from './id/id-field.component';
5
- import { DobField } from './dob/dob.component';
6
2
  import { reportError, useConfig } from '@openmrs/esm-framework';
7
3
  import { builtInFields, type RegistrationConfig } from '../../config-schema';
8
- import { CustomField } from './custom-field.component';
9
4
  import { AddressComponent } from './address/address-field.component';
5
+ import { CauseOfDeathField } from './cause-of-death/cause-of-death.component';
6
+ import { CustomField } from './custom-field.component';
7
+ import { DateAndTimeOfDeathField } from './date-and-time-of-death/date-and-time-of-death.component';
8
+ import { DobField } from './dob/dob.component';
9
+ import { GenderField } from './gender/gender-field.component';
10
+ import { Identifiers } from './id/id-field.component';
11
+ import { NameField } from './name/name-field.component';
10
12
  import { PhoneField } from './phone/phone-field.component';
11
13
 
12
14
  export interface FieldProps {
@@ -35,6 +37,10 @@ export function Field({ name }: FieldProps) {
35
37
  return <GenderField />;
36
38
  case 'dob':
37
39
  return <DobField />;
40
+ case 'dateAndTimeOfDeath':
41
+ return <DateAndTimeOfDeathField />;
42
+ case 'causeOfDeath':
43
+ return <CauseOfDeathField />;
38
44
  case 'address':
39
45
  return <AddressComponent />;
40
46
  case 'id':
@@ -1,6 +1,7 @@
1
- import { type FetchResponse, openmrsFetch, showSnackbar, restBaseUrl } from '@openmrs/esm-framework';
1
+ import { type FetchResponse, openmrsFetch, restBaseUrl, showSnackbar } from '@openmrs/esm-framework';
2
2
  import useSWRImmutable from 'swr/immutable';
3
3
  import { type ConceptAnswers, type ConceptResponse } from '../patient-registration.types';
4
+ import { useMemo } from 'react';
4
5
 
5
6
  export function useConcept(conceptUuid: string): { data: ConceptResponse; isLoading: boolean } {
6
7
  const shouldFetch = typeof conceptUuid === 'string' && conceptUuid !== '';
@@ -15,10 +16,15 @@ export function useConcept(conceptUuid: string): { data: ConceptResponse; isLoad
15
16
  kind: 'error',
16
17
  });
17
18
  }
18
- return { data: data?.data, isLoading };
19
+ const results = useMemo(() => ({ data: data?.data, isLoading }), [data, isLoading]);
20
+ return results;
19
21
  }
20
22
 
21
- export function useConceptAnswers(conceptUuid: string): { data: Array<ConceptAnswers>; isLoading: boolean } {
23
+ export function useConceptAnswers(conceptUuid: string): {
24
+ data: Array<ConceptAnswers>;
25
+ isLoading: boolean;
26
+ error: Error;
27
+ } {
22
28
  const shouldFetch = typeof conceptUuid === 'string' && conceptUuid !== '';
23
29
  const { data, error, isLoading } = useSWRImmutable<FetchResponse<ConceptResponse>, Error>(
24
30
  shouldFetch ? `${restBaseUrl}/concept/${conceptUuid}` : null,
@@ -31,5 +37,6 @@ export function useConceptAnswers(conceptUuid: string): { data: Array<ConceptAns
31
37
  kind: 'error',
32
38
  });
33
39
  }
34
- return { data: data?.data?.answers, isLoading };
40
+ const results = useMemo(() => ({ data: data?.data?.answers, isLoading, error }), [isLoading, error, data]);
41
+ return results;
35
42
  }
@@ -64,8 +64,30 @@
64
64
  }
65
65
 
66
66
  .sexField,
67
- .dobField {
67
+ .dobField,
68
+ .dodField {
68
69
  margin-bottom: layout.$spacing-05;
70
+
71
+ span {
72
+ display: flex;
73
+ flex-flow: row nowrap;
74
+ justify-content: space-between;
75
+ align-items: start;
76
+ }
77
+ }
78
+
79
+ .nonCodedCauseOfDeath {
80
+ margin-top: layout.$spacing-04;
81
+ }
82
+
83
+ .timeOfDeathContainer {
84
+ display: flex;
85
+ align-items: center;
86
+ }
87
+
88
+ .timeOfDeathField {
89
+ flex: none;
90
+ margin-left: layout.$spacing-02;
69
91
  }
70
92
 
71
93
  .dobContentSwitcherLabel {
@@ -11,11 +11,12 @@ export const GenderField: React.FC = () => {
11
11
  const { fieldConfigurations } = useConfig<RegistrationConfig>();
12
12
  const { t } = useTranslation();
13
13
  const [field, meta] = useField('gender');
14
- const { setFieldValue } = useContext(PatientRegistrationContext);
14
+ const { setFieldValue, setFieldTouched } = useContext(PatientRegistrationContext);
15
15
  const fieldConfigs = fieldConfigurations?.gender;
16
16
 
17
17
  const setGender = (gender: string) => {
18
18
  setFieldValue('gender', gender);
19
+ setFieldTouched('gender', true, false);
19
20
  };
20
21
  /**
21
22
  * DO NOT REMOVE THIS COMMENT HERE, ADDS TRANSLATION FOR SEX OPTIONS
@@ -12,6 +12,7 @@ jest.mock('react', () => ({
12
12
  ...(jest.requireActual('react') as any),
13
13
  useContext: jest.fn(() => ({
14
14
  setFieldValue: jest.fn(),
15
+ setFieldTouched: jest.fn(),
15
16
  })),
16
17
  }));
17
18
 
@@ -21,7 +21,7 @@ function checkNumber(value: string) {
21
21
 
22
22
  export const NameField = () => {
23
23
  const { t } = useTranslation();
24
- const { setCapturePhotoProps, currentPhoto, setFieldValue } = useContext(PatientRegistrationContext);
24
+ const { setCapturePhotoProps, currentPhoto, setFieldValue, setFieldTouched } = useContext(PatientRegistrationContext);
25
25
  const {
26
26
  fieldConfigurations: {
27
27
  name: {
@@ -48,6 +48,7 @@ export const NameField = () => {
48
48
  imageData: dataUri,
49
49
  dateTime: photoDateTime,
50
50
  });
51
+ setFieldTouched('photo', true, false);
51
52
  }
52
53
  },
53
54
  [setCapturePhotoProps],
@@ -63,6 +64,9 @@ export const NameField = () => {
63
64
  setFieldValue('familyName', defaultUnknownFamilyName);
64
65
  setUnknownPatient('true');
65
66
  }
67
+ setFieldTouched('givenName', true);
68
+ setFieldTouched('familyName', true);
69
+ setFieldTouched(`attributes.${unidentifiedPatientAttributeTypeUuid}`, true, false);
66
70
  };
67
71
 
68
72
  const firstNameField = (
@@ -20,7 +20,10 @@ const formValues: FormValues = {
20
20
  telephoneNumber: '',
21
21
  isDead: false,
22
22
  deathDate: 'string',
23
+ deathTime: '',
24
+ deathTimeFormat: 'AM',
23
25
  deathCause: 'string',
26
+ nonCodedCauseOfDeath: '',
24
27
  relationships: [],
25
28
  address: {
26
29
  address1: '',
@@ -1,23 +1,24 @@
1
1
  import {
2
2
  type FetchResponse,
3
- type Session,
4
- type StyleguideConfigObject,
5
3
  getConfig,
6
4
  openmrsFetch,
7
5
  queueSynchronizationItem,
8
6
  restBaseUrl,
7
+ type Session,
8
+ type StyleguideConfigObject,
9
+ toOmrsIsoString,
9
10
  } from '@openmrs/esm-framework';
10
11
  import { patientRegistration } from '../constants';
11
12
  import {
12
- type FormValues,
13
13
  type AttributeValue,
14
- type PatientUuidMapType,
15
- type Patient,
16
14
  type CapturePhotoProps,
15
+ type Encounter,
16
+ type FormValues,
17
+ type Patient,
17
18
  type PatientIdentifier,
18
19
  type PatientRegistration,
20
+ type PatientUuidMapType,
19
21
  type RelationshipValue,
20
- type Encounter,
21
22
  } from './patient-registration.types';
22
23
  import {
23
24
  addPatientIdentifier,
@@ -25,14 +26,16 @@ import {
25
26
  deletePersonName,
26
27
  deleteRelationship,
27
28
  generateIdentifier,
29
+ getDatetime,
30
+ saveEncounter,
28
31
  savePatient,
29
32
  savePatientPhoto,
30
33
  saveRelationship,
31
- updateRelationship,
32
34
  updatePatientIdentifier,
33
- saveEncounter,
35
+ updateRelationship,
34
36
  } from './patient-registration.resource';
35
37
  import { type RegistrationConfig } from '../config-schema';
38
+ import dayjs from 'dayjs';
36
39
 
37
40
  export type SavePatientForm = (
38
41
  isNewPatient: boolean,
@@ -62,7 +65,7 @@ export class FormManager {
62
65
  ) => {
63
66
  const syncItem: PatientRegistration = {
64
67
  fhirPatient: FormManager.mapPatientToFhirPatient(
65
- FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, []),
68
+ FormManager.getPatientToCreate(isNewPatient, values, patientUuidMap, initialAddressFieldValues, [], config),
66
69
  ),
67
70
  _patientRegistrationData: {
68
71
  isNewPatient,
@@ -115,6 +118,7 @@ export class FormManager {
115
118
  patientUuidMap,
116
119
  initialAddressFieldValues,
117
120
  patientIdentifiers,
121
+ config,
118
122
  );
119
123
 
120
124
  FormManager.getDeletedNames(values.patientUuid, patientUuidMap).forEach(async (name) => {
@@ -296,6 +300,7 @@ export class FormManager {
296
300
  patientUuidMap: PatientUuidMapType,
297
301
  initialAddressFieldValues: Record<string, any>,
298
302
  identifiers: Array<PatientIdentifier>,
303
+ config?: RegistrationConfig,
299
304
  ): Patient {
300
305
  let birthdate;
301
306
  if (values.birthdate instanceof Date) {
@@ -316,7 +321,7 @@ export class FormManager {
316
321
  birthdateEstimated: values.birthdateEstimated,
317
322
  attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
318
323
  addresses: [values.address],
319
- ...FormManager.getPatientDeathInfo(values),
324
+ ...FormManager.getPatientDeathInfo(values, config),
320
325
  },
321
326
  identifiers,
322
327
  };
@@ -375,12 +380,22 @@ export class FormManager {
375
380
  return attributes;
376
381
  }
377
382
 
378
- static getPatientDeathInfo(values: FormValues) {
379
- const { isDead, deathDate, deathCause } = values;
383
+ static getPatientDeathInfo(values: FormValues, config?: RegistrationConfig) {
384
+ const { isDead, deathDate, deathTime, deathTimeFormat, deathCause, nonCodedCauseOfDeath } = values;
385
+
386
+ if (!isDead) {
387
+ return {
388
+ dead: false,
389
+ };
390
+ }
391
+ const dateTimeOfDeath = toOmrsIsoString(getDatetime(deathDate, deathTime, deathTimeFormat));
392
+
380
393
  return {
381
- dead: isDead,
382
- deathDate: isDead ? deathDate : undefined,
383
- causeOfDeath: isDead ? deathCause : undefined,
394
+ dead: true,
395
+ deathDate: dateTimeOfDeath,
396
+ ...(deathCause === config?.freeTextFieldConceptUuid
397
+ ? { causeOfDeathNonCoded: nonCodedCauseOfDeath, causeOfDeath: null }
398
+ : { causeOfDeath: deathCause, causeOfDeathNonCoded: null }),
384
399
  };
385
400
  }
386
401
 
@@ -66,7 +66,7 @@ export interface TextInputProps
66
66
  * `true` to use the light version. For use on $ui-01 backgrounds only.
67
67
  * Don't use this to make tile background color same as container background color.
68
68
  * 'The `light` prop for `TextInput` has ' +
69
- 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'
69
+ 'been deprecated in favor of the new `Layer` component. It will be removed in the next major release.'
70
70
  */
71
71
  light?: boolean;
72
72
 
@@ -145,6 +145,10 @@ export const Input: React.FC<InputProps> = ({ checkWarning, ...props }) => {
145
145
  t('invalidEmail')
146
146
  t('numberInNameDubious')
147
147
  t('yearsEstimateRequired')
148
+ t('deathdayIsRequired', 'Death date is required when the patient is marked as deceased.')
149
+ t('deathdayInvalidDate', 'Date of death is invalid')
150
+ t('deathCauseRequired', 'Cause of death is required')
151
+ t('nonCodedCauseOfDeathRequired', 'Non-coded cause of death is required')
148
152
  */
149
153
 
150
154
  const value = field.value || '';