@kenyaemr/esm-admin-app 5.4.4-pre.142 → 5.4.4-pre.15

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 (279) hide show
  1. package/package.json +1 -1
  2. package/src/components/confirm-modal/confirmation-operation.test.tsx +8 -19
  3. package/src/components/dashboard/dashboard.component.tsx +4 -1
  4. package/src/components/empty-state/empty-state-log.test.tsx +3 -4
  5. package/src/components/facility-setup/constant/index.ts +3 -0
  6. package/src/components/facility-setup/facility-info.component.tsx +176 -106
  7. package/src/components/facility-setup/facility-info.scss +39 -1
  8. package/src/components/facility-setup/facility-setup.component.tsx +2 -2
  9. package/src/components/facility-setup/type/index.ts +61 -0
  10. package/src/components/facility-setup/useFacilityRegistry.ts +29 -0
  11. package/src/components/global-property/dashboard/global-property-dashboard.component.tsx +23 -0
  12. package/src/components/global-property/dashboard/global-property-dashboard.scss +6 -0
  13. package/src/components/global-property/hooks/useGlobalProperty.ts +64 -0
  14. package/src/components/global-property/index.ts +14 -0
  15. package/src/components/global-property/modal/delete-global-property-modal.component.tsx +71 -0
  16. package/src/components/global-property/modal/delete-global-property-modal.test.tsx +131 -0
  17. package/src/components/global-property/table/global-property-table.component.tsx +249 -0
  18. package/src/components/global-property/table/global-property-table.scss +34 -0
  19. package/src/components/global-property/table/global-property-table.test.tsx +198 -0
  20. package/src/components/global-property/workspace/global-property-form-schema.ts +32 -0
  21. package/src/components/global-property/workspace/global-property.workspace.scss +40 -0
  22. package/src/components/global-property/workspace/global-property.workspace.test.tsx +172 -0
  23. package/src/components/global-property/workspace/global-property.workspace.tsx +260 -0
  24. package/src/components/hook/healthWorkerRegistry.ts +78 -0
  25. package/src/components/hook/useProfessionalRegistryEnums.ts +59 -0
  26. package/src/components/logs-table/operation-log-table.component.tsx +87 -75
  27. package/src/components/logs-table/operation-log.test.tsx +134 -28
  28. package/src/components/modal/hwr-confirmation.modal.scss +80 -4
  29. package/src/components/modal/hwr-confirmation.modal.tsx +118 -128
  30. package/src/components/modal/hwr-sync.modal.tsx +194 -106
  31. package/src/components/users/manage-users/user-details/user-detail.scss +167 -39
  32. package/src/components/users/manage-users/user-details/user-details.component.tsx +130 -122
  33. package/src/components/users/manage-users/user-management.workspace.scss +233 -95
  34. package/src/components/users/manage-users/user-management.workspace.tsx +481 -375
  35. package/src/components/users/userManagementFormSchema.tsx +17 -8
  36. package/src/config-schema.ts +48 -68
  37. package/src/index.ts +9 -1
  38. package/src/root.component.tsx +2 -0
  39. package/src/routes.json +20 -1
  40. package/src/types/index.ts +11 -2
  41. package/translations/am.json +175 -10
  42. package/translations/en.json +108 -40
  43. package/translations/fr.json +171 -101
  44. package/translations/sw.json +312 -147
  45. package/.turbo/turbo-build.log +0 -6
  46. package/dist/1074.js +0 -1
  47. package/dist/1074.js.map +0 -1
  48. package/dist/12.js +0 -17
  49. package/dist/12.js.map +0 -1
  50. package/dist/1201.js +0 -1
  51. package/dist/1201.js.map +0 -1
  52. package/dist/1242.js +0 -1
  53. package/dist/1242.js.map +0 -1
  54. package/dist/1311.js +0 -1
  55. package/dist/1311.js.map +0 -1
  56. package/dist/1462.js +0 -1
  57. package/dist/1462.js.map +0 -1
  58. package/dist/1469.js +0 -1
  59. package/dist/1469.js.map +0 -1
  60. package/dist/1506.js +0 -13
  61. package/dist/1506.js.map +0 -1
  62. package/dist/1718.js +0 -1
  63. package/dist/1718.js.map +0 -1
  64. package/dist/1722.js +0 -1
  65. package/dist/1722.js.map +0 -1
  66. package/dist/1772.js +0 -1
  67. package/dist/1772.js.map +0 -1
  68. package/dist/1889.js +0 -1
  69. package/dist/1889.js.map +0 -1
  70. package/dist/1972.js +0 -1
  71. package/dist/1972.js.map +0 -1
  72. package/dist/1990.js +0 -1
  73. package/dist/1990.js.map +0 -1
  74. package/dist/2016.js +0 -1
  75. package/dist/2016.js.map +0 -1
  76. package/dist/2080.js +0 -1
  77. package/dist/2080.js.map +0 -1
  78. package/dist/2096.js +0 -1
  79. package/dist/2096.js.map +0 -1
  80. package/dist/2153.js +0 -1
  81. package/dist/2153.js.map +0 -1
  82. package/dist/216.js +0 -1
  83. package/dist/216.js.map +0 -1
  84. package/dist/2270.js +0 -1
  85. package/dist/2270.js.map +0 -1
  86. package/dist/2294.js +0 -1
  87. package/dist/2294.js.map +0 -1
  88. package/dist/2345.js +0 -1
  89. package/dist/2345.js.map +0 -1
  90. package/dist/2402.js +0 -1
  91. package/dist/2402.js.map +0 -1
  92. package/dist/2500.js +0 -1
  93. package/dist/2500.js.map +0 -1
  94. package/dist/251.js +0 -1
  95. package/dist/251.js.map +0 -1
  96. package/dist/257.js +0 -1
  97. package/dist/257.js.map +0 -1
  98. package/dist/2586.js +0 -1
  99. package/dist/2586.js.map +0 -1
  100. package/dist/2625.js +0 -1
  101. package/dist/2625.js.map +0 -1
  102. package/dist/2652.js +0 -1
  103. package/dist/2652.js.map +0 -1
  104. package/dist/2685.js +0 -1
  105. package/dist/2685.js.map +0 -1
  106. package/dist/2948.js +0 -1
  107. package/dist/2948.js.map +0 -1
  108. package/dist/3089.js +0 -1
  109. package/dist/3089.js.map +0 -1
  110. package/dist/3190.js +0 -1
  111. package/dist/3190.js.map +0 -1
  112. package/dist/3224.js +0 -1
  113. package/dist/3224.js.map +0 -1
  114. package/dist/3366.js +0 -1
  115. package/dist/3366.js.map +0 -1
  116. package/dist/349.js +0 -1
  117. package/dist/349.js.map +0 -1
  118. package/dist/3548.js +0 -1
  119. package/dist/3548.js.map +0 -1
  120. package/dist/3571.js +0 -1
  121. package/dist/3571.js.map +0 -1
  122. package/dist/3691.js +0 -1
  123. package/dist/3691.js.map +0 -1
  124. package/dist/3775.js +0 -1
  125. package/dist/3775.js.map +0 -1
  126. package/dist/3816.js +0 -1
  127. package/dist/3816.js.map +0 -1
  128. package/dist/3852.js +0 -1
  129. package/dist/3852.js.map +0 -1
  130. package/dist/3906.js +0 -1
  131. package/dist/3906.js.map +0 -1
  132. package/dist/3963.js +0 -1
  133. package/dist/3963.js.map +0 -1
  134. package/dist/405.js +0 -1
  135. package/dist/405.js.map +0 -1
  136. package/dist/4296.js +0 -1
  137. package/dist/4296.js.map +0 -1
  138. package/dist/4337.js +0 -1
  139. package/dist/4337.js.map +0 -1
  140. package/dist/4735.js +0 -1
  141. package/dist/4735.js.map +0 -1
  142. package/dist/4744.js +0 -1
  143. package/dist/4744.js.map +0 -1
  144. package/dist/4813.js +0 -2
  145. package/dist/4813.js.map +0 -1
  146. package/dist/4858.js +0 -1
  147. package/dist/4858.js.map +0 -1
  148. package/dist/487.js +0 -1
  149. package/dist/487.js.map +0 -1
  150. package/dist/4970.js +0 -1
  151. package/dist/4970.js.map +0 -1
  152. package/dist/5202.js +0 -1
  153. package/dist/5202.js.map +0 -1
  154. package/dist/5294.js +0 -1
  155. package/dist/5294.js.map +0 -1
  156. package/dist/545.js +0 -1
  157. package/dist/545.js.map +0 -1
  158. package/dist/552.js +0 -1
  159. package/dist/552.js.map +0 -1
  160. package/dist/5592.js +0 -1
  161. package/dist/5592.js.map +0 -1
  162. package/dist/5669.js +0 -1
  163. package/dist/5669.js.map +0 -1
  164. package/dist/5884.js +0 -1
  165. package/dist/5884.js.map +0 -1
  166. package/dist/5940.js +0 -1
  167. package/dist/5940.js.map +0 -1
  168. package/dist/6092.js +0 -1
  169. package/dist/6092.js.map +0 -1
  170. package/dist/6155.js +0 -1
  171. package/dist/6155.js.map +0 -1
  172. package/dist/6178.js +0 -1
  173. package/dist/6178.js.map +0 -1
  174. package/dist/6399.js +0 -1
  175. package/dist/6399.js.map +0 -1
  176. package/dist/6456.js +0 -1
  177. package/dist/6466.js +0 -3
  178. package/dist/6466.js.map +0 -1
  179. package/dist/6492.js +0 -1
  180. package/dist/6492.js.map +0 -1
  181. package/dist/6676.js +0 -1
  182. package/dist/6676.js.map +0 -1
  183. package/dist/6800.js +0 -1
  184. package/dist/6800.js.map +0 -1
  185. package/dist/6976.js +0 -1
  186. package/dist/6976.js.map +0 -1
  187. package/dist/7005.js +0 -1
  188. package/dist/7005.js.map +0 -1
  189. package/dist/7201.js +0 -1
  190. package/dist/7201.js.map +0 -1
  191. package/dist/7210.js +0 -1
  192. package/dist/7210.js.map +0 -1
  193. package/dist/7234.js +0 -1
  194. package/dist/7234.js.map +0 -1
  195. package/dist/7261.js +0 -1
  196. package/dist/7261.js.map +0 -1
  197. package/dist/7326.js +0 -1
  198. package/dist/7463.js +0 -1
  199. package/dist/7463.js.map +0 -1
  200. package/dist/7528.js +0 -1
  201. package/dist/7528.js.map +0 -1
  202. package/dist/7584.js +0 -1
  203. package/dist/7584.js.map +0 -1
  204. package/dist/7607.js +0 -1
  205. package/dist/7717.js +0 -1
  206. package/dist/7717.js.map +0 -1
  207. package/dist/7737.js +0 -1
  208. package/dist/7737.js.map +0 -1
  209. package/dist/7739.js +0 -1
  210. package/dist/7739.js.map +0 -1
  211. package/dist/7765.js +0 -1
  212. package/dist/7765.js.map +0 -1
  213. package/dist/7820.js +0 -1
  214. package/dist/7820.js.map +0 -1
  215. package/dist/7844.js +0 -1
  216. package/dist/7844.js.map +0 -1
  217. package/dist/7866.js +0 -1
  218. package/dist/7866.js.map +0 -1
  219. package/dist/7916.js +0 -1
  220. package/dist/7916.js.map +0 -1
  221. package/dist/7971.js +0 -1
  222. package/dist/7971.js.map +0 -1
  223. package/dist/8159.js +0 -7
  224. package/dist/8159.js.map +0 -1
  225. package/dist/8244.js +0 -1
  226. package/dist/8244.js.map +0 -1
  227. package/dist/8262.js +0 -1
  228. package/dist/8262.js.map +0 -1
  229. package/dist/834.js +0 -1
  230. package/dist/834.js.map +0 -1
  231. package/dist/8376.js +0 -1
  232. package/dist/8376.js.map +0 -1
  233. package/dist/845.js +0 -1
  234. package/dist/845.js.map +0 -1
  235. package/dist/8546.js +0 -1
  236. package/dist/8546.js.map +0 -1
  237. package/dist/8570.js +0 -1
  238. package/dist/8570.js.map +0 -1
  239. package/dist/87.js +0 -1
  240. package/dist/87.js.map +0 -1
  241. package/dist/8727.js +0 -1
  242. package/dist/8828.js +0 -1
  243. package/dist/8828.js.map +0 -1
  244. package/dist/8860.js +0 -1
  245. package/dist/8860.js.map +0 -1
  246. package/dist/9036.js +0 -1
  247. package/dist/9036.js.map +0 -1
  248. package/dist/9124.js +0 -1
  249. package/dist/9124.js.map +0 -1
  250. package/dist/9182.js +0 -1
  251. package/dist/921.js +0 -1
  252. package/dist/921.js.map +0 -1
  253. package/dist/9404.js +0 -1
  254. package/dist/9404.js.map +0 -1
  255. package/dist/9406.js +0 -1
  256. package/dist/9406.js.map +0 -1
  257. package/dist/9446.js +0 -1
  258. package/dist/9446.js.map +0 -1
  259. package/dist/9449.js +0 -1
  260. package/dist/9449.js.map +0 -1
  261. package/dist/9566.js +0 -5
  262. package/dist/9566.js.map +0 -1
  263. package/dist/9641.js +0 -1
  264. package/dist/9641.js.map +0 -1
  265. package/dist/9711.js +0 -1
  266. package/dist/9711.js.map +0 -1
  267. package/dist/9801.js +0 -1
  268. package/dist/9801.js.map +0 -1
  269. package/dist/9835.js +0 -11
  270. package/dist/9835.js.map +0 -1
  271. package/dist/kenyaemr-esm-admin-app.js +0 -5
  272. package/dist/kenyaemr-esm-admin-app.js.buildmanifest.json +0 -3173
  273. package/dist/kenyaemr-esm-admin-app.js.map +0 -1
  274. package/dist/main.js +0 -5
  275. package/dist/main.js.map +0 -1
  276. package/dist/routes.json +0 -1
  277. package/src/components/facility-setup/facility-setup.resource.tsx +0 -7
  278. package/src/components/hook/healthWorkerAdapter.ts +0 -213
  279. package/src/components/hook/useFacilityInfo.tsx +0 -37
