@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kenyaemr/esm-admin-app",
3
- "version": "5.4.4-pre.142",
3
+ "version": "5.4.4-pre.15",
4
4
  "description": "Facilitates the management of ETL tables",
5
5
  "keywords": [
6
6
  "openmrs"
@@ -1,24 +1,13 @@
1
1
  import React from 'react';
2
2
  import { render, screen, fireEvent } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
- import { useTranslation } from 'react-i18next';
4
+ import { vi, describe, it, expect } from 'vitest';
5
5
  import OperationConfirmation from './confirmation-operation-modal.component';
6
6
 
7
- jest.mock('react-i18next', () => ({
8
- useTranslation: jest.fn(),
9
- }));
10
-
11
7
  describe('OperationConfirmation', () => {
12
- beforeEach(() => {
13
- (useTranslation as jest.Mock).mockReturnValue({
14
- t: (key, defaultValue, options) =>
15
- options?.operationTypeOrName ? `Do you want to ${options.operationTypeOrName}?` : defaultValue || key,
16
- });
17
- });
18
-
19
8
  it('renders the component with provided props', () => {
20
- const closeMock = jest.fn();
21
- const confirmMock = jest.fn();
9
+ const closeMock = vi.fn();
10
+ const confirmMock = vi.fn();
22
11
  const operationName = 'refresh';
23
12
  const operationType = 'refreshed';
24
13
 
@@ -32,14 +21,14 @@ describe('OperationConfirmation', () => {
32
21
  );
33
22
 
34
23
  expect(screen.getByRole('heading', { name: 'Confirmation' })).toBeInTheDocument();
35
- expect(screen.getByText('Do you want to refreshed?')).toBeInTheDocument();
24
+ expect(screen.getByText('Do you want to {{operationTypeOrName}}?')).toBeInTheDocument();
36
25
  expect(screen.getByText('No')).toBeInTheDocument();
37
26
  expect(screen.getByText('Yes')).toBeInTheDocument();
38
27
  });
39
28
 
40
29
  it('calls close when the No button is clicked', () => {
41
- const closeMock = jest.fn();
42
- const confirmMock = jest.fn();
30
+ const closeMock = vi.fn();
31
+ const confirmMock = vi.fn();
43
32
 
44
33
  render(
45
34
  <OperationConfirmation close={closeMock} confirm={confirmMock} operationName="delete" operationType="deleted" />,
@@ -53,8 +42,8 @@ describe('OperationConfirmation', () => {
53
42
  });
54
43
 
55
44
  it('calls confirm when the Yes button is clicked', () => {
56
- const closeMock = jest.fn();
57
- const confirmMock = jest.fn();
45
+ const closeMock = vi.fn();
46
+ const confirmMock = vi.fn();
58
47
 
59
48
  render(
60
49
  <OperationConfirmation close={closeMock} confirm={confirmMock} operationName="delete" operationType="deleted" />,
@@ -12,6 +12,7 @@ import {
12
12
  } from '../logs-table/operation-log-resource';
13
13
  import LogTable from '../logs-table/operation-log-table.component';
14
14
  import { showModal, showSnackbar } from '@openmrs/esm-framework';
15
+ import { extractErrorMessagesFromResponse } from '../locations/helpers';
15
16
 
16
17
  const Dashboard: React.FC = () => {
17
18
  const { t } = useTranslation();
@@ -59,7 +60,9 @@ const Dashboard: React.FC = () => {
59
60
  } catch (error) {
60
61
  showSnackbar({
61
62
  title: t('operationError', '{{operationName}} failed', { operationName }),
62
- subtitle: t('operationErrorSubtitle', 'An error occurred during the operation.'),
63
+ subtitle: t('operationErrorSubtitle', 'An error occurred during the operation. {{error}}', {
64
+ error: extractErrorMessagesFromResponse(error),
65
+ }),
63
66
  kind: 'error',
64
67
  isLowContrast: true,
65
68
  });
@@ -1,12 +1,11 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
  import '@testing-library/jest-dom';
3
- import { EmptyDataIllustration } from '@openmrs/esm-patient-common-lib';
3
+ import { vi, describe, it, expect } from 'vitest';
4
4
  import EmptyState from './empty-state-log.components';
5
5
  import React from 'react';
6
6
 
7
- // Mock the EmptyDataIllustration component
8
- jest.mock('@openmrs/esm-patient-common-lib', () => ({
9
- EmptyDataIllustration: jest.fn(() => <div>Mocked EmptyDataIllustration</div>),
7
+ vi.mock('@openmrs/esm-patient-common-lib', () => ({
8
+ EmptyDataIllustration: vi.fn(() => <div>Mocked EmptyDataIllustration</div>),
10
9
  }));
11
10
 
12
11
  describe('EmptyState', () => {
@@ -0,0 +1,3 @@
1
+ import { restBaseUrl } from '@openmrs/esm-framework';
2
+
3
+ export const facilityRegistryUrl = `${restBaseUrl}/virtualclaims/facility-registry`;
@@ -1,91 +1,123 @@
1
- import { Button, Column, Grid, InlineLoading, Layer, Tag, Tile } from '@carbon/react';
1
+ import { Button, Column, Grid, InlineLoading, Tag, Tile } from '@carbon/react';
2
+ import { Renew, Phone, Email } from '@carbon/react/icons';
2
3
  import { formatDate, parseDate, showSnackbar } from '@openmrs/esm-framework';
3
- import React, { useCallback, useMemo, useState } from 'react';
4
+ import React, { useCallback } from 'react';
4
5
  import { useTranslation } from 'react-i18next';
5
- import { useLocalFacilityInfo, useShaFacilityInfo } from '../hook/useFacilityInfo';
6
6
  import styles from './facility-info.scss';
7
- import { syncPackagesAndInterventions } from './facility-setup.resource';
8
- import { Renew, Phone, Email } from '@carbon/react/icons';
9
- import { SectionCard } from './shared/custom-section-card.component';
10
7
  import { InfoRow } from './shared/custom-info.component';
8
+ import { SectionCard } from './shared/custom-section-card.component';
11
9
  import { StatusTag } from './shared/custom-status-tag.component';
10
+ import { syncFacilityRegistry, useFacilityRegistry } from './useFacilityRegistry';
11
+ import { FacilityRegistryRecord } from './type';
12
+ import { CardHeader } from '@openmrs/esm-patient-common-lib/src';
13
+ import EmptyState from '../empty-state/empty-state-log.components';
12
14
 
13
- const FacilityInfo: React.FC = () => {
15
+ const FacilityRegistryView: React.FC = () => {
14
16
  const { t } = useTranslation();
15
- const [shouldSynchronize, setShouldSynchronize] = useState(false);
16
-
17
- const {
18
- shaFacility,
19
- isLoading: isShaLoading,
20
- error: shaError,
21
- mutate: mutateSha,
22
- } = useShaFacilityInfo(shouldSynchronize);
23
- const { localFacility, isLoading: isLocalLoading, mutate: mutateLocal } = useLocalFacilityInfo();
24
-
25
- const mutateFacility = useCallback(async () => {
26
- const [defaultFacility, sha] = await Promise.all([mutateLocal(), mutateSha()]);
27
- return { shaFacility: sha, defaultFacility };
28
- }, [mutateLocal, mutateSha]);
17
+ const { facility, isLoading, error, notYetSynced, mutate } = useFacilityRegistry();
18
+ const [isSyncing, setIsSyncing] = React.useState(false);
29
19
 
30
- const isLoading = isShaLoading || isLocalLoading;
31
-
32
- const synchronizeFacilityData = useCallback(async () => {
20
+ const handleSync = useCallback(async () => {
21
+ setIsSyncing(true);
33
22
  try {
34
- setShouldSynchronize(true);
35
- const { shaFacility: synced } = await mutateFacility();
36
- showSnackbar({ title: t('syncingHieSuccess', 'Synchronization complete'), kind: 'success', isLowContrast: true });
37
- if (synced?.data?.source !== 'HIE') {
38
- showSnackbar({
39
- kind: 'warning',
40
- title: t('hieSyncFailed', 'HIE sync failed. Pulling local info.'),
41
- isLowContrast: true,
42
- });
43
- }
44
- await syncPackagesAndInterventions();
45
- } catch (error) {
23
+ const result = await syncFacilityRegistry();
24
+ await mutate();
46
25
  showSnackbar({
47
- title: t('syncingHieError', 'Syncing with HIE failed'),
26
+ title: t('facilityRegistrySyncSuccess', 'Facility synced'),
27
+ subtitle: t('facilityRegistrySyncedFrom', 'Pulled {{name}} from the facility registry', {
28
+ name: result.official_name,
29
+ }),
30
+ kind: 'success',
31
+ isLowContrast: true,
32
+ });
33
+ } catch (err: any) {
34
+ showSnackbar({
35
+ title: t('facilityRegistrySyncFailed', 'Sync failed'),
48
36
  subtitle:
49
- error?.responseBody?.error?.message ??
50
- t('hieSynchronizationError', 'An error occurred while synchronizing with HIE'),
37
+ err?.responseBody?.error ??
38
+ err?.message ??
39
+ t('facilityRegistrySyncError', 'Unable to reach the facility registry'),
51
40
  kind: 'error',
52
41
  isLowContrast: true,
53
42
  });
43
+ } finally {
44
+ setIsSyncing(false);
54
45
  }
55
- }, [mutateFacility, t]);
46
+ }, [mutate, t]);
47
+
48
+ if (isLoading) {
49
+ return (
50
+ <div className={styles.root}>
51
+ <InlineLoading description={t('loadingFacility', 'Loading facility…')} />
52
+ </div>
53
+ );
54
+ }
55
+
56
+ if (error) {
57
+ return (
58
+ <div className={styles.root}>
59
+ <Tile className={styles.errorTile}>
60
+ <p>{t('facilityRegistryLoadError', 'Failed to load the facility record.')}</p>
61
+ <Button kind="secondary" size="sm" renderIcon={Renew} onClick={handleSync} disabled={isSyncing}>
62
+ {isSyncing ? t('syncing', 'Syncing…') : t('retry', 'Retry sync')}
63
+ </Button>
64
+ </Tile>
65
+ </div>
66
+ );
67
+ }
68
+
69
+ if (notYetSynced || !facility) {
70
+ return (
71
+ <div>
72
+ <CardHeader title={t('facilityDetails', 'Facility details')}>
73
+ <Button kind="primary" size="sm" renderIcon={Renew} onClick={handleSync} disabled={isSyncing}>
74
+ {isSyncing ? t('syncing', 'Syncing…') : t('syncNow', 'Sync now')}
75
+ </Button>
76
+ </CardHeader>
77
+ <EmptyState
78
+ subTitle={t(
79
+ 'facilityRegistryNotSyncedBody',
80
+ 'This facility has not been pulled from the registry. Click sync to fetch the latest record.',
81
+ )}
82
+ />
83
+ </div>
84
+ );
85
+ }
56
86
 
57
87
  return (
58
88
  <div className={styles.root}>
59
89
  <div className={styles.pageHeader}>
60
90
  <div className={styles.pageActions}>
61
- {isLoading ? (
62
- <InlineLoading description={t('synchronizing', 'Synchronizing...')} />
91
+ {facility.last_synced_at && (
92
+ <span className={styles.lastSynced}>
93
+ {t('lastSynced', 'Last synced')}: {formatDate(parseDate(facility.last_synced_at))}
94
+ </span>
95
+ )}
96
+ {isSyncing ? (
97
+ <InlineLoading description={t('syncing', 'Syncing…')} />
63
98
  ) : (
64
- <Button kind="secondary" size="sm" renderIcon={Renew} onClick={synchronizeFacilityData}>
65
- {t('syncWithHie', 'Sync with HIE')}
99
+ <Button kind="secondary" size="sm" renderIcon={Renew} onClick={handleSync}>
100
+ {t('syncWithRegistry', 'Sync with facility registry')}
66
101
  </Button>
67
102
  )}
68
103
  </div>
69
104
  </div>
105
+
70
106
  <Grid narrow className={styles.grid}>
71
107
  <Column sm={4} md={4} lg={8}>
72
108
  <SectionCard title={t('generalInformation', 'General information')}>
73
- <InfoRow label={t('facilityName', 'Facility name')} value={localFacility?.display} />
74
- <InfoRow label={t('registrationNumber', 'Registration no.')} value={shaFacility?.registrationNumber} />
75
- <InfoRow label={t('fidCode', 'FID code')} value={shaFacility?.fidCode} />
76
- <InfoRow label={t('facilityRegistryCode', 'Registry code')} value={shaFacility?.facilityRegistryCode} />
77
- <InfoRow
78
- label={t('mflCode', 'MFL code')}
79
- value={shaFacility?.mflCode !== '--' ? shaFacility?.mflCode : localFacility?.locationId}
80
- />
81
- <InfoRow label={t('kephLevel', 'KEPH level')} value={shaFacility?.kephLevel} />
82
- <InfoRow label={t('facilityType', 'Facility type')} value={shaFacility?.facilityType} />
83
- <InfoRow label={t('facilityOwnership', 'Ownership')} value={shaFacility?.facilityOwnership} />
109
+ <InfoRow label={t('facilityName', 'Facility name')} value={facility.official_name} />
110
+ <InfoRow label={t('registrationNumber', 'Registration no.')} value={facility.registration_number} />
111
+ <InfoRow label={t('frCode', 'FR code')} value={facility.fr_code} />
112
+ <InfoRow label={t('fidCode', 'FID code')} value={facility.fid_code} />
113
+ <InfoRow label={t('kephLevel', 'KEPH level')} value={facility.keph_level} />
114
+ <InfoRow label={t('facilityType', 'Facility type')} value={facility.facility_type} />
115
+ <InfoRow label={t('facilityOwnership', 'Ownership')} value={facility.facility_ownership} />
84
116
  <InfoRow
85
117
  label={t('hubFacility', 'Hub facility')}
86
118
  value={
87
- <Tag type={shaFacility?.isHub === 'true' ? 'blue' : 'gray'} size="sm">
88
- {shaFacility?.isHub === 'true' ? 'Yes' : 'No'}
119
+ <Tag type={facility.is_hub ? 'blue' : 'gray'} size="sm">
120
+ {facility.is_hub ? t('yes', 'Yes') : t('no', 'No')}
89
121
  </Tag>
90
122
  }
91
123
  />
@@ -94,31 +126,33 @@ const FacilityInfo: React.FC = () => {
94
126
 
95
127
  <Column sm={4} md={4} lg={8}>
96
128
  <SectionCard title={t('licenseAndShaStatus', 'License & SHA status')}>
97
- <InfoRow label={t('licenseNumber', 'License no.')} value={shaFacility?.shaFacilityLicenseNumber} />
98
- <InfoRow label={t('regulatoryBody', 'Regulatory body')} value={shaFacility?.regulatoryBody} />
129
+ <InfoRow label={t('licenseNumber', 'License no.')} value={facility.license_number} />
130
+ <InfoRow label={t('regulatoryBody', 'Regulatory body')} value={facility.regulatory_body} />
99
131
  <InfoRow
100
132
  label={t('licenseStatus', 'License status')}
101
- value={<StatusTag value={shaFacility?.facilityLicenseStatus} />}
133
+ value={<StatusTag value={facility.license_status} />}
102
134
  />
103
135
  <InfoRow
104
136
  label={t('licenseStart', 'License start')}
105
- value={formatDate(parseDate(shaFacility?.facilityLicenseStartDate)) ?? '—'}
137
+ value={facility.license_start_date ? formatDate(parseDate(facility.license_start_date)) : '—'}
106
138
  />
107
139
  <InfoRow
108
140
  label={t('licenseExpiry', 'License expiry')}
109
- value={formatDate(parseDate(shaFacility?.shaFacilityExpiryDate)) ?? '—'}
141
+ value={facility.license_end_date ? formatDate(parseDate(facility.license_end_date)) : '—'}
110
142
  />
111
143
  <InfoRow
112
- label={t('operationalStatus', 'Operational status')}
113
- value={<StatusTag value={shaFacility?.operationalStatus} />}
144
+ label={t('regulatoryOperationalStatus', 'Regulatory status')}
145
+ value={<StatusTag value={facility.regulatory_operational_status} />}
146
+ />
147
+ <InfoRow
148
+ label={t('shaOperationalStatus', 'SHA status')}
149
+ value={<StatusTag value={facility.sha_operational_status} />}
114
150
  />
115
151
  <InfoRow
116
152
  label={t('shaContractStatus', 'SHA contract')}
117
153
  value={
118
- shaFacility?.shaContractStatus &&
119
- shaFacility.shaContractStatus !== '--' &&
120
- shaFacility.shaContractStatus !== '' ? (
121
- <StatusTag value={shaFacility.shaContractStatus} />
154
+ facility.sha_contract_status && facility.sha_contract_status !== '' ? (
155
+ <StatusTag value={facility.sha_contract_status} />
122
156
  ) : (
123
157
  <Tag type="gray" size="sm">
124
158
  {t('notContracted', 'Not contracted')}
@@ -126,17 +160,26 @@ const FacilityInfo: React.FC = () => {
126
160
  )
127
161
  }
128
162
  />
129
- <InfoRow label={t('totalBeds', 'Total beds')} value={shaFacility?.totalBeds} />
163
+ {facility.bed_occupancy && (
164
+ <InfoRow label={t('totalBeds', 'Total beds')} value={facility.bed_occupancy.totalBeds?.toString()} />
165
+ )}
130
166
  </SectionCard>
131
167
  </Column>
132
168
 
133
169
  <Column sm={4} md={4} lg={8}>
134
170
  <SectionCard title={t('location', 'Location')}>
135
- <InfoRow label={t('county', 'County')} value={shaFacility?.county} />
136
- <InfoRow label={t('subCounty', 'Sub-county')} value={shaFacility?.subCounty} />
137
- <InfoRow label={t('town', 'Town')} value={shaFacility?.town} />
138
- <InfoRow label={t('physicalLocation', 'Physical location')} value={shaFacility?.physicalLocation} />
139
- <InfoRow label={t('postalAddress', 'Postal address')} value={shaFacility?.postalAddress} />
171
+ <InfoRow label={t('country', 'Country')} value={facility.address?.country} />
172
+ <InfoRow label={t('county', 'County')} value={facility.address?.county} />
173
+ <InfoRow label={t('subCounty', 'Sub-county')} value={facility.address?.sub_county} />
174
+ <InfoRow label={t('town', 'Town')} value={facility.address?.town} />
175
+ <InfoRow label={t('physicalLocation', 'Physical location')} value={facility.address?.physical_location} />
176
+ <InfoRow label={t('postalAddress', 'Postal address')} value={facility.address?.postal_address} />
177
+ {facility.address?.latitude != null && facility.address?.longitude != null && (
178
+ <InfoRow
179
+ label={t('coordinates', 'Coordinates')}
180
+ value={`${facility.address.latitude}, ${facility.address.longitude}`}
181
+ />
182
+ )}
140
183
  </SectionCard>
141
184
  </Column>
142
185
 
@@ -145,10 +188,10 @@ const FacilityInfo: React.FC = () => {
145
188
  <InfoRow
146
189
  label={t('facilityPhone', 'Phone')}
147
190
  value={
148
- shaFacility?.facilityPhoneNumber && shaFacility.facilityPhoneNumber !== '--' ? (
191
+ facility.facility_phone_number ? (
149
192
  <span className={styles.contactValue}>
150
193
  <Phone size={14} />
151
- {shaFacility.facilityPhoneNumber}
194
+ {facility.facility_phone_number}
152
195
  </span>
153
196
  ) : undefined
154
197
  }
@@ -156,51 +199,39 @@ const FacilityInfo: React.FC = () => {
156
199
  <InfoRow
157
200
  label={t('facilityEmail', 'Email')}
158
201
  value={
159
- shaFacility?.facilityEmail && shaFacility.facilityEmail !== '--' ? (
202
+ facility.facility_email ? (
160
203
  <span className={styles.contactValue}>
161
204
  <Email size={14} />
162
- {shaFacility.facilityEmail}
205
+ {facility.facility_email}
163
206
  </span>
164
207
  ) : undefined
165
208
  }
166
209
  />
167
- {shaFacility?.facilityAdministratorName && shaFacility.facilityAdministratorName !== '--' && (
210
+ {facility.facility_administrator_name && (
168
211
  <div className={styles.adminCard}>
169
212
  <div>
170
- <p className={styles.adminName}>{shaFacility.facilityAdministratorName}</p>
171
- <p className={styles.adminDetail}>{shaFacility.facilityAdministratorPhone}</p>
172
- <p className={styles.adminDetail}>{shaFacility.facilityAdministratorEmail}</p>
213
+ <p className={styles.adminName}>{facility.facility_administrator_name}</p>
214
+ {facility.facility_administrator_phone && (
215
+ <p className={styles.adminDetail}>{facility.facility_administrator_phone}</p>
216
+ )}
217
+ {facility.facility_administrator_email && (
218
+ <p className={styles.adminDetail}>{facility.facility_administrator_email}</p>
219
+ )}
173
220
  </div>
174
221
  </div>
175
222
  )}
176
223
  </SectionCard>
177
224
  </Column>
178
225
 
226
+ <Column sm={4} md={8} lg={16}>
227
+ <SectionCard title={t('bedOccupancy', 'Bed occupancy')}>
228
+ <BedOccupancyView facility={facility} />
229
+ </SectionCard>
230
+ </Column>
231
+
179
232
  <Column sm={4} md={8} lg={16}>
180
233
  <SectionCard title={t('shaContractedServices', 'SHA contracted services')}>
181
- {(() => {
182
- try {
183
- const services = JSON.parse(shaFacility?.shaContractedServices ?? '[]');
184
- if (services.length === 0) {
185
- return (
186
- <p className={styles.emptyState}>
187
- {t('noContractedServices', 'No contracted services on record for this facility.')}
188
- </p>
189
- );
190
- }
191
- return (
192
- <div className={styles.serviceList}>
193
- {services.map((svc: any, idx: number) => (
194
- <Tag key={idx} type="teal" size="sm">
195
- {svc.name ?? svc}
196
- </Tag>
197
- ))}
198
- </div>
199
- );
200
- } catch {
201
- return <p className={styles.emptyState}>—</p>;
202
- }
203
- })()}
234
+ <ServicesView facility={facility} />
204
235
  </SectionCard>
205
236
  </Column>
206
237
  </Grid>
@@ -208,4 +239,43 @@ const FacilityInfo: React.FC = () => {
208
239
  );
209
240
  };
210
241
 
211
- export default FacilityInfo;
242
+ const BedOccupancyView: React.FC<{ facility: FacilityRegistryRecord }> = ({ facility }) => {
243
+ const { t } = useTranslation();
244
+ const bo = facility.bed_occupancy;
245
+ if (!bo) {
246
+ return <p className={styles.emptyState}>{t('noBedOccupancyData', 'No bed occupancy data')}</p>;
247
+ }
248
+ return (
249
+ <div className={styles.bedGrid}>
250
+ <InfoRow label={t('totalBeds', 'Total')} value={bo.totalBeds?.toString()} />
251
+ <InfoRow label={t('normalBeds', 'Normal')} value={bo.normalBeds?.toString()} />
252
+ <InfoRow label={t('icuBeds', 'ICU')} value={bo.icuBeds?.toString()} />
253
+ <InfoRow label={t('hduBeds', 'HDU')} value={bo.hduBeds?.toString()} />
254
+ <InfoRow label={t('dialysisBeds', 'Dialysis')} value={bo.dialysisBeds?.toString()} />
255
+ <InfoRow label={t('cots', 'Cots')} value={bo.numberOfCots?.toString()} />
256
+ </div>
257
+ );
258
+ };
259
+
260
+ const ServicesView: React.FC<{ facility: FacilityRegistryRecord }> = ({ facility }) => {
261
+ const { t } = useTranslation();
262
+ const services = facility.sha_contracted_services ?? [];
263
+ if (services.length === 0) {
264
+ return (
265
+ <p className={styles.emptyState}>
266
+ {t('noContractedServices', 'No contracted services on record for this facility.')}
267
+ </p>
268
+ );
269
+ }
270
+ return (
271
+ <div className={styles.serviceList}>
272
+ {services.map((svc, idx) => (
273
+ <Tag key={idx} type="teal" size="sm">
274
+ {typeof svc === 'string' ? svc : svc.name ?? JSON.stringify(svc)}
275
+ </Tag>
276
+ ))}
277
+ </div>
278
+ );
279
+ };
280
+
281
+ export default FacilityRegistryView;
@@ -11,6 +11,7 @@
11
11
  align-items: center;
12
12
  justify-content: space-between;
13
13
  margin-bottom: layout.$spacing-05;
14
+ margin-right: layout.$spacing-08;
14
15
  }
15
16
 
16
17
  .pageTitle {
@@ -24,7 +25,7 @@
24
25
  align-items: center;
25
26
  gap: layout.$spacing-03;
26
27
  margin-left: auto;
27
- margin-right: layout.$spacing-09;
28
+ margin-right: layout.$spacing-12;
28
29
  }
29
30
  .grid {
30
31
  margin-top: 0;
@@ -128,3 +129,40 @@
128
129
  font-style: italic;
129
130
  margin: 0;
130
131
  }
132
+
133
+ .pageMeta {
134
+ display: flex;
135
+ align-items: center;
136
+ gap: 0.5rem;
137
+ }
138
+
139
+ .lastSynced {
140
+ font-size: 0.875rem;
141
+ color: var(--cds-text-secondary, #525252);
142
+ display: flex;
143
+ align-items: end;
144
+ }
145
+
146
+ .bedGrid {
147
+ display: grid;
148
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
149
+ gap: layout.$spacing-04;
150
+
151
+ .infoRow {
152
+ display: flex;
153
+ justify-content: flex-start;
154
+ gap: layout.$spacing-03;
155
+ padding: layout.$spacing-02 layout.$spacing-03;
156
+ background: colors.$gray-10;
157
+ border-radius: 4px;
158
+ border-bottom: none;
159
+ }
160
+
161
+ .infoLabel {
162
+ margin-right: 0;
163
+ }
164
+
165
+ .infoValue {
166
+ text-align: left;
167
+ }
168
+ }
@@ -1,9 +1,9 @@
1
1
  import { Layer } from '@carbon/react';
2
2
  import React from 'react';
3
3
  import { useTranslation } from 'react-i18next';
4
- import FacilityInfo from './facility-info.component';
5
4
  import styles from './facility-setup.scss';
6
5
  import Header from './header/header.component';
6
+ import FacilityRegistryView from './facility-info.component';
7
7
 
8
8
  const FacilitySetup: React.FC = () => {
9
9
  const { t } = useTranslation();
@@ -12,7 +12,7 @@ const FacilitySetup: React.FC = () => {
12
12
  <Header title={t('facilityDetails', 'Facility Details')} />
13
13
 
14
14
  <Layer className={styles.tableLayer}>
15
- <FacilityInfo />
15
+ <FacilityRegistryView />
16
16
  </Layer>
17
17
  </div>
18
18
  );
@@ -0,0 +1,61 @@
1
+ export type FacilityRegistryAddress = {
2
+ country?: string;
3
+ county_code?: string;
4
+ county?: string;
5
+ sub_county_code?: string;
6
+ sub_county?: string;
7
+ postal_address?: string;
8
+ physical_location?: string;
9
+ town?: string;
10
+ latitude?: number | null;
11
+ longitude?: number | null;
12
+ };
13
+
14
+ export type FacilityRegistryBedOccupancy = {
15
+ totalBeds?: number;
16
+ normalBeds?: number;
17
+ icuBeds?: number;
18
+ hduBeds?: number;
19
+ dialysisBeds?: number;
20
+ numberOfCots?: number;
21
+ };
22
+
23
+ export type FacilityRegistryRecord = {
24
+ fr_code: string;
25
+ fid_code?: string;
26
+ registration_number?: string;
27
+ official_name?: string;
28
+ keph_level?: string;
29
+ facility_type?: string;
30
+ facility_ownership?: string;
31
+ license_status?: string;
32
+ license_number?: string;
33
+ license_start_date?: string | null;
34
+ license_end_date?: string | null;
35
+ regulatory_body?: string;
36
+ sha_contract_status?: string;
37
+ sha_contract_start_date?: string | null;
38
+ sha_contract_end_date?: string | null;
39
+ is_hub?: boolean;
40
+ facility_phone_number?: string;
41
+ facility_email?: string;
42
+ facility_administrator_name?: string;
43
+ facility_administrator_email?: string;
44
+ facility_administrator_phone?: string;
45
+ facility_administrator_identifier?: string;
46
+ address?: FacilityRegistryAddress;
47
+ regulatory_operational_status?: string;
48
+ sha_operational_status?: string;
49
+ bed_occupancy?: FacilityRegistryBedOccupancy;
50
+ sha_contracted_services?: Array<{ name?: string } | string>;
51
+ last_synced_at?: string;
52
+ last_sync_status?: 'SUCCESS' | 'FAILURE';
53
+ };
54
+
55
+ export type FacilityRegistrySyncResult = {
56
+ success: boolean;
57
+ fr_code: string;
58
+ official_name: string;
59
+ last_synced_at: string;
60
+ last_sync_status: 'SUCCESS' | 'FAILURE';
61
+ };
@@ -0,0 +1,29 @@
1
+ import useSWR from 'swr';
2
+ import { openmrsFetch, restBaseUrl, type FetchResponse } from '@openmrs/esm-framework';
3
+ import { FacilityRegistryRecord, FacilityRegistrySyncResult } from './type';
4
+ import { facilityRegistryUrl } from './constant';
5
+
6
+ export const useFacilityRegistry = () => {
7
+ const { data, error, isLoading, mutate } = useSWR<FetchResponse<FacilityRegistryRecord>>(
8
+ facilityRegistryUrl,
9
+ openmrsFetch,
10
+ {
11
+ revalidateOnFocus: false,
12
+ },
13
+ );
14
+
15
+ const notYetSynced = error?.response?.status === 404;
16
+
17
+ return {
18
+ facility: data?.data,
19
+ isLoading,
20
+ error: notYetSynced ? null : error,
21
+ notYetSynced,
22
+ mutate,
23
+ };
24
+ };
25
+
26
+ export const syncFacilityRegistry = async (): Promise<FacilityRegistrySyncResult> => {
27
+ const response = await openmrsFetch<FacilityRegistrySyncResult>(facilityRegistryUrl + '/sync', { method: 'POST' });
28
+ return response.data;
29
+ };