@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,18 +1,19 @@
1
1
  import React from 'react';
2
- import userEvent from '@testing-library/user-event';
3
- import { render, screen } from '@testing-library/react';
4
2
  import { Form, Formik } from 'formik';
3
+ import { render, screen } from '@testing-library/react';
4
+ import userEvent from '@testing-library/user-event';
5
5
  import { getDefaultsFromConfigSchema, useConfig } from '@openmrs/esm-framework';
6
- import { Identifiers } from './id-field.component';
7
- import { mockOpenmrsId, mockIdentifierTypes, mockPatient, mockSession } from '__mocks__';
8
- import { type RegistrationConfig, esmPatientRegistrationSchema } from '../../../config-schema';
9
- import { type Resources, ResourcesContext } from '../../../offline.resources';
6
+ import { type AddressTemplate, type IdentifierSource } from '../../patient-registration.types';
7
+ import { mockIdentifierTypes, mockOpenmrsId, mockPatient, mockSession } from '__mocks__';
8
+ import { esmPatientRegistrationSchema, type RegistrationConfig } from '../../../config-schema';
9
+ import { ResourcesContext, type Resources } from '../../../offline.resources';
10
10
  import { PatientRegistrationContext, type PatientRegistrationContextProps } from '../../patient-registration-context';
11
+ import { Identifiers, setIdentifierSource } from './id-field.component';
11
12
 
12
13
  const mockUseConfig = jest.mocked(useConfig<RegistrationConfig>);
13
14
 
14
15
  const mockResourcesContextValue = {
15
- addressTemplate: null,
16
+ addressTemplate: null as unknown as AddressTemplate,
16
17
  currentSession: mockSession.data,
17
18
  identifierTypes: [],
18
19
  relationshipTypes: [],
@@ -52,7 +53,7 @@ const mockContextValues: PatientRegistrationContextProps = {
52
53
  setInitialFormValues: jest.fn(),
53
54
  validationSchema: null,
54
55
  values: mockInitialFormValues,
55
- };
56
+ } as unknown as PatientRegistrationContextProps;
56
57
 
