@palladium-ethiopia/esm-admin-app 5.4.2-pre.100

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 (149) hide show
  1. package/.turbo/turbo-build.log +15 -0
  2. package/README.md +12 -0
  3. package/dist/117.js +1 -0
  4. package/dist/152.js +1 -0
  5. package/dist/152.js.map +1 -0
  6. package/dist/209.js +1 -0
  7. package/dist/209.js.map +1 -0
  8. package/dist/41.js +1 -0
  9. package/dist/41.js.map +1 -0
  10. package/dist/442.js +1 -0
  11. package/dist/442.js.map +1 -0
  12. package/dist/466.js +1 -0
  13. package/dist/466.js.map +1 -0
  14. package/dist/555.js +1 -0
  15. package/dist/555.js.map +1 -0
  16. package/dist/61.js +1 -0
  17. package/dist/61.js.map +1 -0
  18. package/dist/672.js +15 -0
  19. package/dist/672.js.map +1 -0
  20. package/dist/689.js +1 -0
  21. package/dist/689.js.map +1 -0
  22. package/dist/710.js +1 -0
  23. package/dist/710.js.map +1 -0
  24. package/dist/712.js +1 -0
  25. package/dist/712.js.map +1 -0
  26. package/dist/771.js +1 -0
  27. package/dist/771.js.map +1 -0
  28. package/dist/789.js +1 -0
  29. package/dist/789.js.map +1 -0
  30. package/dist/806.js +1 -0
  31. package/dist/826.js +1 -0
  32. package/dist/826.js.map +1 -0
  33. package/dist/914.js +27 -0
  34. package/dist/914.js.map +1 -0
  35. package/dist/926.js +17 -0
  36. package/dist/926.js.map +1 -0
  37. package/dist/ethiopia-esm-admin-app.js +6 -0
  38. package/dist/ethiopia-esm-admin-app.js.buildmanifest.json +556 -0
  39. package/dist/ethiopia-esm-admin-app.js.map +1 -0
  40. package/dist/main.js +32 -0
  41. package/dist/main.js.map +1 -0
  42. package/dist/routes.json +1 -0
  43. package/jest.config.js +8 -0
  44. package/package.json +52 -0
  45. package/rspack.config.js +1 -0
  46. package/src/components/confirm-modal/confirmation-operation-modal.component.tsx +43 -0
  47. package/src/components/confirm-modal/confirmation-operation.test.tsx +69 -0
  48. package/src/components/dashboard/dashboard.component.tsx +131 -0
  49. package/src/components/dashboard/dashboard.scss +38 -0
  50. package/src/components/dashboard/etl-dashboard.component.tsx +11 -0
  51. package/src/components/empty-state/empty-state-log.components.tsx +20 -0
  52. package/src/components/empty-state/empty-state-log.scss +28 -0
  53. package/src/components/empty-state/empty-state-log.test.tsx +24 -0
  54. package/src/components/facility-setup/card.component.tsx +16 -0
  55. package/src/components/facility-setup/facility-info.component.tsx +142 -0
  56. package/src/components/facility-setup/facility-info.scss +87 -0
  57. package/src/components/facility-setup/facility-setup.component.tsx +21 -0
  58. package/src/components/facility-setup/facility-setup.resource.tsx +7 -0
  59. package/src/components/facility-setup/facility-setup.scss +38 -0
  60. package/src/components/facility-setup/header/header.component.tsx +23 -0
  61. package/src/components/facility-setup/header/header.scss +19 -0
  62. package/src/components/header/header-illustration.component.tsx +13 -0
  63. package/src/components/header/header.component.tsx +28 -0
  64. package/src/components/header/header.scss +19 -0
  65. package/src/components/hook/healthWorkerAdapter.ts +213 -0
  66. package/src/components/hook/useFacilityInfo.tsx +37 -0
  67. package/src/components/hook/useSystemRoleSetting.tsx +33 -0
  68. package/src/components/locations/auto-suggest/autosuggest.component.tsx +149 -0
  69. package/src/components/locations/auto-suggest/autosuggest.scss +61 -0
  70. package/src/components/locations/auto-suggest/location-autosuggest.component.tsx +94 -0
  71. package/src/components/locations/auto-suggest/location-autosuggest.scss +48 -0
  72. package/src/components/locations/common/results-tile.component.tsx +45 -0
  73. package/src/components/locations/common/results-tile.scss +86 -0
  74. package/src/components/locations/forms/add-location/add-location.workspace.scss +34 -0
  75. package/src/components/locations/forms/add-location/add-location.workspace.tsx +200 -0
  76. package/src/components/locations/forms/search-location/search-location.workspace.scss +79 -0
  77. package/src/components/locations/forms/search-location/search-location.workspace.tsx +215 -0
  78. package/src/components/locations/header/header.component.tsx +48 -0
  79. package/src/components/locations/header/header.scss +58 -0
  80. package/src/components/locations/helpers/index.ts +16 -0
  81. package/src/components/locations/home/home-locations.component.tsx +18 -0
  82. package/src/components/locations/home/home-locations.scss +8 -0
  83. package/src/components/locations/hooks/UseFacilityLocations.ts +12 -0
  84. package/src/components/locations/hooks/useLocation.ts +18 -0
  85. package/src/components/locations/hooks/useLocationTags.ts +15 -0
  86. package/src/components/locations/tables/locations-table.component.tsx +243 -0
  87. package/src/components/locations/tables/locations-table.resource.ts +26 -0
  88. package/src/components/locations/tables/locations-table.scss +115 -0
  89. package/src/components/locations/types/index.ts +120 -0
  90. package/src/components/locations/utils/index.ts +5 -0
  91. package/src/components/logs-table/operation-log-resource.ts +41 -0
  92. package/src/components/logs-table/operation-log-table.component.tsx +120 -0
  93. package/src/components/logs-table/operation-log.scss +10 -0
  94. package/src/components/logs-table/operation-log.test.tsx +47 -0
  95. package/src/components/modal/hwr-confirmation.modal.scss +21 -0
  96. package/src/components/modal/hwr-confirmation.modal.tsx +170 -0
  97. package/src/components/modal/hwr-empty.modal.component.tsx +54 -0
  98. package/src/components/modal/hwr-sync.modal.scss +30 -0
  99. package/src/components/modal/hwr-sync.modal.tsx +209 -0
  100. package/src/components/modal/hwr-sync.resource.ts +23 -0
  101. package/src/components/provider-banner/provider-banner.component.tsx +106 -0
  102. package/src/components/provider-banner/provider-banner.module.scss +51 -0
  103. package/src/components/provider-banner/provider-banner.resource.ts +29 -0
  104. package/src/components/side-menu/left-panel.scss +42 -0
  105. package/src/components/side-menu/left-pannel.component.tsx +22 -0
  106. package/src/components/users/header/header.scss +90 -0
  107. package/src/components/users/header/user-management-header.component.tsx +42 -0
  108. package/src/components/users/manage-users/hooks/useProviderAttributeMapping.ts +110 -0
  109. package/src/components/users/manage-users/hooks/useUserFormSteps.ts +119 -0
  110. package/src/components/users/manage-users/hooks/useUserFormSubmission.ts +264 -0
  111. package/src/components/users/manage-users/hooks/useUserManagementForm.ts +122 -0
  112. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-list/user-role-scope-list.component.tsx +177 -0
  113. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-fields.scss +117 -0
  114. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope-fields.component.tsx +290 -0
  115. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/user-role-scope.workspace.tsx +316 -0
  116. package/src/components/users/manage-users/manage-user-role-scope/user-role-scope-workspace/userRoleScopeFormSchema.tsx +43 -0
  117. package/src/components/users/manage-users/manage-user.component.tsx +19 -0
  118. package/src/components/users/manage-users/manage-user.scss +31 -0
  119. package/src/components/users/manage-users/provider-autosuggest.component.tsx +117 -0
  120. package/src/components/users/manage-users/provider-search.resource.ts +34 -0
  121. package/src/components/users/manage-users/sections/demographic-section.component.tsx +156 -0
  122. package/src/components/users/manage-users/sections/login-section.component.tsx +88 -0
  123. package/src/components/users/manage-users/sections/provider-section.component.tsx +270 -0
  124. package/src/components/users/manage-users/sections/roles-section.component.tsx +88 -0
  125. package/src/components/users/manage-users/user-details/user-detail.scss +75 -0
  126. package/src/components/users/manage-users/user-details/user-details.component.tsx +182 -0
  127. package/src/components/users/manage-users/user-list/user-list.component.tsx +378 -0
  128. package/src/components/users/manage-users/user-list/user-list.resource.ts +30 -0
  129. package/src/components/users/manage-users/user-list/user-list.scss +37 -0
  130. package/src/components/users/manage-users/user-management.constants.ts +20 -0
  131. package/src/components/users/manage-users/user-management.utils.ts +100 -0
  132. package/src/components/users/manage-users/user-management.workspace.scss +172 -0
  133. package/src/components/users/manage-users/user-management.workspace.tsx +334 -0
  134. package/src/components/users/userManagementFormSchema.tsx +179 -0
  135. package/src/config-schema.ts +142 -0
  136. package/src/constants.ts +50 -0
  137. package/src/declarations.d.ts +2 -0
  138. package/src/index.ts +55 -0
  139. package/src/left-pannel-link.component.tsx +40 -0
  140. package/src/root.component.tsx +39 -0
  141. package/src/root.scss +12 -0
  142. package/src/routes.json +100 -0
  143. package/src/setup-tests.ts +1 -0
  144. package/src/types/index.ts +385 -0
  145. package/src/user-management.resources.ts +232 -0
  146. package/src/utils/utils.ts +20 -0
  147. package/translations/am.json +159 -0
  148. package/translations/en.json +159 -0
  149. package/tsconfig.json +5 -0