@@ -1,147 +1,218 @@
1
- import React, { useState } from 'react';
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
2
  import { useTranslation } from 'react-i18next';
3
3
  import { Button, Column, Search, ComboBox, InlineLoading } from '@carbon/react';
4
- import styles from './hwr-sync.modal.scss';
5
- import { useConfig, showSnackbar, formatDate, parseDate, showToast, restBaseUrl } from '@openmrs/esm-framework';
4
+ import { useConfig, showSnackbar, showToast, restBaseUrl } from '@openmrs/esm-framework';
6
5
  import { mutate } from 'swr';
7
- import { CustomHIEPractitionerResponse, type PractitionerResponse, type ProviderResponse } from '../../types';
6
+
7
+ import styles from './hwr-sync.modal.scss';
8
+ import { type ProviderResponse } from '../../types';
8
9
  import { ConfigObject } from '../../config-schema';
9
- import { searchHealthCareWork, HealthWorkerAdapter } from '../hook/healthWorkerAdapter';
10
10
  import { createProviderAttribute, updateProviderAttributes } from './hwr-sync.resource';
11
+ import { searchHealthCareWork, ProfessionalRegistryResponse } from '../hook/healthWorkerRegistry';
12
+ import {
13
+ useProfessionalRegistryIdentificationTypes,
14
+ useProfessionalRegistryRegulators,
15
+ } from '../hook/useProfessionalRegistryEnums';
11
16
 