57
58
  describe('Identifiers', () => {
58
59
  beforeEach(() => {
@@ -120,3 +121,21 @@ describe('Identifiers', () => {
120
121
  expect(screen.getByRole('button', { name: 'Close overlay' })).toBeInTheDocument();
121
122
  });
122
123
  });
124
+
125
+ describe('setIdentifierSource', () => {
126
+ describe('auto-generation', () => {
127
+ it('should return auto-generated as the identifier value', () => {
128
+ const identifierSource = { autoGenerationOption: { automaticGenerationEnabled: true } } as IdentifierSource;
129
+ const { identifierValue } = setIdentifierSource(identifierSource, '', '');
130
+ expect(identifierValue).toBe('auto-generated');
131
+ });
132
+
133
+ it('should return the identifier value when manual entry enabled', () => {
134
+ const identifierSource = {
135
+ autoGenerationOption: { automaticGenerationEnabled: true, manualEntryEnabled: true },
136
+ } as IdentifierSource;
137
+ const { identifierValue } = setIdentifierSource(identifierSource, '10001V', '');
138
+ expect(identifierValue).toBe('10001V');
139
+ });
140
+ });
141
+ });
@@ -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 = (
@@ -33,7 +33,7 @@ export function ObsField({ fieldDefinition }: ObsFieldProps) {
33
33
  return null;
34
34
  }
35
35
 
36
- switch (concept.datatype.display) {
36
+ switch (concept?.datatype?.display) {
37
37
  case 'Text':
38
38
  return (
39
39
  <TextObsField
@@ -98,6 +98,7 @@ export function CodedPersonAttributeField({
98
98
  return (
99
99
  <>
100
100
  <Select
101
+ style={{ marginBottom: '1rem' }}
101
102
  id={id}
102
103
  name={`person-attribute-${personAttributeType.uuid}`}
103
104
  labelText={label ?? personAttributeType?.display}
@@ -1,19 +1,43 @@
1
- import React from 'react';
2
- import { Field } from 'formik';
1
+ import React, { useContext, useEffect } from 'react';
2
+ import { Field, type FieldProps } from 'formik';
3
3
  import { Layer, Select, SelectItem } from '@carbon/react';
4
- import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
5
4
  import { useTranslation } from 'react-i18next';
6
- import styles from './../field.scss';
7
5
  import classNames from 'classnames';
6
+ import { PatientRegistrationContext } from '../../patient-registration-context';
7
+ import styles from './../field.scss';
8
+ import { ResourcesContext } from '../../../offline.resources';
9
+ import useUpdateIdentifierRequirement from './useUpdateIdentifierRequirement';
8
10
 
9
- type CustomPersonAttributeFieldProps = {
11
+ interface PersonAttributeTypeResponse {
12
+ uuid: string;
13
+ display?: string;
14
+ }
15
+
16
+ interface ConceptAnswer {
17
+ uuid?: string;
18
+ name?: string;
19
+ label?: string;
20
+ showServiceExpression?: {
21
+ attributeTypeUuid: string;
22
+ value: string;
23
+ };
24
+ }
25
+
26
+ interface CustomPersonAttributeFieldProps {
10
27
  id: string;
11
28
  personAttributeType: PersonAttributeTypeResponse;
12
29
  answerConceptSetUuid: string;
13
30
  label?: string;
14
- customConceptAnswers: Array<{ uuid: string; label?: string }>;
31
+ customConceptAnswers: ConceptAnswer[];
15
32
  required: boolean;
16
- };
33
+ }
34
+
35
+ interface PatientRegistrationContextType {
36
+ setFieldValue: (field: string, value: any) => void;
37
+ values: {
38
+ attributes?: Record<string, string>;
39
+ };
40
+ }
17
41
 
18
42
  const CustomPersonAttributeField: React.FC<CustomPersonAttributeFieldProps> = ({
19
43
  personAttributeType,
@@ -24,30 +48,55 @@ const CustomPersonAttributeField: React.FC<CustomPersonAttributeFieldProps> = ({
24
48
  }) => {
25
49
  const { t } = useTranslation();
26
50
  const fieldName = `attributes.${personAttributeType.uuid}`;
51
+ const { setFieldValue, values } = useContext(PatientRegistrationContext);
52
+ useUpdateIdentifierRequirement(setFieldValue, values);
53
+ // TODO: Improve this logic
54
+ const filteredCustomConceptAnswers = customConceptAnswers.filter((answer) => {
55
+ const showExpression = answer.showServiceExpression;
56
+ if (!showExpression) return true;
57
+
58
+ const attributeValue = values?.attributes?.[showExpression.attributeTypeUuid];
59
+ const answerCadreId = answer.name;
60
+
61
+ if (answerCadreId == null) return true;
62
+
63
+ return showExpression.value.toLowerCase() === attributeValue?.toLowerCase();
64
+ });
65
+
66
+ useEffect(() => {
67
+ return () => {
68
+ setFieldValue(fieldName, '');
69
+ };
70
+ }, [fieldName, setFieldValue]);
71
+
72
+ const renderSelect = ({ field, form: { touched, errors } }: FieldProps) => {
73
+ const hasError = errors[fieldName] && touched[fieldName];
74
+ const displayLabel = label ?? personAttributeType?.display ?? '';
75
+
76
+ return (
77
+ <Select
78
+ id={id}
79
+ name={`person-attribute-${personAttributeType.uuid}`}
80
+ labelText={displayLabel}
81
+ invalid={Boolean(hasError)}
82
+ required={required}
83
+ {...field}>
84
+ <SelectItem value="" text={t('selectAnOption', 'Select an option')} />
85
+ {filteredCustomConceptAnswers.map((answer) => (
86
+ <SelectItem
87
+ key={answer.uuid ?? answer.name}
88
+ value={answer.uuid ?? answer.name ?? ''}
89
+ text={answer.label ?? answer.uuid ?? answer.name ?? ''}
90
+ />
91
+ ))}
92
+ </Select>
93
+ );
94
+ };
27
95
 
28
96
  return (
29
97
  <div className={classNames(styles.customField, styles.halfWidthInDesktopView)}>
30
98
  <Layer>
31
- <Field name={fieldName}>
32
- {({ field, form: { touched, errors }, meta }) => {
33
- return (
34
- <>
35
- <Select
36
- id={id}
37
- name={`person-attribute-${personAttributeType.uuid}`}
38
- labelText={label ?? personAttributeType?.display}
39
- invalid={errors[fieldName] && touched[fieldName]}
40
- required={required}
41
- {...field}>
42
- <SelectItem value={''} text={t('selectAnOption', 'Select an option')} />
43
- {customConceptAnswers.map((answer) => (
44
- <SelectItem key={answer.uuid} value={answer.uuid} text={answer.uuid} />
45
- ))}
46
- </Select>
47
- </>
48
- );
49
- }}
50
- </Field>
99
+ <Field name={fieldName}>{renderSelect}</Field>
51
100
  </Layer>
52
101
  </div>
53
102
  );
@@ -0,0 +1,105 @@
1
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Field, useField } from 'formik';
4
+ import { type PersonAttributeTypeResponse } from '../../patient-registration.types';
5
+ import styles from './../field.scss';
6
+ import { useLocations } from './location-person-attribute-field.resource';
7
+ import { ComboBox, InlineLoading, Layer } from '@carbon/react';
8
+ import { useTranslation } from 'react-i18next';
9
+
10
+ export interface LocationPersonAttributeFieldProps {
11
+ id: string;
12
+ personAttributeType: PersonAttributeTypeResponse;
13
+ label?: string;
14
+ locationTag: string;
15
+ required?: boolean;
16
+ }
17
+
18
+ export function LocationPersonAttributeField({
19
+ personAttributeType,
20
+ id,
21
+ label,
22
+ locationTag,
23
+ required,
24
+ }: LocationPersonAttributeFieldProps) {
25
+ const { t } = useTranslation();
26
+ const fieldName = `attributes.${personAttributeType.uuid}`;
27
+ const [field, meta, { setValue }] = useField(`attributes.${personAttributeType.uuid}`);
28
+ const [searchQuery, setSearchQuery] = useState<string>('');
29
+ const { locations, isLoading, loadingNewData } = useLocations(locationTag || null, searchQuery);
30
+ const prevLocationOptions = useRef([]);
31
+
32
+ const locationOptions = useMemo(() => {
33
+ if (!(isLoading && loadingNewData)) {
34
+ const newOptions = locations.map(({ resource: { id, name } }) => ({ value: id, label: name }));
35
+ prevLocationOptions.current = newOptions;
36
+ return newOptions;
37
+ }
38
+ return prevLocationOptions.current;
39
+ }, [locations, isLoading, loadingNewData]);
40
+
41
+ const selectedItem = useMemo(() => {
42
+ if (typeof meta.value === 'string') {
43
+ return locationOptions.find(({ value }) => value === meta.value) || null;
44
+ }
45
+ if (typeof meta.value === 'object' && meta.value) {
46
+ return locationOptions.find(({ value }) => value === meta.value.uuid) || null;
47
+ }
48
+ return null;
49
+ }, [locationOptions, meta.value]);
50
+
51
+ // Callback for when updating the combobox input
52
+ const handleInputChange = useCallback(
53
+ (value: string | null) => {
54
+ if (value) {
55
+ // If the value exists in the locationOptions (i.e. a label matches the input), exit the function
56
+ if (locationOptions.find(({ label }) => label === value)) return;
57
+ // If the input is a new value, set the search query
58
+ setSearchQuery(value);
59
+ // Clear the current selected value since the input doesn't match any existing options
60
+ setValue(null);
61
+ }
62
+ },
63
+ [locationOptions, setValue],
64
+ );
65
+ const handleSelect = useCallback(
66
+ ({ selectedItem }) => {
67
+ if (selectedItem) {
68
+ setValue(selectedItem.value);
69
+ }
70
+ },
71
+ [setValue],
72
+ );
73
+
74
+ return (
75
+ <div
76
+ className={classNames(styles.customField, styles.halfWidthInDesktopView, styles.locationAttributeFieldContainer)}>
77
+ <Layer>
78
+ <Field name={fieldName}>
79
+ {({ field, form: { touched, errors } }) => {
80
+ return (
81
+ <ComboBox
82
+ id={id}
83
+ name={`person-attribute-${personAttributeType.uuid}`}
84
+ titleText={label}
85
+ items={locationOptions}
86
+ placeholder={t('searchLocationPersonAttribute', 'Search location')}
87
+ onInputChange={handleInputChange}
88
+ required={required}
89
+ onChange={handleSelect}
90
+ selectedItem={selectedItem}
91
+ invalid={errors[fieldName] && touched[fieldName]}
92
+ typeahead
93
+ />
94
+ );
95
+ }}
96
+ </Field>
97
+ </Layer>
98
+ {loadingNewData && (
99
+ <div className={styles.loadingContainer}>
100
+ <InlineLoading />
101
+ </div>
102
+ )}
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,48 @@
1
+ import { useMemo } from 'react';
2
+ import { type FetchResponse, fhirBaseUrl, openmrsFetch, useDebounce } from '@openmrs/esm-framework';
3
+ import { type LocationEntry, type LocationResponse } from '@kenyaemr/esm-service-queues-app/src/types';
4
+ import useSWR from 'swr';
5
+
6
+ interface UseLocationsResult {
7
+ locations: Array<LocationEntry>;
8
+ isLoading: boolean;
9
+ loadingNewData: boolean;
10
+ }
11
+
12
+ export function useLocations(locationTag: string | null, searchQuery: string = ''): UseLocationsResult {
13
+ const debouncedSearchQuery = useDebounce(searchQuery);
14
+
15
+ const constructUrl = useMemo(() => {
16
+ let url = `${fhirBaseUrl}/Location?`;
17
+ let urlSearchParameters = new URLSearchParams();
18
+ urlSearchParameters.append('_summary', 'data');
19
+
20
+ if (!debouncedSearchQuery) {
21
+ urlSearchParameters.append('_count', '10');
22
+ }
23
+
24
+ if (locationTag) {
25
+ urlSearchParameters.append('_tag', locationTag);
26
+ }
27
+
28
+ if (typeof debouncedSearchQuery === 'string' && debouncedSearchQuery != '') {
29
+ urlSearchParameters.append('name:contains', debouncedSearchQuery);
30
+ }
31
+
32
+ return url + urlSearchParameters.toString();
33
+ }, [locationTag, debouncedSearchQuery]);
34
+
35
+ const { data, error, isLoading, isValidating } = useSWR<FetchResponse<LocationResponse>, Error>(
36
+ constructUrl,
37
+ openmrsFetch,
38
+ );
39
+
40
+ return useMemo(
41
+ () => ({
42
+ locations: data?.data?.entry || [],
43
+ isLoading,
44
+ loadingNewData: isValidating,
45
+ }),
46
+ [data, isLoading, isValidating],
47
+ );
48
+ }
@@ -1,11 +1,12 @@
1
1
  import React, { useMemo } from 'react';
2
- import { InlineNotification, TextInputSkeleton, SkeletonText } from '@carbon/react';
2
+ import { InlineNotification, TextInputSkeleton } from '@carbon/react';
3
3
  import { type FieldDefinition } from '../../../config-schema';
4
4
  import { CodedPersonAttributeField } from './coded-person-attribute-field.component';
5
5
  import { usePersonAttributeType } from './person-attributes.resource';
6
6
  import { TextPersonAttributeField } from './text-person-attribute-field.component';
7
7
  import { useTranslation } from 'react-i18next';
8
8
  import styles from '../field.scss';
9
+ import { LocationPersonAttributeField } from './location-person-attribute-field.component';
9
10
  import CustomPersonAttributeField from './custom-person-attribute-field.component';
10
11
 
11
12
  export interface PersonAttributeFieldProps {
@@ -55,6 +56,16 @@ export function PersonAttributeField({ fieldDefinition }: PersonAttributeFieldPr
55
56
  required={fieldDefinition.validation?.required ?? false}
56
57
  />
57
58
  );
59
+ case 'org.openmrs.Location':
60
+ return (
61
+ <LocationPersonAttributeField
62
+ personAttributeType={personAttributeType}
63
+ locationTag={fieldDefinition.locationTag}
64
+ label={fieldDefinition.label}
65
+ id={fieldDefinition?.id}
66
+ required={fieldDefinition.validation?.required ?? false}
67
+ />
68
+ );
58
69
  default:
59
70
  return (
60
71
  <InlineNotification kind="error" title="Error">
@@ -0,0 +1,83 @@
1
+ import { useEffect, useCallback, useRef, useMemo, useContext } from 'react';
2
+ import { deleteIdentifierType, initializeIdentifier } from '../id/id-field.component';
3
+ import { ResourcesContext } from '../../../offline.resources';
4
+
5
+ const useUpdateIdentifierRequirement = (setFieldValue, values) => {
6
+ const { identifierTypes = [] } = useContext(ResourcesContext);
7
+ const previousAttributes = useRef(values.attributes);
8
+ const previousIdentifiers = useRef(values.identifiers);
9
+
10
+ const publicationNumberIdentifier = useMemo(
11
+ () => identifierTypes?.find((identifier) => identifier.name === 'Publication Number'),
12
+ [identifierTypes],
13
+ );
14
+
15
+ // Memoize the civilian check
16
+ const isCivilian = useMemo(() => Object.values(values.attributes ?? {}).includes('Civilian'), [values.attributes]);
17
+
18
+ // Memoize the identifier initialization logic
19
+ const initializePublicationIdentifier = useCallback(
20
+ (currentIdentifiers) => {
21
+ if (!publicationNumberIdentifier) {
22
+ console.warn('Publication Number identifier type not found');
23
+ return null;
24
+ }
25
+
26
+ const initializedIdentifier = initializeIdentifier(
27
+ publicationNumberIdentifier,
28
+ currentIdentifiers[publicationNumberIdentifier.uuid],
29
+ );
30
+
31
+ return initializedIdentifier;
32
+ },
33
+ [publicationNumberIdentifier],
34
+ );
35
+ // Only run the effect if isCivilian is true
36
+ useEffect(() => {
37
+ // Skip if we don't have the required data
38
+ if (!values.attributes || !publicationNumberIdentifier) {
39
+ return;
40
+ }
41
+
42
+ // Check if relevant values have actually changed
43
+ const attributesChanged = previousAttributes.current !== values.attributes;
44
+ const identifiersChanged = previousIdentifiers.current !== values.identifiers;
45
+
46
+ if (!attributesChanged && !identifiersChanged) {
47
+ return;
48
+ }
49
+
50
+ // Update refs
51
+ previousAttributes.current = values.attributes;
52
+ previousIdentifiers.current = values.identifiers;
53
+ const isDependant = Object.values(values.attributes ?? {}).includes('Dependant');
54
+ // Only proceed if the user is a civilian
55
+ if (isCivilian && isDependant) {
56
+ const initializedIdentifier = initializePublicationIdentifier(values.identifiers);
57
+
58
+ // check if values.identifiers already has the publication number identifier
59
+ const hasPublicationNumberIdentifier = values.identifiers[publicationNumberIdentifier.fieldName];
60
+
61
+ if (initializedIdentifier && !hasPublicationNumberIdentifier) {
62
+ setFieldValue('identifiers', {
63
+ ...values.identifiers,
64
+ [publicationNumberIdentifier.fieldName]: { ...initializedIdentifier, required: true },
65
+ });
66
+ }
67
+ } else {
68
+ // Before deleting the publication number identifier, check if it exists
69
+ if (values.identifiers[publicationNumberIdentifier.fieldName]) {
70
+ setFieldValue('identifiers', deleteIdentifierType(values.identifiers, publicationNumberIdentifier.fieldName));
71
+ }
72
+ }
73
+ }, [
74
+ values.attributes,
75
+ values.identifiers,
76
+ isCivilian,
77
+ publicationNumberIdentifier,
78
+ initializePublicationIdentifier,
79
+ setFieldValue,
80
+ ]);
81
+ };
82
+
83
+ export default useUpdateIdentifierRequirement;
@@ -1,8 +1,11 @@
1
1
  import { FormManager } from './form-manager';
2
2
  import { type FormValues } from './patient-registration.types';
3
+ import { generateIdentifier } from './patient-registration.resource';
3
4
 
4
5
  jest.mock('./patient-registration.resource');
5
6
 
7
+ const mockGenerateIdentifier = generateIdentifier as jest.Mock;
8
+
6
9
  const formValues: FormValues = {
7
10
  patientUuid: '',
8
11
  givenName: '',
@@ -20,7 +23,10 @@ const formValues: FormValues = {
20
23
  telephoneNumber: '',
21
24
  isDead: false,
22
25
  deathDate: 'string',
26
+ deathTime: '',
27
+ deathTimeFormat: 'AM',
23
28
  deathCause: 'string',
29
+ nonCodedCauseOfDeath: '',
24
30
  relationships: [],
25
31
  address: {
26
32
  address1: '',
@@ -63,5 +69,20 @@ describe('FormManager', () => {
63
69
  },
64
70
  ]);
65
71
  });
72
+
73
+ it('should generate identifier if it has autoGeneration and manual entry disabled', async () => {
74
+ formValues.identifiers.foo.autoGeneration = true;
75
+ formValues.identifiers.foo.selectedSource.autoGenerationOption.manualEntryEnabled = false;
76
+ mockGenerateIdentifier.mockResolvedValue({ data: { identifier: '10001V' } });
77
+ await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
78
+ expect(mockGenerateIdentifier.mock.calls).toHaveLength(1);
79
+ });
80
+
81
+ it('should not generate identifiers if manual entry enabled and identifier value given', async () => {
82
+ formValues.identifiers.foo.autoGeneration = true;
83
+ formValues.identifiers.foo.selectedSource.autoGenerationOption.manualEntryEnabled = true;
84
+ await FormManager.savePatientIdentifiers(true, undefined, formValues.identifiers, {}, 'Nyc');
85
+ expect(mockGenerateIdentifier.mock.calls).toHaveLength(0);
86
+ });
66
87
  });
67
88
  });
@@ -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) => {
@@ -236,11 +240,16 @@ export class FormManager {
236
240
  initialValue,
237
241
  } = patientIdentifier;
238
242
 
239
- const identifier = !autoGeneration
240
- ? identifierValue
241
- : await (
242
- await generateIdentifier(selectedSource.uuid)
243
- ).data.identifier;
243
+ const autoGenerationManualEntry =
244
+ autoGeneration && selectedSource?.autoGenerationOption?.manualEntryEnabled && !!identifierValue;
245
+
246
+ const identifier =
247
+ !autoGeneration || autoGenerationManualEntry
248
+ ? identifierValue
249
+ : await (
250
+ await generateIdentifier(selectedSource.uuid)
251
+ ).data.identifier;
252
+
244
253
  const identifierToCreate = {
245
254
  uuid: identifierUuid,
246
255
  identifier,
@@ -296,6 +305,7 @@ export class FormManager {
296
305
  patientUuidMap: PatientUuidMapType,
297
306
  initialAddressFieldValues: Record<string, any>,
298
307
  identifiers: Array<PatientIdentifier>,
308
+ config?: RegistrationConfig,
299
309
  ): Patient {
300
310
  let birthdate;
301
311
  if (values.birthdate instanceof Date) {
@@ -316,7 +326,7 @@ export class FormManager {
316
326
  birthdateEstimated: values.birthdateEstimated,
317
327
  attributes: FormManager.getPatientAttributes(isNewPatient, values, patientUuidMap),
318
328
  addresses: [values.address],
319
- ...FormManager.getPatientDeathInfo(values),
329
+ ...FormManager.getPatientDeathInfo(values, config),
320
330
  },
321
331
  identifiers,
322
332
  };
@@ -375,12 +385,22 @@ export class FormManager {
375
385
  return attributes;
376
386
  }
377
387
 
378
- static getPatientDeathInfo(values: FormValues) {
379
- const { isDead, deathDate, deathCause } = values;
388
+ static getPatientDeathInfo(values: FormValues, config?: RegistrationConfig) {
389
+ const { isDead, deathDate, deathTime, deathTimeFormat, deathCause, nonCodedCauseOfDeath } = values;
390
+
391
+ if (!isDead) {
392
+ return {
393
+ dead: false,
394
+ };
395
+ }
396
+ const dateTimeOfDeath = toOmrsIsoString(getDatetime(deathDate, deathTime, deathTimeFormat));
397
+
380
398
  return {
381
- dead: isDead,
382
- deathDate: isDead ? deathDate : undefined,
383
- causeOfDeath: isDead ? deathCause : undefined,
399
+ dead: true,
400
+ deathDate: dateTimeOfDeath,
401
+ ...(deathCause === config?.freeTextFieldConceptUuid
402
+ ? { causeOfDeathNonCoded: nonCodedCauseOfDeath, causeOfDeath: null }
403
+ : { causeOfDeath: deathCause, causeOfDeathNonCoded: null }),
384
404
  };
385
405
  }
386
406