@ampath/esm-patient-registration-app 6.0.1-pre.24 → 6.0.1-pre.245

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 (136) hide show
  1. package/.turbo/turbo-build.log +24 -23
  2. package/dist/132.js +1 -1
  3. package/dist/197.js +1 -1
  4. package/dist/236.js +1 -1
  5. package/dist/300.js +1 -1
  6. package/dist/335.js +1 -1
  7. package/dist/41.js +1 -1
  8. package/dist/41.js.map +1 -1
  9. package/dist/421.js +1 -0
  10. package/dist/421.js.map +1 -0
  11. package/dist/449.js +1 -0
  12. package/dist/449.js.map +1 -0
  13. package/dist/531.js +2 -0
  14. package/dist/531.js.LICENSE.txt +24 -0
  15. package/dist/531.js.map +1 -0
  16. package/dist/55.js +1 -1
  17. package/dist/652.js +1 -1
  18. package/dist/661.js +1 -1
  19. package/dist/757.js +1 -0
  20. package/dist/757.js.map +1 -0
  21. package/dist/813.js +1 -0
  22. package/dist/813.js.map +1 -0
  23. package/dist/828.js +1 -0
  24. package/dist/828.js.map +1 -0
  25. package/dist/830.js +1 -0
  26. package/dist/830.js.map +1 -0
  27. package/dist/831.js +1 -1
  28. package/dist/831.js.map +1 -1
  29. package/dist/879.js +1 -1
  30. package/dist/913.js +1 -1
  31. package/dist/913.js.LICENSE.txt +3 -3
  32. package/dist/913.js.map +1 -1
  33. package/dist/927.js +1 -0
  34. package/dist/927.js.map +1 -0
  35. package/dist/99.js +1 -1
  36. package/dist/ampath-esm-patient-registration-app.js +1 -1
  37. package/dist/ampath-esm-patient-registration-app.js.buildmanifest.json +204 -132
  38. package/dist/ampath-esm-patient-registration-app.js.map +1 -1
  39. package/dist/main.js +1 -1
  40. package/dist/main.js.LICENSE.txt +0 -32
  41. package/dist/main.js.map +1 -1
  42. package/dist/routes.json +1 -1
  43. package/package.json +7 -10
  44. package/src/config-schema.ts +19 -10
  45. package/src/index.ts +11 -4
  46. package/src/offline.resources.ts +13 -18
  47. package/src/offline.ts +6 -4
  48. package/src/patient-photo.extension.tsx +9 -0
  49. package/src/patient-registration/field/address/custom-address-field.component.tsx +1 -0
  50. package/src/patient-registration/field/custom-field.component.tsx +6 -0
  51. package/src/patient-registration/field/dob/dob.component.tsx +45 -49
  52. package/src/patient-registration/field/field.resource.ts +3 -3
  53. package/src/patient-registration/field/obs/obs-field.component.tsx +1 -1
  54. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.component.tsx +4 -0
  55. package/src/patient-registration/field/person-attributes/custom-person-attribute-field.component.tsx +56 -0
  56. package/src/patient-registration/field/person-attributes/person-attribute-field.component.tsx +22 -7
  57. package/src/patient-registration/field/person-attributes/person-attributes.resource.ts +2 -2
  58. package/src/patient-registration/field/phone/phone-field.component.tsx +1 -0
  59. package/src/patient-registration/form-manager.ts +13 -6
  60. package/src/patient-registration/patient-registration-hooks.ts +133 -9
  61. package/src/patient-registration/patient-registration.component.tsx +55 -13
  62. package/src/patient-registration/{patient-registration.resource.test.tsx → patient-registration.resource.testt.tsx} +4 -4
  63. package/src/patient-registration/patient-registration.resource.ts +15 -75
  64. package/src/patient-registration/patient-registration.scss +0 -8
  65. package/src/patient-registration/patient-registration.types.ts +7 -1
  66. package/src/patient-registration/section/patient-relationships/relationships-section.component.tsx +5 -1
  67. package/src/patient-registration/section/patient-relationships/relationships.resource.tsx +2 -2
  68. package/src/patient-verification/assets/counties.json +236 -0
  69. package/src/patient-verification/assets/verification-assets.ts +11 -0
  70. package/src/patient-verification/patient-verification-hook.tsx +181 -0
  71. package/src/patient-verification/patient-verification-utils.ts +179 -0
  72. package/src/patient-verification/patient-verification.component.tsx +124 -0
  73. package/src/patient-verification/patient-verification.scss +25 -0
  74. package/src/patient-verification/verification-modal/confirm-prompt.component.tsx +72 -0
  75. package/src/patient-verification/verification-modal/empty-prompt.component.tsx +35 -0
  76. package/src/patient-verification/verification-types.ts +50 -0
  77. package/src/patient-verification-HIE/assets/counties.json +236 -0
  78. package/src/patient-verification-HIE/assets/verification-assets.ts +11 -0
  79. package/src/patient-verification-HIE/patient-verification-hook.tsx +181 -0
  80. package/src/patient-verification-HIE/patient-verification-utils.ts +179 -0
  81. package/src/patient-verification-HIE/patient-verification.component.tsx +124 -0
  82. package/src/patient-verification-HIE/patient-verification.scss +25 -0
  83. package/src/patient-verification-HIE/verification-modal/confirm-prompt.component.tsx +72 -0
  84. package/src/patient-verification-HIE/verification-modal/empty-prompt.component.tsx +35 -0
  85. package/src/patient-verification-HIE/verification-types.ts +50 -0
  86. package/src/routes.json +12 -3
  87. package/translations/am.json +26 -12
  88. package/translations/ar.json +26 -12
  89. package/translations/en.json +18 -4
  90. package/translations/es.json +10 -0
  91. package/translations/fr.json +6 -0
  92. package/translations/he.json +18 -0
  93. package/translations/km.json +18 -0
  94. package/translations/zh.json +30 -22
  95. package/translations/zh_CN.json +30 -22
  96. package/dist/229.js +0 -2
  97. package/dist/229.js.LICENSE.txt +0 -56
  98. package/dist/229.js.map +0 -1
  99. package/dist/537.js +0 -1
  100. package/dist/537.js.map +0 -1
  101. package/dist/56.js +0 -1
  102. package/dist/56.js.map +0 -1
  103. package/dist/885.js +0 -1
  104. package/dist/885.js.map +0 -1
  105. package/dist/918.js +0 -1
  106. package/dist/918.js.map +0 -1
  107. package/src/patient-registration/field/address/tests/address-hierarchy.test.tsx +0 -214
  108. package/src/patient-registration/field/address/tests/address-search-component.test.tsx +0 -135
  109. package/src/patient-registration/field/dob/dob.test.tsx +0 -75
  110. package/src/patient-registration/field/field.test.tsx +0 -294
  111. package/src/patient-registration/field/id/id-field.test.tsx +0 -107
  112. package/src/patient-registration/field/person-attributes/coded-attributes.component.tsx +0 -60
  113. package/src/patient-registration/field/person-attributes/coded-person-attribute-field.test.tsx +0 -127
  114. package/src/patient-registration/field/person-attributes/person-attribute-field.test.tsx +0 -187
  115. package/src/patient-registration/field/person-attributes/text-person-attribute-field.test.tsx +0 -88
  116. package/src/patient-registration/form-manager.test.ts +0 -67
  117. package/src/patient-registration/input/basic-input/select/select-input.test.tsx +0 -49
  118. package/src/patient-registration/input/custom-input/autosuggest/autosuggest.test.tsx +0 -132
  119. package/src/patient-registration/input/custom-input/identifier/identifier-input.test.tsx +0 -107
  120. package/src/patient-registration/patient-registration.test.tsx +0 -471
  121. package/src/patient-registration/section/death-info/death-info-section.test.tsx +0 -64
  122. package/src/patient-registration/section/demographics/demographics-section.test.tsx +0 -83
  123. package/src/patient-registration/section/patient-relationships/relationships-section.test.tsx +0 -100
  124. package/src/patient-verification/client-registry-constants.ts +0 -13
  125. package/src/patient-verification/client-registry.component.tsx +0 -66
  126. package/src/patient-verification/client-registry.scss +0 -1
  127. package/src/patient-verification/utils.tsx +0 -56
  128. package/src/patient-verification/verification-modal.scss +0 -20
  129. package/src/patient-verification/verification.component.tsx +0 -48
  130. package/src/root.test.tsx +0 -32
  131. package/src/widgets/cancel-patient-edit.test.tsx +0 -27
  132. package/src/widgets/display-photo.component.tsx +0 -30
  133. package/src/widgets/display-photo.test.tsx +0 -37
  134. package/src/widgets/edit-patient-details-button.test.tsx +0 -41
  135. /package/src/patient-registration/input/custom-input/identifier/{utils.test.ts → utils.testt.ts} +0 -0
  136. /package/src/widgets/{delete-identifier-confirmation-modal.test.tsx → delete-identifier-confirmation-modal.testt.tsx} +0 -0