12
17
  interface HWRSyncModalProps {
13
18
  close: () => void;
14
19
  provider: ProviderResponse;
15
20
  }
16
21
 
22
+ interface EnumEntry {
23
+ code: string;
24
+ label: string;
25
+ }
26
+
27
+ const pickCurrentLicense = (licenses: ProfessionalRegistryResponse['professional']['licenses']) => {
28
+ if (!licenses || licenses.length === 0) {
29
+ return undefined;
30
+ }
31
+ const sorted = [...licenses]
32
+ .filter((l) => l.license_end)
33
+ .sort((a, b) => new Date(b.license_end).getTime() - new Date(a.license_end).getTime());
34
+ const now = Date.now();
35
+ return sorted.find((l) => new Date(l.license_end).getTime() >= now) ?? sorted[0];
36
+ };
37
+
17
38
  const HWRSyncModal: React.FC<HWRSyncModalProps> = ({ close, provider }) => {
18
39
  const { t } = useTranslation();
19
40
  const [syncLoading, setSyncLoading] = useState(false);
20
41
 
21
- const config = useConfig<ConfigObject>();
22
42
  const {
23
- providerNationalIdUuid,
24
- licenseBodyUuid,
43
+ licenseNumberUuid,
25
44
  licenseExpiryDateUuid,
45
+ licenseBodyUuid,
46
+ qualificationUuid,
47
+ specialtyUuid,
48
+ providerCadreUuid,
49
+ practiceTypeUuid,
50
+ providerNationalIdUuid,
26
51
  passportNumberUuid,
27
- licenseNumberUuid,
28
- identifierTypes,
52
+ providerUniqueIdentifierAttributeTypeUuid,
53
+ externalProviderIdentifierUuid,
54
+ providerHieFhirReference,
29
55
  phoneNumberUuid,
30
- qualificationUuid,
31
56
  providerAddressUuid,
32
- providerHieFhirReference,
57
+ } = useConfig<ConfigObject>();
58
+ const { regulators } = useProfessionalRegistryRegulators();
59
+ const { identificationTypes } = useProfessionalRegistryIdentificationTypes();
60
+
61
+ const storedIdentifiers = useMemo(() => {
62
+ const attrValue = (uuid: string) => provider?.attributes?.find((a) => a.attributeType?.uuid === uuid)?.value || '';
63
+
64
+ return {
65
+ nationalId: attrValue(providerNationalIdUuid),
66
+ puid: attrValue(providerUniqueIdentifierAttributeTypeUuid),
67
+ externalRef: attrValue(externalProviderIdentifierUuid),
68
+ passport: attrValue(passportNumberUuid),
69
+ licenseBody: attrValue(licenseBodyUuid),
70
+ };
71
+ }, [
72
+ provider,
73
+ providerNationalIdUuid,
33
74
  providerUniqueIdentifierAttributeTypeUuid,
34
- regulatorOptions,
35
- } = config;
36
-
37
- const attributeMapping = {
38
- [identifierTypes[0]?.key]:
39
- provider.attributes.find((attr) => attr.attributeType.uuid === providerNationalIdUuid)?.value || '--',
40
- [identifierTypes[1]?.key]:
41
- provider.attributes.find((attr) => attr.attributeType.uuid === licenseBodyUuid)?.value || '--',
42
- [identifierTypes[2]?.key]:
43
- provider.attributes.find((attr) => attr.attributeType.uuid === passportNumberUuid)?.value || '--',
44
- };
75
+ externalProviderIdentifierUuid,
76
+ passportNumberUuid,
77
+ licenseBodyUuid,
78
+ ]);
79
+
80
+ const initialIdentifierType = useMemo(() => {
81
+ if (storedIdentifiers.nationalId) {
82
+ return 'National ID';
83
+ }
84
+ if (storedIdentifiers.externalRef) {
85
+ return 'registration_number';
86
+ }
87
+ if (storedIdentifiers.passport) {
88
+ return 'Passport';
89
+ }
90
+ return '';
91
+ }, [storedIdentifiers]);
92
+
93
+ const initialIdentifier = useMemo(() => {
94
+ if (storedIdentifiers.nationalId) {
95
+ return storedIdentifiers.nationalId;
96
+ }
97
+ if (storedIdentifiers.externalRef) {
98
+ return storedIdentifiers.externalRef;
99
+ }
100
+ if (storedIdentifiers.passport) {
101
+ return storedIdentifiers.passport;
102
+ }
103
+ return '';
104
+ }, [storedIdentifiers]);
105
+
106
+ const initialRegulator = storedIdentifiers.licenseBody;
45
107
 