@@ -0,0 +1,87 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .omrs-main-content {
6
+ background-color: colors.$white;
7
+ }
8
+
9
+ .bottomBorder {
10
+ margin-bottom: layout.$spacing-05;
11
+ }
12
+
13
+ .btnLayer {
14
+ display: flex;
15
+ padding-top: layout.$spacing-05;
16
+ padding-right: layout.$spacing-05;
17
+ padding-bottom: layout.$spacing-05;
18
+ margin-top: layout.$spacing-05;
19
+ flex-direction: row;
20
+ justify-content: flex-end;
21
+ background-color: white;
22
+ width: 100%;
23
+ }
24
+
25
+ .tableLayer {
26
+ padding-left: layout.$spacing-05;
27
+ padding-right: layout.$spacing-05;
28
+ background: white;
29
+ padding-top: layout.$spacing-01;
30
+ }
31
+
32
+ .loading {
33
+ display: flex;
34
+ padding-top: layout.$spacing-05;
35
+ padding-right: layout.$spacing-05;
36
+ padding-bottom: layout.$spacing-05;
37
+ margin-top: layout.$spacing-05;
38
+ flex-direction: row;
39
+ justify-content: flex-end;
40
+ background-color: white;
41
+ width: 100%;
42
+ }
43
+
44
+ .facilityInfoContainer {
45
+ margin: layout.$spacing-05;
46
+ }
47
+
48
+ .card {
49
+ padding: 1rem;
50
+ background-color: colors.$gray-10;
51
+ border: 1px solid #e0e0e0;
52
+ border-radius: 2px;
53
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
54
+ }
55
+
56
+ .cardTitle {
57
+ margin-bottom: 1rem;
58
+ font-size: 1.25rem;
59
+ font-weight: 600;
60
+ }
61
+
62
+ .cardDivider {
63
+ border: none;
64
+ border-top: 1px solid #e0e0e0;
65
+ margin: 0.5rem 0;
66
+ }
67
+ .cardContent {
68
+ font-size: 1rem;
69
+ line-height: 1.8;
70
+ }
71
+
72
+ .cardContent p {
73
+ display: flex;
74
+ justify-content: space-between;
75
+ margin-bottom: 0.5rem;
76
+ }
77
+ .cardContent br {
78
+ display: flex;
79
+ justify-content: space-between;
80
+ margin-bottom: 0.5rem;
81
+ }
82
+
83
+ .cardContent strong {
84
+ min-width: 150px;
85
+ display: inline-block;
86
+ color: #393939;
87
+ }
@@ -0,0 +1,21 @@
1
+ import { Layer } from '@carbon/react';
2
+ import React from 'react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import FacilityInfo from './facility-info.component';
5
+ import styles from './facility-setup.scss';
6
+ import Header from './header/header.component';
7
+
8
+ const FacilitySetup: React.FC = () => {
9
+ const { t } = useTranslation();
10
+ return (
11
+ <div className="omrs-main-content">
12
+ <Header title={t('facilityDetails', 'Facility Details')} />
13
+
14
+ <Layer className={styles.tableLayer}>
15
+ <FacilityInfo />
16
+ </Layer>
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default FacilitySetup;
@@ -0,0 +1,7 @@
1
+ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+
3
+ export const syncPackagesAndInterventions = async () => {
4
+ const packagesSyncURL = `${restBaseUrl}/kenyaemr/sha-benefits-package?synchronize=true`;
5
+ const intervesionSyncUrl = `${restBaseUrl}/kenyaemr/sha-interventions?synchronize=true`;
6
+ await Promise.all([packagesSyncURL, intervesionSyncUrl].map((url) => openmrsFetch(url)));
7
+ };
@@ -0,0 +1,38 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .omrs-main-content {
6
+ background-color: white;
7
+ }
8
+
9
+ .btnLayer {
10
+ display: flex;
11
+ padding-top: layout.$spacing-05;
12
+ padding-right: layout.$spacing-05;
13
+ padding-bottom: layout.$spacing-05;
14
+ margin-top: layout.$spacing-05;
15
+ flex-direction: row;
16
+ justify-content: flex-end;
17
+ background-color: white;
18
+ width: 100%;
19
+ }
20
+
21
+ .tableLayer {
22
+ padding-left: layout.$spacing-05;
23
+ padding-right: layout.$spacing-05;
24
+ background: white;
25
+ padding-top: layout.$spacing-01;
26
+ }
27
+
28
+ .loading {
29
+ display: flex;
30
+ padding-top: layout.$spacing-05;
31
+ padding-right: layout.$spacing-05;
32
+ padding-bottom: layout.$spacing-05;
33
+ margin-top: layout.$spacing-05;
34
+ flex-direction: row;
35
+ justify-content: flex-end;
36
+ background-color: white;
37
+ width: 100%;
38
+ }
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Development } from '@carbon/react/icons';
4
+ import { useSession, PageHeader } from '@openmrs/esm-framework';
5
+ import styles from './header.scss';
6
+
7
+ interface HeaderProps {
8
+ title: string;
9
+ }
10
+
11
+ const Header: React.FC<HeaderProps> = ({ title }) => {
12
+ const { t } = useTranslation();
13
+ const session = useSession();
14
+ const location = session?.sessionLocation?.display;
15
+
16
+ return (
17
+ <div className={styles.header}>
18
+ <PageHeader title={title} illustration={<Development size={32} />} className={styles.header} />
19
+ </div>
20
+ );
21
+ };
22
+
23
+ export default Header;
@@ -0,0 +1,19 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .header {
6
+ @include type.type-style('body-compact-02');
7
+ height: layout.$spacing-12;
8
+ display: flex;
9
+ justify-content: space-between;
10
+ padding: layout.$spacing-05;
11
+ background: white;
12
+ border: 1px solid colors.$gray-20;
13
+ }
14
+ .svgContainer svg {
15
+ width: layout.$spacing-10;
16
+ height: layout.$spacing-10;
17
+ margin-right: layout.$spacing-06;
18
+ fill: var(--brand-03);
19
+ }
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import styles from './header.scss';
3
+ import { IbmCloudant } from '@carbon/react/icons';
4
+
5
+ const ETLIllustration: React.FC = () => {
6
+ return (
7
+ <div className={styles.svgContainer}>
8
+ <IbmCloudant className={styles.iconOveriders} />
9
+ </div>
10
+ );
11
+ };
12
+
13
+ export default ETLIllustration;
@@ -0,0 +1,28 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { Calendar, Location } from '@carbon/react/icons';
4
+ import { formatDate, useSession, PageHeader } from '@openmrs/esm-framework';
5
+ import styles from './header.scss';
6
+ import ETLIllustration from './header-illustration.component';
7
+
8
+ interface HeaderProps {
9
+ title: string;
10
+ }
11
+
12
+ const Header: React.FC<HeaderProps> = ({ title }) => {
13
+ const { t } = useTranslation();
14
+ const session = useSession();
15
+ const location = session?.sessionLocation?.display;
16
+
17
+ return (
18
+ <div className={styles.header}>
19
+ <PageHeader
20
+ title={t('etlAdministration', 'ETL Administration')}
21
+ illustration={<ETLIllustration />}
22
+ className={styles.header}
23
+ />
24
+ </div>
25
+ );
26
+ };
27
+
28
+ export default Header;
@@ -0,0 +1,19 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@carbon/colors';
4
+
5
+ .header {
6
+ @include type.type-style('body-compact-02');
7
+ height: layout.$spacing-12;
8
+ display: flex;
9
+ justify-content: space-between;
10
+ padding: layout.$spacing-05;
11
+ background: white;
12
+ border: 1px solid colors.$gray-20;
13
+ }
14
+ .svgContainer svg {
15
+ width: layout.$spacing-10;
16
+ height: layout.$spacing-10;
17
+ margin-right: layout.$spacing-06;
18
+ fill: var(--brand-03);
19
+ }
@@ -0,0 +1,213 @@
1
+ import { FetchResponse, makeUrl, openmrsFetch, restBaseUrl } from '@openmrs/esm-framework';
2
+ import { HWR_API_NO_CREDENTIALS, PROVIDER_NOT_FOUND, RESOURCE_NOT_FOUND, UNKNOWN } from '../../constants';
3
+ import { CustomHIEPractitionerResponse, PractitionerResponse } from '../../types';
4
+ import { useState } from 'react';
5
+
6
+ /**
7
+ * Unified response type that can handle both FHIR and custom HIE formats
8
+ */
9
+ export interface UnifiedHealthWorkerResponse {
10
+ fhirFormat: boolean;
11
+ data: PractitionerResponse | CustomHIEPractitionerResponse;
12
+ }
13
+
14
+ /**
15
+ * Adapter to normalize health worker data based on response format
16
+ */
17
+ export class HealthWorkerAdapter {
18
+ /**
19
+ * Extracts common practitioner information from either response format
20
+ */
21
+ static normalize(response: UnifiedHealthWorkerResponse): NormalizedPractitioner {
22
+ if (response.fhirFormat) {
23
+ return this.normalizeFHIRResponse(response.data as PractitionerResponse);
24
+ } else {
25
+ return this.normalizeCustomResponse(response.data as CustomHIEPractitionerResponse);
26
+ }
27
+ }
28
+
29
+ private static normalizeFHIRResponse(data: PractitionerResponse): NormalizedPractitioner {
30
+ const resource = data.entry?.[0]?.resource;
31
+ if (!resource) {
32
+ throw new Error('Invalid FHIR response: No resource found');
33
+ }
34
+
35
+ const name = resource.name?.[0];
36
+ const identifierByType = (code: string) =>
37
+ resource.identifier?.find((id) => id.type?.coding?.[0]?.code === code)?.value;
38
+
39
+ const primaryQualification = resource.qualification?.[0];
40
+ const licenseExtension = primaryQualification?.extension?.find(
41
+ (ext) => ext.url.includes('license') || ext.url.includes('registration'),
42
+ );
43
+
44
+ return {
45
+ id: resource.id,
46
+ fullName: name?.text || '',
47
+ firstName: name?.text?.split(' ')[0] || '',
48
+ middleName: name?.text?.split(' ')[1] || '',
49
+ lastName: name?.text?.split(' ')[2] || name?.text?.split(' ')[1] || '',
50
+ gender: resource.gender || '',
51
+ registrationId: identifierByType('registration') || '',
52
+ externalReferenceId: identifierByType('license') || '',
53
+ nationalId: identifierByType('national_id') || '',
54
+ passportNumber: identifierByType('passport') || '',
55
+ licenseNumber: identifierByType('license') || '',
56
+ licensingBody: licenseExtension?.valueCoding?.display || '',
57
+ specialty: primaryQualification?.code?.coding?.[0]?.display || '',
58
+ qualification: primaryQualification?.code?.coding?.[0]?.display || '',
59
+ phoneNumber: resource.telecom?.find((t) => t.system === 'phone')?.value || '',
60
+ email: resource.telecom?.find((t) => t.system === 'email')?.value || '',
61
+ licenseStartDate: primaryQualification?.period?.start || '',
62
+ licenseEndDate: primaryQualification?.period?.end || '',
63
+ isActive: resource.active,
64
+ providerUniqueIdentifier: resource.id,
65
+ status: resource.active ? 'active' : 'inactive',
66
+ };
67
+ }
68
+
69
+ private static normalizeCustomResponse(data: CustomHIEPractitionerResponse): NormalizedPractitioner {
70
+ const { membership, licenses, contacts, identifiers, professional_details } = data.message;
71
+
72
+ const mostRecentLicense = licenses
73
+ ?.filter((l) => l.license_end)
74
+ .sort((a, b) => new Date(b.license_end).getTime() - new Date(a.license_end).getTime())[0];
75
+
76
+ return {
77
+ id: membership.id,
78
+ fullName: membership.full_name,
79
+ firstName: membership.first_name,
80
+ middleName: membership.middle_name,
81
+ lastName: membership.last_name,
82
+ gender: membership.gender,
83
+ registrationId: membership.registration_id,
84
+ externalReferenceId: membership.external_reference_id,
85
+ nationalId: identifiers.identification_number,
86
+ passportNumber: '',
87
+ licenseNumber: membership.external_reference_id,
88
+ licensingBody: membership.licensing_body,
89
+ specialty: membership.specialty || professional_details?.specialty,
90
+ qualification: professional_details?.educational_qualifications || membership.specialty,
91
+ phoneNumber: contacts.phone,
92
+ email: contacts.email,
93
+ licenseStartDate: mostRecentLicense?.license_start || '',
94
+ licenseEndDate: mostRecentLicense?.license_end || '',
95
+ isActive: membership.is_active === 1,
96
+ providerUniqueIdentifier: membership.id,
97
+ status: membership.status,
98
+ professionalCadre: professional_details?.professional_cadre,
99
+ practiceType: professional_details?.practice_type,
100
+ licenses: licenses,
101
+ };
102
+ }
103
+
104
+ /**
105
+ * Type guard to check if response is FHIR format
106
+ */
107
+ static isFHIRFormat(response: any): response is PractitionerResponse {
108
+ return response.resourceType === 'Bundle' || (response.entry && Array.isArray(response.entry));
109
+ }
110
+ }
111
+
112
+ /**
113
+ * Normalized practitioner data structure
114
+ */
115
+ export interface NormalizedPractitioner {
116
+ id: string;
117
+ fullName: string;
118
+ firstName: string;
119
+ middleName: string;
120
+ lastName: string;
121
+ gender: string;
122
+ registrationId: string;
123
+ externalReferenceId: string;
124
+ nationalId: string;
125
+ passportNumber: string;
126
+ licenseNumber: string;
127
+ licensingBody: string;
128
+ specialty: string;
129
+ qualification: string;
130
+ phoneNumber: string;
131
+ email: string;
132
+ licenseStartDate: string;
133
+ licenseEndDate: string;
134
+ isActive: boolean;
135
+ providerUniqueIdentifier: string;
136
+ status: string;
137
+ professionalCadre?: string;
138
+ practiceType?: string;
139
+ licenses?: Array<{
140
+ id: string;
141
+ external_reference_id: string;
142
+ license_type: string;
143
+ license_start: string;
144
+ license_end: string;
145
+ }>;
146
+ }
147
+
148
+ /**
149
+ * Search for health care worker with automatic format detection
150
+ */
151
+ export const searchHealthCareWork = async (
152
+ identifierType: string,
153
+ identifierNumber: string,
154
+ regulator: string,
155
+ ): Promise<UnifiedHealthWorkerResponse> => {
156
+ const url = `${restBaseUrl}/kenyaemr/practitionersearch?identifierType=${identifierType}&identifierNumber=${identifierNumber}&regulator=${regulator}`;
157
+
158
+ const response = await fetch(makeUrl(url));
159
+
160
+ if (!response.ok) {
161
+ if (response.status === 401) {
162
+ throw new Error(HWR_API_NO_CREDENTIALS);
163
+ } else if (response.status === 404) {
164
+ throw new Error(RESOURCE_NOT_FOUND);
165
+ }
166
+ throw new Error(UNKNOWN);
167
+ }
168
+
169
+ const responseData = await response.json();
170
+
171
+ // Check for error in response
172
+ if (responseData?.issue) {
173
+ throw new Error(PROVIDER_NOT_FOUND);
174
+ }
175
+
176
+ // Determine format based on fhirFormat flag or structure
177
+ const isFhir = responseData.fhirFormat === true || HealthWorkerAdapter.isFHIRFormat(responseData);
178
+
179
+ return {
180
+ fhirFormat: isFhir,
181
+ data: responseData,
182
+ };
183
+ };
184
+
185
+ /**
186
+ * Hook for searching health care worker with normalized data
187
+ */
188
+ export const useHealthWorkerSearch = () => {
189
+ const [isSearching, setIsSearching] = useState(false);
190
+ const [error, setError] = useState<string | null>(null);
191
+
192
+ const search = async (
193
+ identifierType: string,
194
+ identifierNumber: string,
195
+ regulator: string,
196
+ ): Promise<NormalizedPractitioner | null> => {
197
+ try {
198
+ setIsSearching(true);
199
+ setError(null);
200
+
201
+ const response = await searchHealthCareWork(identifierType, identifierNumber, regulator);
202
+ return HealthWorkerAdapter.normalize(response);
203
+ } catch (err) {
204
+ const errorMessage = err instanceof Error ? err.message : UNKNOWN;
205
+ setError(errorMessage);
206
+ return null;
207
+ } finally {
208
+ setIsSearching(false);
209
+ }
210
+ };
211
+
212
+ return { search, isSearching, error };
213
+ };
@@ -0,0 +1,37 @@
1
+ import { FetchResponse, openmrsFetch, restBaseUrl, useSession } from '@openmrs/esm-framework';
2
+ import useSWR from 'swr';
3
+ import { DefaultFacility, SHAFacility } from '../../types';
4
+
5
+ export function useShaFacilityInfo(shouldSynchronize: boolean = false) {
6
+ const { authenticated } = useSession();
7
+ const url = `${restBaseUrl}/kenyaemr/sha-facility-status?synchronize=${shouldSynchronize}`;
8
+
9
+ const { data, isLoading, error, mutate } = useSWR<FetchResponse<SHAFacility>>(
10
+ authenticated ? url : null,
11
+ openmrsFetch,
12
+ );
13
+
14
+ return {
15
+ isLoading,
16
+ shaFacility: data?.data,
17
+ error,
18
+ mutate,
19
+ };
20
+ }
21
+
22
+ export const useLocalFacilityInfo = () => {
23
+ const url = `${restBaseUrl}/kenyaemr/default-facility`;
24
+ const { authenticated } = useSession();
25
+
26
+ const { data, isLoading, error, mutate } = useSWR<FetchResponse<DefaultFacility>>(
27
+ authenticated ? url : null,
28
+ openmrsFetch,
29
+ );
30
+
31
+ return {
32
+ isLoading,
33
+ localFacility: data?.data,
34
+ error,
35
+ mutate,
36
+ };
37
+ };
@@ -0,0 +1,33 @@
1
+ import { OpenmrsResource, openmrsFetch } from '@openmrs/esm-framework';
2
+ import useSWRImmutable from 'swr/immutable';
3
+
4
+ export interface RoleCategory {
5
+ category: string;
6
+ roles: Array<string>;
7
+ }
8
+
9
+ export function useSystemUserRoleConfigSetting() {
10
+ const { data, error, isLoading, mutate } = useSWRImmutable<{ data: { results: Array<OpenmrsResource> } }, Error>(
11
+ `/ws/rest/v1/systemsetting?q=kenyaemr.userRole.config&v=custom:(uuid,value,property)`,
12
+ openmrsFetch,
13
+ {
14
+ revalidateOnFocus: false,
15
+ revalidateOnReconnect: false,
16
+ },
17
+ );
18
+
19
+ const userRolesConfigResource = data?.data?.results?.find(
20
+ (resource) => resource.property === 'kenyaemr.userRole.config',
21
+ );
22
+
23
+ let rolesConfig: RoleCategory[] = [];
24
+ if (userRolesConfigResource?.value) {
25
+ try {
26
+ rolesConfig = JSON.parse(userRolesConfigResource.value) as RoleCategory[];
27
+ } catch (error) {
28
+ console.error('Error parsing roles configuration:', error);
29
+ }
30
+ }
31
+
32
+ return { rolesConfig, isLoading, mutate, error };
33
+ }
@@ -0,0 +1,149 @@
1
+ import { InlineLoading, Layer, Search } from '@carbon/react';
2
+ import classNames from 'classnames';
3
+ import React, { type HTMLAttributes, useEffect, useRef, useState } from 'react';
4
+ import { useTranslation } from 'react-i18next';
5
+ import styles from './autosuggest.scss';
6
+
7
+ type InputPropsBase = Omit<HTMLAttributes<HTMLInputElement>, 'onChange'>;
8
+
9
+ interface SearchProps extends InputPropsBase {
10
+ autoComplete?: string;
11
+ className?: string;
12
+ closeButtonLabelText?: string;
13
+ defaultValue?: string | number;
14
+ disabled?: boolean;
15
+ isExpanded?: boolean;
16
+ id?: string;
17
+ labelText: React.ReactNode;
18
+ onChange?(e: { target: HTMLInputElement; type: 'change' }): void;
19
+ onClear?(): void;
20
+ onExpand?(e: React.MouseEvent<HTMLDivElement> | React.KeyboardEvent<HTMLDivElement>): void;
21
+ placeholder?: string;
22
+ renderIcon?: React.ComponentType | React.FunctionComponent;
23
+ role?: string;
24
+ size?: 'sm' | 'md' | 'lg';
25
+ type?: string;
26
+ value?: string | number;
27
+ }
28
+
29
+ interface AutosuggestProps extends SearchProps {
30
+ getDisplayValue: (item: any) => string;
31
+ getFieldValue: (item: any) => string;
32
+ getSearchResults: (query: string) => Promise<any>;
33
+ onSuggestionSelected: (field: string, value: string) => void;
34
+ invalid?: boolean;
35
+ invalidText?: string;
36
+ renderSuggestionItem?: (item: any) => React.ReactNode;
37
+ renderEmptyState?: (value: any) => React.ReactNode;
38
+ }
39
+
40
+ export const Autosuggest: React.FC<AutosuggestProps> = ({
41
+ getDisplayValue,
42
+ getFieldValue,
43
+ getSearchResults,
44
+ onSuggestionSelected,
45
+ invalid,
46
+ invalidText,
47
+ renderSuggestionItem,
48
+ renderEmptyState,
49
+ ...searchProps
50
+ }) => {
51
+ const { t } = useTranslation();
52
+ const [suggestions, setSuggestions] = useState<any[]>([]);
53
+ const [isLoading, setIsLoading] = useState(false);
54
+ const searchBox = useRef<HTMLInputElement>(null);
55
+ const wrapper = useRef<HTMLDivElement>(null);
56
+ const { id: name, labelText } = searchProps;
57
+
58
+ useEffect(() => {
59
+ const handleClickOutsideComponent = (e: MouseEvent) => {
60
+ if (wrapper.current && !wrapper.current.contains(e.target as Node)) {
61
+ setSuggestions([]);
62
+ }
63
+ };
64
+
65
+ document.addEventListener('mousedown', handleClickOutsideComponent);
66
+ return () => {
67
+ document.removeEventListener('mousedown', handleClickOutsideComponent);
68
+ };
69
+ }, [wrapper]);
70
+
71
+ const handleChange = (e) => {
72
+ const query = e.target.value;
73
+ onSuggestionSelected(name, undefined);
74
+
75
+ if (query && query.trim()) {
76
+ setIsLoading(true);
77
+ getSearchResults(query.trim())
78
+ .then((results) => {
79
+ setSuggestions(results || []);
80
+ setIsLoading(false);
81
+ })
82
+ .catch((error) => {
83
+ setSuggestions([]);
84
+ setIsLoading(false);
85
+ });
86
+ } else {
87
+ setSuggestions([]);
88
+ setIsLoading(false);
89
+ }
90
+ };
91
+
92
+ const handleClear = () => {
93
+ onSuggestionSelected(name, undefined);
94
+ setSuggestions([]);
95
+ setIsLoading(false);
96
+ };
97
+
98
+ const handleClick = (index: number) => {
99
+ const selectedSuggestion = suggestions[index];
100
+ if (selectedSuggestion) {
101
+ const fieldValue = getFieldValue(selectedSuggestion);
102
+ const displayValue = getDisplayValue(selectedSuggestion);
103
+ if (searchBox.current) {
104
+ searchBox.current.value = displayValue;
105
+ }
106
+ onSuggestionSelected(name, fieldValue);
107
+ setSuggestions([]);
108
+ }
109
+ };
110
+
111
+ return (
112
+ <div className={styles.autocomplete} ref={wrapper}>
113
+ <label className="cds--label">{labelText}</label>
114
+ <Layer className={classNames({ [styles.invalid]: invalid })}>
115
+ <Search
116
+ id="autosuggest"
117
+ onChange={handleChange}
118
+ onClear={handleClear}
119
+ ref={searchBox}
120
+ className={styles.autocompleteSearch}
121
+ {...searchProps}
122
+ />
123
+ </Layer>
124
+ {isLoading && (
125
+ <div className={styles.loading}>
126
+ <InlineLoading status="active" description={t('loading', 'Loading' + '...')} />
127
+ </div>
128
+ )}
129
+ {suggestions.length > 0 ? (
130
+ <ul className={styles.suggestions}>
131
+ {suggestions.map((suggestion, index) => {
132
+ return (
133
+ <li
134
+ key={getFieldValue(suggestion) || index}
135
+ onClick={() => handleClick(index)}
136
+ role="presentation"
137
+ style={{ cursor: 'pointer' }}>
138
+ {renderSuggestionItem ? renderSuggestionItem(suggestion) : getDisplayValue(suggestion)}
139
+ </li>
140
+ );
141
+ })}
142
+ </ul>
143
+ ) : (
144
+ !isLoading && renderEmptyState && renderEmptyState(searchBox.current?.value || '')
145
+ )}
146
+ {invalid && <label className={classNames(styles.invalidMsg)}>{invalidText}</label>}
147
+ </div>
148
+ );
149
+ };