@@ -0,0 +1,181 @@
1
+ import { type FetchResponse, openmrsFetch, showNotification, showToast, showSnackbar } from '@openmrs/esm-framework';
2
+ import { generateNUPIPayload, handleClientRegistryResponse } from './patient-verification-utils';
3
+ import useSWR from 'swr';
4
+ import useSWRImmutable from 'swr/immutable';
5
+ import {
6
+ type ConceptAnswers,
7
+ type ConceptResponse,
8
+ type FormValues,
9
+ } from '../patient-registration/patient-registration.types';
10
+
11
+ export function searchClientRegistry(
12
+ identifierType: string,
13
+ searchTerm: string,
14
+ token: string,
15
+ countryCode: string = 'KE',
16
+ ) {
17
+ const url = `https://afyakenyaapi.health.go.ke/partners/registry/search/${countryCode}/${identifierType}/${searchTerm}`;
18
+ return fetch(url, { headers: { Authorization: `Bearer ${token}` } }).then((r) => r.json());
19
+ }
20
+
21
+ export function savePatientToClientRegistry(formValues: FormValues) {
22
+ const createdRegistryPatient = generateNUPIPayload(formValues);
23
+ return fetch(`https://afyakenyaapi.health.go.ke/partners/registry`, {
24
+ headers: { Authorization: `Bearer ${formValues.token}`, 'Content-Type': 'application/json' },
25
+ method: 'POST',
26
+ body: JSON.stringify(createdRegistryPatient),
27
+ });
28
+ }
29
+
30
+ export async function handleSavePatientToClientRegistry(
31
+ formValues: FormValues,
32
+ setValues: (values: FormValues, shouldValidate?: boolean) => void,
33
+ inEditMode: boolean,
34
+ ) {
35
+ const mode = inEditMode ? 'edit' : 'new';
36
+ switch (mode) {
37
+ case 'edit': {
38
+ try {
39
+ const searchResponse = await searchClientRegistry(
40
+ 'national-id',
41
+ formValues.identifiers['nationalId'].identifierValue,
42
+ formValues.token,
43
+ );
44
+
45
+ // if client does not exists post client to registry
46
+ if (searchResponse?.clientExists === false) {
47
+ postToRegistry(formValues, setValues);
48
+ }
49
+ } catch (error) {
50
+ showSnackbar({
51
+ title: 'Client registry error',
52
+ subtitle: `${error}`,
53
+ timeoutInMs: 10000,
54
+ kind: 'error',
55
+ isLowContrast: true,
56
+ });
57
+ }
58
+ return;
59
+ }
60
+ case 'new': {
61
+ postToRegistry(formValues, setValues);
62
+ }
63
+ }
64
+ }
65
+
66
+ export function useConceptAnswers(conceptUuid: string): { data: Array<ConceptAnswers>; isLoading: boolean } {
67
+ const { data, error, isLoading } = useSWR<FetchResponse<ConceptResponse>, Error>(
68
+ `/ws/rest/v1/concept/${conceptUuid}`,
69
+ openmrsFetch,
70
+ );
71
+ if (error) {
72
+ showToast({
73
+ title: error.name,
74
+ description: error.message,
75
+ kind: 'error',
76
+ });
77
+ }
78
+ return { data: data?.data?.answers ?? [], isLoading };
79
+ }
80
+
81
+ const urlencoded = new URLSearchParams();
82
+ // urlencoded.append('client_id', 'palladium.partner.client');
83
+ // urlencoded.append('client_secret', '28f95b2a');
84
+ // urlencoded.append('grant_type', 'client_credentials');
85
+ // urlencoded.append('scope', 'DHP.Gateway DHP.Partners');
86
+
87
+ urlencoded.append('client_id', 'ampath.partner.client');
88
+ urlencoded.append('client_secret', '3ae8a7f6');
89
+ urlencoded.append('grant_type', 'client_credentials');
90
+ urlencoded.append('scope', 'DHP.Gateway DHP.Partners');
91
+
92
+ const swrFetcher = async (url) => {
93
+ const res = await fetch(url, {
94
+ method: 'POST',
95
+ body: urlencoded,
96
+ redirect: 'follow',
97
+ });
98
+
99
+ // If the status code is not in the range 200-299,
100
+ // we still try to parse and throw it.
101
+ if (!res.ok) {
102
+ const error = new Error('An error occurred while fetching the data.') as any;
103
+ // Attach extra info to the error object.
104
+ error.info = await res.json();
105
+ error.status = res.status;
106
+ throw error;
107
+ }
108
+
109
+ return res.json();
110
+ };
111
+
112
+ export function useGlobalProperties() {
113
+ const { data, isLoading, error } = useSWRImmutable(
114
+ `https://afyakenyaidentityapi.health.go.ke/connect/token`,
115
+ swrFetcher,
116
+ { refreshInterval: 864000 },
117
+ );
118
+ return { data: data, isLoading, error };
119
+ }
120
+
121
+ async function postToRegistry(
122
+ formValues: FormValues,
123
+ setValues: (values: FormValues, shouldValidate?: boolean) => void,
124
+ ) {
125
+ try {
126
+ const clientRegistryResponse = await savePatientToClientRegistry(formValues);
127
+ if (clientRegistryResponse.ok) {
128
+ const savedValues = await clientRegistryResponse.json();
129
+ const nupiIdentifier = {
130
+ ['nationalUniquePatientIdentifier']: {
131
+ identifierTypeUuid: 'cba702b9-4664-4b43-83f1-9ab473cbd64d',
132
+ identifierName: 'National Unique Patient Identifier (NUPI)',
133
+ identifierValue: savedValues['clientNumber'],
134
+ initialValue: savedValues['clientNumber'],
135
+ identifierUuid: undefined,
136
+ selectedSource: { uuid: '', name: '' },
137
+ preferred: false,
138
+ required: false,
139
+ },
140
+ };
141
+ setValues({ ...formValues, identifiers: { ...formValues.identifiers, ...nupiIdentifier } });
142
+ showToast({
143
+ title: 'Posted patient to client registry successfully',
144
+ description: `The patient has been saved to client registry`,
145
+ kind: 'success',
146
+ });
147
+ } else {
148
+ const responseError = await clientRegistryResponse.json();
149
+ const errorMessage = Object.values(responseError.errors ?? {})
150
+ .map((error: any) => error.join())
151
+ .toString();
152
+ setValues({
153
+ ...formValues,
154
+ attributes: {
155
+ ...formValues.attributes,
156
+ ['5553d509-f03a-4982-8e16-0d6f3d70fb8b']: 'Failed validation',
157
+ ['d8f4d295-3f31-47ec-b377-825bd38820b2']: 'Failed',
158
+ },
159
+ });
160
+ showNotification({
161
+ title: responseError.title,
162
+ description: errorMessage,
163
+ kind: 'warning',
164
+ millis: 150000,
165
+ });
166
+ }
167
+ } catch (error) {
168
+ showNotification({ kind: 'error', title: 'NUPI Post failed', description: JSON.stringify(error) });
169
+ }
170
+ }
171
+
172
+ export const useFacilityName = (facilityCode) => {
173
+ const apiUrl = `/ws/rest/v1/amrs/facilityName?facilityCode=${facilityCode}`;
174
+ const { data, error, isLoading } = useSWRImmutable<FetchResponse>(apiUrl, openmrsFetch);
175
+
176
+ return {
177
+ facilityName: data ? data?.data : null,
178
+ isLoading: isLoading,
179
+ isError: error,
180
+ };
181
+ };
@@ -0,0 +1,179 @@
1
+ import { showModal } from '@openmrs/esm-framework';
2
+ import { type FormikProps } from 'formik';
3
+ import { type ClientRegistryPatient, type RegistryPatient } from './verification-types';
4
+ import counties from './assets/counties.json';
5
+ import { type FormValues } from '../patient-registration/patient-registration.types';
6
+ import { capitalize } from 'lodash-es';
7
+
8
+ export function handleClientRegistryResponse(
9
+ clientResponse: ClientRegistryPatient,
10
+ props: FormikProps<FormValues>,
11
+ searchTerm: string,
12
+ ) {
13
+ if (clientResponse?.clientExists === false) {
14
+ const nupiIdentifiers = {
15
+ ['nationalId']: {
16
+ initialValue: searchTerm,
17
+ identifierUuid: undefined,
18
+ selectedSource: { uuid: '', name: '' },
19
+ preferred: false,
20
+ required: false,
21
+ identifierTypeUuid: '58a47054-1359-11df-a1f1-0026b9348838',
22
+ identifierName: 'Kenyan National ID Number',
23
+ identifierValue: searchTerm,
24
+ },
25
+ };
26
+ const dispose = showModal('empty-client-registry-modal', {
27
+ onConfirm: () => {
28
+ props.setValues({ ...props.values, identifiers: { ...props.values.identifiers, ...nupiIdentifiers } });
29
+ dispose();
30
+ },
31
+ close: () => dispose(),
32
+ });
33
+ }
34
+
35
+ if (clientResponse?.clientExists) {
36
+ const {
37
+ client: {
38
+ middleName,
39
+ lastName,
40
+ firstName,
41
+ contact,
42
+ country,
43
+ countyOfBirth,
44
+ residence,
45
+ identifications,
46
+ gender,
47
+ dateOfBirth,
48
+ isAlive,
49
+ clientNumber,
50
+ educationLevel,
51
+ occupation,
52
+ maritalStatus,
53
+ },
54
+ } = clientResponse;
55
+
56
+ const nupiIdentifiers = {
57
+ ['nationalId']: {
58
+ initialValue: identifications !== undefined && identifications[0]?.identificationNumber,
59
+ identifierUuid: undefined,
60
+ selectedSource: { uuid: '', name: '' },
61
+ preferred: false,
62
+ required: false,
63
+ identifierTypeUuid: '58a47054-1359-11df-a1f1-0026b9348838',
64
+ identifierName: 'Kenyan National ID Number',
65
+ identifierValue: identifications !== undefined && identifications[0]?.identificationNumber,
66
+ },
67
+
68
+ ['nationalUniquePatientIdentifier']: {
69
+ identifierTypeUuid: 'cba702b9-4664-4b43-83f1-9ab473cbd64d',
70
+ identifierName: 'National Unique Patient Identifier (NUPI)',
71
+ identifierValue: clientNumber,
72
+ initialValue: clientNumber,
73
+ identifierUuid: undefined,
74
+ selectedSource: { uuid: '', name: '' },
75
+ preferred: false,
76
+ required: false,
77
+ },
78
+ };
79
+
80
+ const dispose = showModal('confirm-client-registry-modal', {
81
+ onConfirm: () => {
82
+ props.setValues({
83
+ ...props.values,
84
+ familyName: lastName,
85
+ middleName: middleName,
86
+ givenName: firstName,
87
+ gender: clientResponse.client.gender,
88
+ birthdate: new Date(dateOfBirth),
89
+ isDead: !isAlive,
90
+ attributes: {
91
+ '72a759a8-1359-11df-a1f1-0026b9348838': contact?.primaryPhone,
92
+ 'b0a08406-09c0-4f8b-8cb5-b22b6d4a8e46': contact?.secondaryPhone,
93
+ '2f65dbcb-3e58-45a3-8be7-fd1dc9aa0faa': contact?.emailAddress ?? '',
94
+ },
95
+ address: {
96
+ address1: residence?.address,
97
+ address2: '',
98
+ address4: capitalize(residence?.ward ?? ''),
99
+ cityVillage: residence?.village,
100
+ stateProvince: capitalize(residence?.subCounty ?? ''),
101
+ countyDistrict: counties.find((county) => county.code === parseInt(residence?.county))?.name,
102
+ country: 'Kenya',
103
+ postalCode: residence?.address,
104
+ },
105
+ identifiers: { ...props.values.identifiers, ...nupiIdentifiers },
106
+ obs: {
107
+ 'a899a9f2-1350-11df-a1f1-0026b9348838':
108
+ props.values.concepts.find((concept) =>
109
+ concept.display?.toLowerCase()?.includes(clientResponse.client.maritalStatus?.toLowerCase()),
110
+ )?.uuid ?? '',
111
+ 'a89e48ae-1350-11df-a1f1-0026b9348838':
112
+ props.values.concepts.find((concept) =>
113
+ concept.display?.toLowerCase()?.includes(clientResponse.client.educationLevel?.toLowerCase()),
114
+ )?.uuid ?? '',
115
+ 'a8a0a00e-1350-11df-a1f1-0026b9348838':
116
+ clientResponse.client.occupation === undefined || clientResponse.client.occupation === null
117
+ ? 'a899e0ac-1350-11df-a1f1-0026b9348838'
118
+ : (props.values.concepts.find(
119
+ (concept) => concept.display?.toLowerCase() === clientResponse.client.occupation?.toLowerCase(),
120
+ )?.uuid ?? 'a8aaf3e2-1350-11df-a1f1-0026b9348838'),
121
+ },
122
+ });
123
+ dispose();
124
+ },
125
+ close: () => dispose(),
126
+ patient: clientResponse.client,
127
+ });
128
+ }
129
+ }
130
+
131
+ export function generateNUPIPayload(formValues: FormValues): RegistryPatient {
132
+ const educationLevel = formValues.concepts.find(
133
+ (concept) => concept.uuid === formValues.obs['a89e48ae-1350-11df-a1f1-0026b9348838'],
134
+ );
135
+ const occupation = formValues.concepts.find(
136
+ (concept) => concept.uuid === formValues.obs['a8a0a00e-1350-11df-a1f1-0026b9348838'],
137
+ );
138
+ const maritalStatus = formValues.concepts.find(
139
+ (concept) => concept.uuid === formValues.obs['a899a9f2-1350-11df-a1f1-0026b9348838'],
140
+ );
141
+
142
+ let createRegistryPatient: RegistryPatient = {
143
+ firstName: formValues?.givenName,
144
+ middleName: formValues?.middleName,
145
+ lastName: formValues?.familyName,
146
+ gender: formValues?.gender === 'Male' ? 'male' : 'female',
147
+ dateOfBirth: new Date(formValues.birthdate).toISOString(),
148
+ isAlive: !formValues.isDead,
149
+ residence: {
150
+ county: `0${counties.find((county) => county.name.includes(formValues.address['countyDistrict']))?.code}`,
151
+ subCounty: formValues.address['stateProvince']?.toLocaleLowerCase(),
152
+ ward: formValues.address['address4']?.toLocaleLowerCase(),
153
+ village: formValues.address['cityVillage'],
154
+ landmark: formValues.address['address2'],
155
+ address: formValues.address['postalCode'],
156
+ },
157
+ nextOfKins: [],
158
+ contact: {
159
+ primaryPhone: formValues.attributes['72a759a8-1359-11df-a1f1-0026b9348838'],
160
+ secondaryPhone: formValues.attributes['b0a08406-09c0-4f8b-8cb5-b22b6d4a8e46'],
161
+ emailAddress: formValues.attributes['2f65dbcb-3e58-45a3-8be7-fd1dc9aa0faa'],
162
+ },
163
+ country: 'KE',
164
+ countyOfBirth: `0${counties.find((county) => county.name.includes(formValues.address['countyDistrict']))?.code}`,
165
+ educationLevel: educationLevel?.display?.toLowerCase() ?? '',
166
+ religion: '',
167
+ occupation: occupation?.display?.toLowerCase() ?? '',
168
+ maritalStatus: maritalStatus?.display?.toLowerCase() ?? '',
169
+ originFacilityKmflCode: '',
170
+ nascopCCCNumber: '',
171
+ identifications: [
172
+ {
173
+ identificationType: 'national-id',
174
+ identificationNumber: formValues.identifiers['nationalId']?.identifierValue,
175
+ },
176
+ ],
177
+ };
178
+ return createRegistryPatient;
179
+ }
@@ -0,0 +1,124 @@
1
+ import React, { useState } from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Tile, ComboBox, Layer, Button, Search, InlineLoading, InlineNotification } from '@carbon/react';
4
+ import styles from './patient-verification.scss';
5
+ import { countries, verificationIdentifierTypes } from './assets/verification-assets';
6
+ import { searchClientRegistry, useGlobalProperties } from './patient-verification-hook';
7
+ import { showSnackbar, showToast } from '@openmrs/esm-framework';
8
+ import { handleClientRegistryResponse } from './patient-verification-utils';
9
+ import { type FormikProps } from 'formik';
10
+ import { type FormValues } from '../patient-registration/patient-registration.types';
11
+
12
+ interface PatientVerificationProps {
13
+ props: FormikProps<FormValues>;
14
+ setInitialFormValues: React.Dispatch<FormValues>;
15
+ }
16
+
17
+ const PatientVerification: React.FC<PatientVerificationProps> = ({ props }) => {
18
+ const { t } = useTranslation();
19
+ const { data, isLoading, error } = useGlobalProperties();
20
+ const [verificationCriteria, setVerificationCriteria] = useState({
21
+ searchTerm: '',
22
+ identifierType: '',
23
+ countryCode: 'KE',
24
+ });
25
+ const [isLoadingSearch, setIsLoadingSearch] = useState(false);
26
+
27
+ const handleSearch = async () => {
28
+ setIsLoadingSearch(true);
29
+ try {
30
+ const clientRegistryResponse = await searchClientRegistry(
31
+ verificationCriteria.identifierType,
32
+ verificationCriteria.searchTerm,
33
+ props.values.token,
34
+ verificationCriteria.countryCode,
35
+ );
36
+ setIsLoadingSearch(false);
37
+
38
+ handleClientRegistryResponse(clientRegistryResponse, props, verificationCriteria.searchTerm);
39
+ } catch (error) {
40
+ showSnackbar({
41
+ title: 'Client registry error',
42
+ subtitle: `Please reload the registration page and re-try again, if the issue persist contact system administrator`,
43
+ timeoutInMs: 10000,
44
+ kind: 'error',
45
+ isLowContrast: true,
46
+ });
47
+ setIsLoadingSearch(false);
48
+ }
49
+ };
50
+
51
+ return (
52
+ <div id={'patientVerification'}>
53
+ <h3 className={styles.productiveHeading02} style={{ color: '#161616' }}>
54
+ {t('clientVerificationWithClientRegistry', 'Client verification with client registry')}
55
+ </h3>
56
+ <div style={{ margin: '1rem 0 1rem' }}>
57
+ <Layer>
58
+ {isLoading && <InlineLoading status="active" iconDescription="Loading" description="Loading data..." />}
59
+ </Layer>
60
+ {error && (
61
+ <InlineNotification
62
+ className={styles.errorWrapper}
63
+ aria-label="closes notification"
64
+ kind="error"
65
+ lowContrast
66
+ statusIconDescription="notification"
67
+ subtitle={t(
68
+ 'clientRegistryErrorSubtitle',
69
+ 'Please proceed with registration contact system admin and try again later',
70
+ )}
71
+ title={t('clientRegistryError', 'Error occurred while reaching the client registry')}
72
+ />
73
+ )}
74
+ <Tile className={styles.verificationWrapper}>
75
+ <Layer>
76
+ <ComboBox
77
+ ariaLabel={t('selectCountry', 'Select country')}
78
+ id="selectCountry"
79
+ items={countries}
80
+ itemToString={(item) => item?.name ?? ''}
81
+ label="Select country"
82
+ titleText={t('selectCountry', 'Select country')}
83
+ initialSelectedItem={countries[0]}
84
+ onChange={({ selectedItem }) =>
85
+ setVerificationCriteria({ ...verificationCriteria, countryCode: selectedItem?.initials })
86
+ }
87
+ />
88
+ </Layer>
89
+ <Layer>
90
+ <ComboBox
91
+ ariaLabel={t('selectIdentifierType', 'Select identifier type')}
92
+ id="selectIdentifierType"
93
+ items={verificationIdentifierTypes}
94
+ itemToString={(item) => item?.name ?? ''}
95
+ label="Select identifier type"
96
+ titleText={t('selectIdentifierType', 'Select identifier type')}
97
+ onChange={({ selectedItem }) =>
98
+ setVerificationCriteria({ ...verificationCriteria, identifierType: selectedItem?.value })
99
+ }
100
+ />
101
+ </Layer>
102
+ <Layer>
103
+ <Search
104
+ id="clientRegistrySearch"
105
+ autoFocus
106
+ placeHolderText={t('searchClientRegistry', 'Search client registry')}
107
+ disabled={!verificationCriteria.identifierType}
108
+ onChange={(event) => setVerificationCriteria({ ...verificationCriteria, searchTerm: event.target.value })}
109
+ />
110
+ </Layer>
111
+ {!isLoadingSearch ? (
112
+ <Button disabled={!verificationCriteria.identifierType && !isLoading} size="md" onClick={handleSearch}>
113
+ {t('validate', 'Validate')}
114
+ </Button>
115
+ ) : (
116
+ <InlineLoading status="active" iconDescription="Loading" description="Searching client registry" />
117
+ )}
118
+ </Tile>
119
+ </div>
120
+ </div>
121
+ );
122
+ };
123
+
124
+ export default PatientVerification;
@@ -0,0 +1,25 @@
1
+ @use '@carbon/colors';
2
+ @import '../patient-registration/patient-registration.scss';
3
+
4
+ /* Desktop */
5
+ :global(.omrs-breakpoint-gt-tablet) {
6
+ .verificationWrapper {
7
+ display: grid;
8
+ grid-template-columns: 1fr 1fr 1fr 1fr;
9
+ column-gap: 0.325rem;
10
+ align-items: flex-end;
11
+ }
12
+ }
13
+
14
+ /* Tablet */
15
+ :global(.omrs-breakpoint-lt-desktop) {
16
+ .verificationWrapper {
17
+ row-gap: 0.5rem;
18
+ display: flex;
19
+ flex-direction: column;
20
+ }
21
+ }
22
+
23
+ .errorWrapper {
24
+ margin: 0 0 1rem 0;
25
+ }
@@ -0,0 +1,72 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '@carbon/react';
4
+ import { age, ExtensionSlot, formatDate } from '@openmrs/esm-framework';
5
+ import capitalize from 'lodash-es/capitalize';
6
+ import { useFacilityName } from '../patient-verification-hook';
7
+
8
+ const PatientInfo: React.FC<{ label: string; value: string }> = ({ label, value }) => {
9
+ return (
10
+ <div style={{ display: 'grid', gridTemplateColumns: '0.25fr 0.75fr', margin: '0.25rem' }}>
11
+ <span style={{ minWidth: '5rem', fontWeight: 'bold' }}>{label}</span>
12
+ <span>{value}</span>
13
+ </div>
14
+ );
15
+ };
16
+
17
+ interface ConfirmPromptProps {
18
+ onConfirm: void;
19
+ close: void;
20
+ patient: any;
21
+ }
22
+
23
+ const ConfirmPrompt: React.FC<ConfirmPromptProps> = ({ close, onConfirm, patient }) => {
24
+ const { t } = useTranslation();
25
+ return (
26
+ <>
27
+ <div className="cds--modal-header">
28
+ <h3 className="cds--modal-header__heading">
29
+ {t('clientRegistryEmpty', `Patient ${patient?.firstName} ${patient?.lastName} found`)}
30
+ </h3>
31
+ </div>
32
+ <div className="cds--modal-content">
33
+ <p>
34
+ {t(
35
+ 'patientDetailsFound',
36
+ 'Patient information found in the registry, do you want to use the information to continue with registration?',
37
+ )}
38
+ </p>
39
+ <div style={{ display: 'flex', margin: '1rem' }}>
40
+ <ExtensionSlot
41
+ style={{ display: 'flex', alignItems: 'center' }}
42
+ name="patient-photo-slot"
43
+ state={{ patientName: `${patient?.firstName} ${patient?.lastName}` }}
44
+ />
45
+ <div style={{ width: '100%', marginLeft: '0.625rem' }}>
46
+ <PatientInfo
47
+ label={t('patientName', 'Patient name')}
48
+ value={`${patient?.firstName} ${patient?.lastName}`}
49
+ />
50
+ <PatientInfo
51
+ label={t('nationalId', 'National ID')}
52
+ value={patient?.identifications[0]?.identificationNumber}
53
+ />
54
+ <PatientInfo label={t('age', 'Age')} value={age(patient?.dateOfBirth)} />
55
+ <PatientInfo label={t('dateOfBirth', 'Date of birth')} value={formatDate(new Date(patient?.dateOfBirth))} />
56
+ <PatientInfo label={t('gender', 'Gender')} value={capitalize(patient?.gender)} />
57
+ <PatientInfo label={t('nupi', 'NUPI')} value={patient?.clientNumber} />
58
+ <PatientInfo label={t('shaNumber', 'SHA Number')} value={'--'} />
59
+ </div>
60
+ </div>
61
+ </div>
62
+ <div className="cds--modal-footer">
63
+ <Button kind="secondary" onClick={close}>
64
+ {t('cancel', 'Cancel')}
65
+ </Button>
66
+ <Button onClick={onConfirm}>{t('useValues', 'Use values')}</Button>
67
+ </div>
68
+ </>
69
+ );
70
+ };
71
+
72
+ export default ConfirmPrompt;
@@ -0,0 +1,35 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Button } from '@carbon/react';
4
+
5
+ interface EmptyPromptProps {
6
+ onConfirm: void;
7
+ close: void;
8
+ }
9
+
10
+ const EmptyPrompt: React.FC<EmptyPromptProps> = ({ close, onConfirm }) => {
11
+ const { t } = useTranslation();
12
+ return (
13
+ <>
14
+ <div className="cds--modal-header">
15
+ <h3 className="cds--modal-header__heading">{t('clientRegistryEmpty', 'Create & Post Patient')}</h3>
16
+ </div>
17
+ <div className="cds--modal-content">
18
+ <p>
19
+ {t(
20
+ 'patientNotFound',
21
+ 'The patient records could not be found in Client registry, do you want to continue to create and post patient to registry',
22
+ )}
23
+ </p>
24
+ </div>
25
+ <div className="cds--modal-footer">
26
+ <Button kind="secondary" onClick={close}>
27
+ {t('cancel', 'Cancel')}
28
+ </Button>
29
+ <Button onClick={onConfirm}>{t('continue', 'Continue to registration')}</Button>
30
+ </div>
31
+ </>
32
+ );
33
+ };
34
+
35
+ export default EmptyPrompt;
@@ -0,0 +1,50 @@
1
+ export interface ClientIdentification {
2
+ identificationType: string;
3
+ identificationNumber: string;
4
+ }
5
+
6
+ interface ClientContact {
7
+ primaryPhone: string;
8
+ secondaryPhone?: string;
9
+ emailAddress?: string;
10
+ }
11
+
12
+ export interface ClientRegistryPatient {
13
+ clientExists: boolean;
14
+ client?: RegistryPatient;
15
+ }
16
+
17
+ export interface RegistryPatient {
18
+ clientNumber?: string;
19
+ firstName: string;
20
+ middleName: string;
21
+ lastName: string;
22
+ dateOfBirth: string;
23
+ maritalStatus?: string;
24
+ gender: string;
25
+ occupation?: string;
26
+ religion?: string;
27
+ educationLevel?: string;
28
+ country: string;
29
+ countyOfBirth?: string;
30
+ isAlive: boolean;
31
+ originFacilityKmflCode?: string;
32
+ isOnART?: string;
33
+ nascopCCCNumber?: string;
34
+ residence: {
35
+ county: string;
36
+ subCounty: string;
37
+ ward: string;
38
+ village: string;
39
+ landmark: string;
40
+ address: string;
41
+ };
42
+ identifications: Array<ClientIdentification>;
43
+ contact: ClientContact;
44
+ nextOfKins: Array<{
45
+ name: string;
46
+ relationship: string;
47
+ residence: string;
48
+ contact: ClientContact;
49
+ }>;
50
+ }