46
108
  const [searchHWR, setSearchHWR] = useState({
47
- identifierType: identifierTypes[0]?.key,
48
- identifier: attributeMapping[identifierTypes[0]?.key],
49
- regulator: regulatorOptions[0]?.key,
109
+ identifierType: initialIdentifierType,
110
+ identifier: initialIdentifier,
111
+ regulator: initialRegulator,
50
112
  });
51
113
 
52
- const handleIdentifierTypeChange = (selectedItem: { key: string; name: string } | null) => {
53
- const selectedKey = selectedItem?.key ?? '';
114
+ useEffect(() => {
54
115
  setSearchHWR((prev) => ({
55
116
  ...prev,
56
- identifierType: selectedKey,
57
- identifier: attributeMapping[selectedKey] || '',
117
+ identifierType: prev.identifierType || initialIdentifierType,
118
+ identifier: prev.identifier || initialIdentifier,
119
+ regulator: prev.regulator || initialRegulator,
58
120
  }));
59
- };
121
+ }, [initialIdentifierType, initialIdentifier, initialRegulator]);
122
+
123
+ const handleIdentifierTypeChange = (newType: string) => {
124
+ const valueForType: Record<string, string> = {
125
+ 'National ID': storedIdentifiers.nationalId,
126
+ registration_number: storedIdentifiers.externalRef,
127
+ Passport: storedIdentifiers.passport,
128
+ };
60
129
 
61
- const handleRegulatorChange = (selectedItem: { key: string; name: string } | null) => {
62
- const selectedKey = selectedItem?.key ?? '';
63
130
  setSearchHWR((prev) => ({
64
131
  ...prev,
65
- regulator: selectedKey,
132
+ identifierType: newType,
133
+ identifier: valueForType[newType] ?? '',
66
134
  }));
67
135
  };
68
136
 
69
- const isSearchDisabled = () => !searchHWR.identifier;
137
+ const selectedIdentificationType = useMemo<EnumEntry | null>(
138
+ () => identificationTypes?.find((i) => i.code === searchHWR.identifierType) ?? null,
139
+ [identificationTypes, searchHWR.identifierType],
140
+ );
141
+
142
+ const selectedRegulator = useMemo<EnumEntry | null>(
143
+ () => regulators?.find((r) => r.code === searchHWR.regulator) ?? null,
144
+ [regulators, searchHWR.regulator],
145
+ );
146
+
147
+ const isSearchDisabled = () =>
148
+ !searchHWR.identifier || !searchHWR.identifierType || !searchHWR.regulator || syncLoading;
70
149
 
71
150
  const handleSync = async () => {
72
- try {
73
- setSyncLoading(true);
74
- const unifiedResponse = await searchHealthCareWork(
75
- searchHWR.identifierType,
76
- searchHWR.identifier,
77
- searchHWR.regulator,
78
- );
151
+ setSyncLoading(true);
79
152
 
80
- const normalizedData = HealthWorkerAdapter.normalize(unifiedResponse);
153
+ try {
154
+ const response = await searchHealthCareWork(searchHWR.identifierType, searchHWR.identifier, searchHWR.regulator);
81
155
 
82
- if (!normalizedData) {
156
+ if (!response?.professional) {
83
157
  throw new Error(t('noResults', 'No results found'));
84
158
  }
85
159
 
160
+ const { membership, contacts, identifiers, professional_details, licenses } = response.professional;
161
+ const currentLicense = pickCurrentLicense(licenses);
162
+
86
163
  const updatableAttributes = [
87
- { attributeType: licenseNumberUuid, value: normalizedData.licenseNumber },
88
- { attributeType: licenseBodyUuid, value: normalizedData.registrationId },
164
+ { attributeType: licenseNumberUuid, value: currentLicense?.external_reference_id },
89
165
  {
90
166
  attributeType: licenseExpiryDateUuid,
91
- value: normalizedData.licenseEndDate ? parseDate(normalizedData.licenseEndDate) : null,
92
- },
93
- { attributeType: phoneNumberUuid, value: normalizedData.phoneNumber },
94
- { attributeType: qualificationUuid, value: normalizedData.qualification },
95
- {
96
- attributeType: providerHieFhirReference,
97
- value: JSON.stringify({
98
- ...unifiedResponse.data,
99
- fhirFormat: unifiedResponse.fhirFormat,
100
- searchParameters: {
101
- regulator: searchHWR.regulator,
102
- identifierType: searchHWR.identifierType,
103
- },
104
- }),
105
- },
106
- { attributeType: providerAddressUuid, value: normalizedData.email },
107
- { attributeType: providerNationalIdUuid, value: normalizedData.nationalId },
108
- {
109
- attributeType: providerUniqueIdentifierAttributeTypeUuid,
110
- value: normalizedData.providerUniqueIdentifier,
167
+ value: currentLicense?.license_end ? new Date(currentLicense.license_end).toISOString() : null,
111
168
  },
112
- ].filter((attr) => attr.value !== undefined && attr.value !== null && attr.value !== '');
169
+ { attributeType: licenseBodyUuid, value: membership?.licensing_body },
170
+ { attributeType: qualificationUuid, value: professional_details?.educational_qualifications },
171
+ { attributeType: specialtyUuid, value: membership?.specialty },
172
+ { attributeType: providerCadreUuid, value: professional_details?.professional_cadre },
173
+ { attributeType: practiceTypeUuid, value: professional_details?.practice_type },
174
+
175
+ { attributeType: providerNationalIdUuid, value: identifiers?.identification_number },
176
+ { attributeType: providerUniqueIdentifierAttributeTypeUuid, value: membership?.id },
177
+ { attributeType: externalProviderIdentifierUuid, value: membership?.external_reference_id },
178
+ { attributeType: providerHieFhirReference, value: membership?.id },
179
+
180
+ { attributeType: phoneNumberUuid, value: contacts?.phone },
181
+ { attributeType: providerAddressUuid, value: contacts?.email },
182
+ ].filter(
183
+ (attr): attr is { attributeType: string; value: string } =>
184
+ attr.value !== undefined && attr.value !== null && attr.value !== '',
185
+ );
113
186
 
114
187
  await Promise.all(
115
188
  updatableAttributes.map((attr) => {
116
- const existingAttribute = provider.attributes.find(
117
- (at) => at.attributeType.uuid === attr.attributeType,
118
- )?.uuid;
119
-
120
- const payload = {
121
- attributeType: attr.attributeType,
122
- value: attr.value,
123
- };
124
-
125
- if (!existingAttribute) {
126
- return createProviderAttribute(payload, provider.uuid);
127
- }
128
- return updateProviderAttributes(payload, provider.uuid, existingAttribute);
189
+ const existing = provider.attributes?.find((at) => at.attributeType?.uuid === attr.attributeType)?.uuid;
190
+ const payload = { attributeType: attr.attributeType, value: attr.value };
191
+ return existing
192
+ ? updateProviderAttributes(payload, provider.uuid, existing)
193
+ : createProviderAttribute(payload, provider.uuid);
129
194
  }),
130
195
  );
131
196
 
132
197
  mutate((key) => typeof key === 'string' && key.startsWith(`${restBaseUrl}/provider`));
198
+
133
199
  showSnackbar({
134
- title: 'Success',
200
+ title: t('syncSuccess', 'Sync successful'),
135
201
  kind: 'success',
136
- subtitle: t('syncMessage', 'user details synced successfully'),
202
+ subtitle: t('syncMessage', 'Provider details synced from the registry'),
203
+ isLowContrast: true,
137
204
  });
205
+
138
206
  close();
139
- } catch (err) {
207
+ } catch (err: any) {
140
208
  showToast({
141
209
  critical: false,
142
210
  kind: 'error',
143
- description: t('errorSyncMsg', `Failed to sync the account with ${searchHWR.identifier}. ${err}`),
144
- title: t('hwrERROR', 'Sync Failed'),
211
+ description: t('errorSyncMsg', 'Failed to sync {{identifier}}: {{error}}', {
212
+ identifier: searchHWR.identifier,
213
+ error: err?.message ?? 'unknown error',
214
+ }),
215
+ title: t('hwrError', 'Sync failed'),
145
216
  });
146
217
  } finally {
147
218
  setSyncLoading(false);
@@ -153,53 +224,70 @@ const HWRSyncModal: React.FC<HWRSyncModalProps> = ({ close, provider }) => {
153
224
  <div className="cds--modal-header">
154
225
  <h3 className="cds--modal-header__heading">{t('healthWorkerRegistry', 'Health worker registry')}</h3>
155
226
  </div>
227
+
156
228
  <div className="cds--modal-content">
157
- <p>{t('healthWorkerSync', 'Health worker information to be synced with the registry.')}</p>
229
+ <p className={styles.intro}>
230
+ {t(
231
+ 'healthWorkerSync',
232
+ 'Look up this provider in the Kenya health worker registry and overwrite local attributes with registry values.',
233
+ )}
234
+ </p>
235
+
158
236
  <div className={styles.modalContainer}>
159
237
  <Column className={styles.identifierTypeColumn}>
160
238
  <ComboBox
161
- onChange={({ selectedItem }) => handleIdentifierTypeChange(selectedItem)}
162
- id="formIdentifierType"
239
+ id="syncIdentifierType"
163
240
  titleText={t('identificationType', 'Identification Type')}
164
241
  placeholder={t('chooseIdentifierType', 'Choose identifier type')}
165
- initialSelectedItem={identifierTypes.find((item) => item.key === searchHWR.identifierType)}
166
- items={identifierTypes}
167
- itemToString={(item) => (item ? item.name : '')}
168
- className={styles.ComboBox}
242
+ items={identificationTypes ?? []}
243
+ itemToString={(item) => item?.label ?? ''}
244
+ selectedItem={selectedIdentificationType}
245
+ onChange={({ selectedItem }) => handleIdentifierTypeChange(selectedItem?.code ?? '')}
246
+ className={styles.comboBox}
169
247
  />
170
248
  </Column>
249
+
171
250
  <Column className={styles.identifierTypeColumn}>
172
251
  <ComboBox
173
- onChange={({ selectedItem }) => handleRegulatorChange(selectedItem)}
174
- id="regulatorOptions"
252
+ id="syncRegulator"
175
253
  titleText={t('regulator', 'Regulator')}
176
254
  placeholder={t('chooseRegulator', 'Choose regulator')}
177
- initialSelectedItem={regulatorOptions.find((item) => item.key === searchHWR.regulator)}
178
- items={regulatorOptions}
179
- itemToString={(item) => (item ? item.name : '')}
180
- className={styles.ComboBox}
255
+ items={regulators ?? []}
256
+ itemToString={(item) => item?.label ?? ''}
257
+ selectedItem={selectedRegulator}
258
+ onChange={({ selectedItem }) =>
259
+ setSearchHWR((prev) => ({ ...prev, regulator: selectedItem?.code ?? '' }))
260
+ }
261
+ className={styles.comboBox}
181
262
  />
182
263
  </Column>
264
+
183
265
  <Column className={styles.identifierTypeColumn}>
184
266
  <span className={styles.identifierTypeHeader}>{t('identifierNumber', 'Identifier number*')}</span>
185
267
  <Search
268
+ id="syncSearch"
186
269
  labelText={t('enterIdentifierNumber', 'Enter identifier number')}
187
- className={styles.formSearch}
188
- value={searchHWR.identifier}
189
270
  placeholder={t('enterIdentifierNumber', 'Enter identifier number')}
190
- id="formSearchHealthWorkers"
191
- disabled={isSearchDisabled()}
192
- onChange={(value) => setSearchHWR({ ...searchHWR, identifier: value.target.value })}
271
+ value={searchHWR.identifier}
272
+ onChange={(e) => setSearchHWR((prev) => ({ ...prev, identifier: e.target.value }))}
273
+ className={styles.formSearch}
193
274
  />
194
275
  </Column>
195
276
  </div>
196
277
  </div>
278
+
197
279
  <div className="cds--modal-footer">
198
- <Button kind="secondary" onClick={close}>
280
+ <Button kind="secondary" onClick={close} disabled={syncLoading}>
199
281
  {t('cancel', 'Cancel')}
200
282
  </Button>
201
- <Button disabled={isSearchDisabled() || syncLoading} onClick={handleSync}>
202
- {syncLoading ? <InlineLoading status="active" description={t('syncing', 'Syncing...')} /> : t('sync', 'Sync')}
283
+ <Button onClick={handleSync} disabled={isSearchDisabled()}>
284
+ {syncLoading ? (
285
+ <span className={styles.syncingLabel}>
286
+ <InlineLoading status="active" description={t('syncing', 'Syncing...')} />
287
+ </span>
288
+ ) : (
289
+ t('sync', 'Sync')
290
+ )}
203
291
  </Button>
204
292
  </div>
205
293
  </>
@@ -1,75 +1,203 @@
1
+ @use '@carbon/colors';
1
2
  @use '@carbon/layout';
2
3
  @use '@carbon/type';
3
- @use '@carbon/colors';
4
4
  @use '@openmrs/esm-styleguide/src/vars' as *;
5
5
 
6
- .patientName {
7
- @include type.type-style('label-02');
8
- color: colors.$gray-100;
9
- font-weight: 800;
10
- margin-left: layout.$spacing-04;
11
- margin-right: layout.$spacing-01;
12
- }
13
- .middot {
14
- margin: 0 layout.$spacing-01;
15
- }
16
- .spanField {
17
- margin-left: layout.$spacing-04;
18
- margin-right: layout.$spacing-01;
6
+ .providerDetailsContainer {
7
+ width: 100%;
8
+ background-color: colors.$gray-10;
9
+ padding: layout.$spacing-04;
19
10
  }
20
11
 
21
- .gender {
22
- display: flex;
23
- align-items: center;
24
- color: colors.$gray-70;
25
- gap: layout.$spacing-01;
26
- margin-left: layout.$spacing-02;
27
- }
28
- .statusTag {
12
+ .patientBanner {
29
13
  display: flex;
30
- align-items: center;
31
- color: colors.$gray-70;
32
- gap: layout.$spacing-01;
33
- margin-left: layout.$spacing-02;
14
+ gap: layout.$spacing-05;
15
+ padding: layout.$spacing-05 layout.$spacing-06;
16
+ background-color: colors.$white;
17
+ border-radius: 6px;
18
+ border: 1px solid colors.$gray-20;
34
19
  }
35
- .demographics {
20
+
21
+ .patientAvatar {
22
+ flex-shrink: 0;
23
+ width: 4rem;
24
+ height: 4rem;
36
25
  display: flex;
37
26
  align-items: center;
38
- margin-bottom: layout.$spacing-02;
39
- color: $text-02;
40
- text-transform: uppercase;
27
+ justify-content: center;
28
+ background-color: colors.$gray-10;
29
+ overflow: hidden;
30
+
31
+ :global(img) {
32
+ width: 100%;
33
+ height: 100%;
34
+ object-fit: cover;
35
+ }
41
36
  }
42
37
 
43
- .patientBanner {
38
+ .patientInfo {
44
39
  display: flex;
40
+ flex-direction: column;
41
+ flex: 1;
42
+ min-width: 0;
43
+ gap: layout.$spacing-04;
45
44
  }
46
45
 
47
- .patientInfo {
46
+ .row {
48
47
  display: flex;
49
48
  flex-direction: column;
50
- width: 100%;
49
+ }
50
+
51
+ .patientNameRow {
52
+ align-items: flex-start;
51
53
  }
52
54
 
53
55
  .flexRow {
54
56
  display: flex;
55
57
  flex-flow: row wrap;
56
58
  align-items: center;
57
- margin-top: layout.$spacing-04;
59
+ gap: layout.$spacing-03;
60
+ row-gap: layout.$spacing-02;
58
61
  }
59
- .patientAvatar {
60
- margin-top: layout.$spacing-06;
62
+
63
+ .tagRow {
64
+ display: inline-flex;
65
+ align-items: center;
66
+ flex-wrap: wrap;
67
+ gap: layout.$spacing-02;
68
+ }
69
+
70
+ .patientName {
71
+ @include type.type-style('heading-03');
72
+ color: colors.$gray-100;
73
+ font-weight: 600;
74
+ margin: 0;
75
+ }
76
+
77
+ .gender {
78
+ @include type.type-style('body-01');
79
+ color: colors.$gray-70;
80
+ }
81
+
82
+ .statusTag {
83
+ display: inline-flex;
84
+ align-items: center;
85
+ }
86
+
87
+ .puidBadge {
88
+ @include type.type-style('label-02');
89
+ display: inline-block;
90
+ background-color: colors.$gray-10;
91
+ color: colors.$gray-90;
92
+ padding: layout.$spacing-02 layout.$spacing-03;
93
+ border-radius: 4px;
94
+ border: 1px solid colors.$gray-20;
95
+ align-self: flex-start;
96
+
97
+ strong {
98
+ color: colors.$gray-100;
99
+ font-weight: 600;
100
+ }
101
+ }
102
+
103
+ .spanField {
104
+ @include type.type-style('body-01');
105
+ display: inline-flex;
106
+ align-items: center;
107
+ gap: layout.$spacing-01;
108
+ color: colors.$gray-90;
109
+ }
110
+
111
+ .fieldLabel {
112
+ color: colors.$gray-70;
113
+ font-weight: 400;
114
+ }
115
+
116
+ .fieldValue {
117
+ color: colors.$gray-100;
118
+ font-weight: 500;
119
+ }
120
+
121
+ .middot {
122
+ color: colors.$gray-50;
123
+ user-select: none;
61
124
  }
125
+
126
+ .demographics {
127
+ display: flex;
128
+ align-items: center;
129
+ margin-bottom: layout.$spacing-02;
130
+ color: $text-02;
131
+ text-transform: uppercase;
132
+ }
133
+
62
134
  .viewRoles {
63
- margin-top: layout.$spacing-06;
135
+ margin-top: layout.$spacing-04;
136
+ padding-top: layout.$spacing-04;
137
+ border-top: 1px solid colors.$gray-20;
138
+
139
+ :global(.cds--accordion) {
140
+ border-top: none;
141
+ }
142
+
143
+ :global(.cds--accordion__heading) {
144
+ @include type.type-style('label-02');
145
+ padding: layout.$spacing-03 0;
146
+ }
147
+
148
+ :global(.cds--accordion__title) {
149
+ color: colors.$gray-100;
150
+ font-weight: 600;
151
+ }
64
152
  }
153
+
65
154
  .roleContainer {
66
155
  display: flex;
67
156
  flex-direction: column;
68
- gap: 0.5rem;
157
+ gap: layout.$spacing-02;
158
+ padding: layout.$spacing-03 0;
159
+ }
160
+
161
+ .roleName {
162
+ color: colors.$gray-100;
163
+ font-weight: 600;
69
164
  }
70
165
 
71
166
  .roleDescription {
167
+ @include type.type-style('body-01');
72
168
  margin: 0;
73
169
  color: colors.$gray-70;
74
- font-size: type.type-scale(1);
170
+ font-size: 0.875rem;
171
+ line-height: 1.4;
172
+ }
173
+
174
+ @media (max-width: 768px) {
175
+ .patientBanner {
176
+ flex-direction: column;
177
+ gap: layout.$spacing-04;
178
+ padding: layout.$spacing-04;
179
+ }
180
+
181
+ .patientAvatar {
182
+ width: 3rem;
183
+ height: 3rem;
184
+ }
185
+
186
+ .patientName {
187
+ @include type.type-style('heading-02');
188
+ }
189
+
190
+ .flexRow {
191
+ flex-direction: column;
192
+ align-items: flex-start;
193
+ gap: layout.$spacing-02;
194
+ }
195
+
196
+ .middot {
197
+ display: none;
198
+ }
199
+
200
+ .spanField {
201
+ width: 100%;
202
+ }
75
203
  }