@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,47 +1,153 @@
1
1
  import React from 'react';
2
2
  import { render, screen } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
+ import { vi, describe, it, expect, beforeEach } from 'vitest';
4
5
  import LogTable from './operation-log-table.component';
5
6
  import { ETLResponse } from '../../types';
6
7
 
7
- describe('LogTable Component', () => {
8
- const mockLogData: ETLResponse[] = [
9
- {
10
- script_name: 'Initial Population of Tables',
11
- start_time: '2024-11-28T09:30:12',
12
- stop_time: '2024-11-28T09:33:15',
13
- status: 'Success',
14
- },
15
- {
16
- script_name: 'Initial Creation of Tables',
17
- start_time: '2024-11-28T09:30:12',
18
- stop_time: '2024-11-28T09:33:15',
19
- status: 'Failed',
8
+ vi.mock('react-i18next', () => ({
9
+ useTranslation: () => ({ t: (key: string, fallback: string) => fallback }),
10
+ }));
11
+
12
+ vi.mock('@openmrs/esm-framework', () => ({
13
+ useLayoutType: vi.fn(() => 'desktop'),
14
+ usePagination: vi.fn((data) => ({
15
+ results: data,
16
+ currentPage: 1,
17
+ goTo: vi.fn(),
18
+ })),
19
+ formatDate: vi.fn((date: Date) => date.toISOString()),
20
+ }));
21
+
22
+ vi.mock('@openmrs/esm-patient-common-lib', () => ({
23
+ CardHeader: ({ title }: { title: string }) => <div>{title}</div>,
24
+ PatientChartPagination: ({ currentItems, totalItems }: { currentItems: number; totalItems: number }) => (
25
+ <div data-testid="pagination">
26
+ {currentItems}/{totalItems}
27
+ </div>
28
+ ),
29
+ }));
30
+
31
+ vi.mock('../empty-state/empty-state-log.components', () => ({
32
+ default: ({ subTitle }: { subTitle: string }) => <div data-testid="empty-state">{subTitle}</div>,
33
+ }));
34
+
35
+ vi.mock('@carbon/react', async (importOriginal) => {
36
+ const original = await importOriginal<typeof import('@carbon/react')>();
37
+ return {
38
+ ...original,
39
+ DataTable: ({
40
+ children,
41
+ rows,
42
+ headers,
43
+ }: {
44
+ children: (props: any) => React.ReactNode;
45
+ rows: any[];
46
+ headers: any[];
47
+ }) => {
48
+ if (typeof children === 'function') {
49
+ const renderRows = rows.map((row) => ({
50
+ ...row,
51
+ cells: headers.map((h) => ({
52
+ id: `${row.id}:${h.key}`,
53
+ value: row[h.key],
54
+ info: { header: h.key },
55
+ })),
56
+ }));
57
+ return children({
58
+ rows: renderRows,
59
+ headers,
60
+ getTableProps: () => ({}),
61
+ getHeaderProps: ({ header }: any) => ({ key: header.key }),
62
+ getRowProps: () => ({}),
63
+ });
64
+ }
65
+ return children;
20
66
  },
21
- ];
67
+ };
68
+ });
22
69
 
23
- it('renders the table with correct headers and rows', () => {
24
- render(<LogTable logData={mockLogData} isLoading={false} />);
70
+ vi.mock('classnames', () => ({ default: (...args: string[]) => args.filter(Boolean).join(' ') }));
25
71
 
26
- const headers = ['Procedure', 'Start time', 'End time', 'Completion status'];
27
- headers.forEach((header) => {
28
- expect(screen.getByText(header)).toBeInTheDocument();
29
- });
72
+ vi.mock('./operation-log.scss', () => ({ default: {} }));
30
73
 
31
- const rows = screen.getAllByRole('row');
32
- expect(rows.length).toBe(3);
74
+ const mockLogData: ETLResponse[] = [
75
+ {
76
+ script_name: 'Script A',
77
+ start_time: '2024-01-01T08:00:00Z',
78
+ stop_time: '2024-01-01T09:00:00Z',
79
+ status: 'Success',
80
+ },
81
+ {
82
+ script_name: 'Script B',
83
+ start_time: '2024-01-02T10:00:00Z',
84
+ stop_time: '2024-01-02T11:00:00Z',
85
+ status: 'Failed',
86
+ },
87
+ ];
88
+
89
+ describe('LogTable', () => {
90
+ beforeEach(() => {
91
+ vi.clearAllMocks();
33
92
  });
34
93
 
35
- it('displays "No ETL Operation logs found" when logData is empty', () => {
36
- render(<LogTable logData={[]} isLoading={false} />);
94
+ it('renders the card header', () => {
95
+ render(<LogTable logData={mockLogData} isLoading={false} />);
96
+ expect(screen.getByText('Facility Info')).toBeInTheDocument();
97
+ });
98
+
99
+ it('shows skeleton while loading with no data', () => {
100
+ render(<LogTable logData={[]} isLoading={true} />);
101
+ expect(screen.getByRole('table')).toBeInTheDocument();
102
+ });
37
103
 
104
+ it('shows empty state when not loading and no data', () => {
105
+ render(<LogTable logData={[]} isLoading={false} />);
106
+ expect(screen.getByTestId('empty-state')).toBeInTheDocument();
38
107
  expect(screen.getByText('No ETL Operation logs found')).toBeInTheDocument();
39
108
  });
40
109
 
41
- it('shows skeleton loading state when isLoading is true', () => {
42
- render(<LogTable logData={[]} isLoading={true} />);
110
+ it('renders table headers when data is present', () => {
111
+ render(<LogTable logData={mockLogData} isLoading={false} />);
112
+ expect(screen.getByText('Procedure')).toBeInTheDocument();
113
+ expect(screen.getByText('Start time')).toBeInTheDocument();
114
+ expect(screen.getByText('End time')).toBeInTheDocument();
115
+ expect(screen.getByText('Completion status')).toBeInTheDocument();
116
+ });
117
+
118
+ it('renders script names in the table', () => {
119
+ render(<LogTable logData={mockLogData} isLoading={false} />);
120
+ expect(screen.getByText('Script A')).toBeInTheDocument();
121
+ expect(screen.getByText('Script B')).toBeInTheDocument();
122
+ });
123
+
124
+ it('renders Success status as a green tag', () => {
125
+ render(<LogTable logData={mockLogData} isLoading={false} />);
126
+ const successTag = screen.getByText('Success');
127
+ expect(successTag).toBeInTheDocument();
128
+ });
129
+
130
+ it('renders Failed status as a red tag', () => {
131
+ render(<LogTable logData={mockLogData} isLoading={false} />);
132
+ const failedTag = screen.getByText('Failed');
133
+ expect(failedTag).toBeInTheDocument();
134
+ });
135
+
136
+ it('renders pagination when data is present', () => {
137
+ render(<LogTable logData={mockLogData} isLoading={false} />);
138
+ expect(screen.getByTestId('pagination')).toBeInTheDocument();
139
+ });
140
+
141
+ it('renders -- for missing start_time', () => {
142
+ const dataWithMissingTime: ETLResponse[] = [
143
+ { script_name: 'Script C', start_time: '', stop_time: '', status: 'Success' },
144
+ ];
145
+ render(<LogTable logData={dataWithMissingTime} isLoading={false} />);
146
+ expect(screen.getAllByText('--').length).toBeGreaterThanOrEqual(2);
147
+ });
43
148
 
44
- const skeleton = screen.getByLabelText('etl table');
45
- expect(skeleton).toBeInTheDocument();
149
+ it('does not show empty state when data is present', () => {
150
+ render(<LogTable logData={mockLogData} isLoading={false} />);
151
+ expect(screen.queryByTestId('empty-state')).not.toBeInTheDocument();
46
152
  });
47
153
  });
@@ -2,20 +2,96 @@
2
2
  @use '@carbon/layout';
3
3
  @use '@carbon/type';
4
4
 
5
+ .modalIntro {
6
+ margin-bottom: layout.$spacing-05;
7
+ color: colors.$gray-70;
8
+ }
9
+
5
10
  .healthWorkerInfoContainer {
6
11
  display: grid;
7
- grid-template-columns: 0.25fr 0.75fr;
8
- margin: layout.$spacing-02;
12
+ grid-template-columns: 0.4fr 0.6fr;
13
+ margin: layout.$spacing-02 0;
14
+ font-size: 0.875rem;
9
15
  }
16
+
10
17
  .healthWorkerInfoLabel {
11
18
  min-width: layout.$spacing-11;
12
- font-weight: bold;
19
+ font-weight: 600;
20
+ color: colors.$gray-70;
13
21
  }
22
+
14
23
  .healthWorkerOverview {
15
24
  display: flex;
16
- margin: layout.$spacing-05;
25
+ align-items: center;
26
+ margin: layout.$spacing-05 0;
27
+ padding: layout.$spacing-05;
28
+ background-color: colors.$gray-10;
29
+ border-radius: 4px;
17
30
  }
31
+
18
32
  .healthWorkerPhoto {
19
33
  display: flex;
20
34
  align-items: center;
35
+ flex-shrink: 0;
36
+ }
37
+
38
+ .healthWorkerSummary {
39
+ flex: 1;
40
+ margin-left: layout.$spacing-05;
41
+ }
42
+
43
+ .healthWorkerName {
44
+ margin: 0 0 layout.$spacing-02 0;
45
+ font-size: 1.25rem;
46
+ font-weight: 600;
47
+ }
48
+
49
+ .healthWorkerSubtitle {
50
+ color: colors.$gray-70;
51
+ font-size: 0.875rem;
52
+ margin-bottom: layout.$spacing-03;
53
+ }
54
+
55
+ .tagRow {
56
+ display: flex;
57
+ gap: layout.$spacing-02;
58
+ flex-wrap: wrap;
59
+ }
60
+
61
+ .sectionHeading {
62
+ margin: layout.$spacing-05 0 layout.$spacing-03 0;
63
+ padding-bottom: layout.$spacing-02;
64
+ border-bottom: 1px solid colors.$gray-20;
65
+ font-size: 0.875rem;
66
+ font-weight: 600;
67
+ text-transform: uppercase;
68
+ letter-spacing: 0.04em;
69
+ color: colors.$gray-70;
70
+ }
71
+
72
+ .licenseList {
73
+ display: flex;
74
+ flex-direction: column;
75
+ gap: layout.$spacing-03;
76
+ }
77
+
78
+ .licenseCard {
79
+ padding: layout.$spacing-04;
80
+ border: 1px solid colors.$gray-20;
81
+ border-radius: 4px;
82
+ background-color: colors.$white;
83
+ }
84
+
85
+ .licenseHeader {
86
+ display: flex;
87
+ justify-content: space-between;
88
+ align-items: center;
89
+ margin-bottom: layout.$spacing-03;
90
+ }
91
+
92
+ .licenseType {
93
+ font-weight: 600;
94
+ text-transform: uppercase;
95
+ font-size: 0.75rem;
96
+ letter-spacing: 0.04em;
21
97
  }
@@ -1,162 +1,152 @@
1
- import { Button, Tag } from '@carbon/react';
1
+ import { Button, Tag, SkeletonText } from '@carbon/react';
2
2
  import { ExtensionSlot } from '@openmrs/esm-framework';
3
- import capitalize from 'lodash-es/capitalize';
4
- import React from 'react';
3
+ import React, { useMemo } from 'react';
5
4
  import { useTranslation } from 'react-i18next';
6
- import { CustomHIEPractitionerResponse, type PractitionerResponse } from '../../types';
7
- import { NormalizedPractitioner } from '../hook/healthWorkerAdapter';
8
- import styles from './hwr-confirmation.modal.scss';
5
+ import { ProfessionalRegistryResponse } from '../hook/healthWorkerRegistry';
9
6
  import { formatDateTime } from '../../utils/utils';
10
-
11
- interface HealthWorkerInfoProps {
12
- label: string;
13
- value: string | boolean | React.ReactNode;
14
- }
15
-
16
- const HealthWorkerInfo: React.FC<HealthWorkerInfoProps> = ({ label, value }) => {
17
- return (
18
- <div className={styles.healthWorkerInfoContainer}>
19
- <span className={styles.healthWorkerInfoLabel}>{label}</span>
20
- <span>{value}</span>
21
- </div>
22
- );
23
- };
7
+ import styles from './hwr-confirmation.modal.scss';
24
8
 
25
9
  interface HWRConfirmModalProps {
26
10
  onConfirm: () => void;
27
11
  close: () => void;
28
- healthWorker: CustomHIEPractitionerResponse | PractitionerResponse;
29
- normalizedData: NormalizedPractitioner;
30
- fhirFormat: boolean;
12
+ healthWorker: ProfessionalRegistryResponse;
31
13
  }
32
14
 
33
- const HWRConfirmModal: React.FC<HWRConfirmModalProps> = ({
34
- close,
35
- onConfirm,
36
- healthWorker,
37
- normalizedData,
38
- fhirFormat,
39
- }) => {
15
+ type License = NonNullable<ProfessionalRegistryResponse['professional']['licenses']>[number];
16
+
17
+ const InfoRow: React.FC<{ label: string; value: React.ReactNode }> = ({ label, value }) => (
18
+ <div className={styles.healthWorkerInfoContainer}>
19
+ <span className={styles.healthWorkerInfoLabel}>{label}</span>
20
+ <span>{value || '--'}</span>
21
+ </div>
22
+ );
23
+
24
+ const SectionHeading: React.FC<{ children: React.ReactNode }> = ({ children }) => (
25
+ <div className={styles.sectionHeading}>{children}</div>
26
+ );
27
+
28
+ const useCurrentLicense = (licenses: Array<License> = []): License | undefined =>
29
+ useMemo(() => {
30
+ if (licenses.length === 0) {
31
+ return undefined;
32
+ }
33
+ const sorted = [...licenses]
34
+ .filter((l) => l.license_end)
35
+ .sort((a, b) => new Date(b.license_end).getTime() - new Date(a.license_end).getTime());
36
+ const now = Date.now();
37
+ return sorted.find((l) => new Date(l.license_end).getTime() >= now) ?? sorted[0];
38
+ }, [licenses]);
39
+
40
+ const HWRConfirmModal: React.FC<HWRConfirmModalProps> = ({ close, onConfirm, healthWorker }) => {
40
41
  const { t } = useTranslation();
42
+ const { professional } = healthWorker;
43
+ const { membership, licenses, contacts, identifiers, professional_details } = professional;
41
44
 
42
- const isLicenseValid = normalizedData.licenseEndDate ? new Date(normalizedData.licenseEndDate) > new Date() : false;
45
+ const currentLicense = useCurrentLicense(licenses);
46
+ const isLicenseValid = currentLicense?.license_end ? new Date(currentLicense.license_end) > new Date() : false;
43
47
 
44
48
  return (
45
49
  <>
46
50
  <div className="cds--modal-header">
47
51
  <h3 className="cds--modal-header__heading">{t('healthWorkerRegistry', 'Health worker registry')}</h3>
48
52
  </div>
53
+
49
54
  <div className="cds--modal-content">
50
- <p>
55
+ <p className={styles.modalIntro}>
51
56
  {t(
52
57
  'healthWorkerDetailsFound',
53
- 'Health worker information found in the registry, do you want to use the information to continue with registration?',
58
+ 'Health worker found in the registry. Use this information to pre-fill the registration form?',
54
59
  )}
55
60
  </p>
61
+
56
62
  <div className={styles.healthWorkerOverview}>
57
63
  <ExtensionSlot
58
64
  className={styles.healthWorkerPhoto}
59
65
  name="patient-photo-slot"
60
- state={{
61
- patientName: normalizedData.fullName || '',
62
- }}
66
+ state={{ patientName: membership.full_name || '' }}
63
67
  />
64
- <div style={{ width: '100%', marginLeft: '0.625rem' }}>
65
- <HealthWorkerInfo
66
- label={t('healthWorkerName', 'Health worker name')}
67
- value={normalizedData.fullName || '--'}
68
- />
69
-
70
- <HealthWorkerInfo
71
- label={t('providerUniqueIdentifier', 'Provider unique identifier')}
72
- value={normalizedData.providerUniqueIdentifier || '--'}
73
- />
74
-
75
- <HealthWorkerInfo
76
- label={t('registrationId', 'Registration ID')}
77
- value={normalizedData.registrationId || '--'}
78
- />
79
-
80
- <HealthWorkerInfo
81
- label={t('externalReferenceId', 'External Reference ID')}
82
- value={normalizedData.externalReferenceId || '--'}
83
- />
84
-
85
- <HealthWorkerInfo label={t('gender', 'Gender')} value={normalizedData.gender || '--'} />
86
-
87
- <HealthWorkerInfo label={t('status', 'Status')} value={normalizedData.status || '--'} />
88
-
89
- {normalizedData.phoneNumber && (
90
- <HealthWorkerInfo label={t('phone', 'Phone')} value={normalizedData.phoneNumber} />
91
- )}
92
-
93
- {normalizedData.email && <HealthWorkerInfo label={t('email', 'Email')} value={normalizedData.email} />}
94
-
95
- {normalizedData.nationalId && (
96
- <HealthWorkerInfo
97
- label={t('identificationNumber', 'Identification Number')}
98
- value={normalizedData.nationalId}
99
- />
100
- )}
101
-
102
- <HealthWorkerInfo
103
- label={t('licensingBody', 'Licensing Body')}
104
- value={normalizedData.licensingBody || '--'}
105
- />
106
-
107
- <HealthWorkerInfo label={t('specialty', 'Specialty')} value={normalizedData.specialty || '--'} />
108
-
109
- {normalizedData.professionalCadre && (
110
- <HealthWorkerInfo
111
- label={t('professionalCadre', 'Professional Cadre')}
112
- value={normalizedData.professionalCadre}
113
- />
114
- )}
115
-
116
- {normalizedData.practiceType && (
117
- <HealthWorkerInfo label={t('practiceType', 'Practice Type')} value={normalizedData.practiceType} />
118
- )}
119
-
120
- {normalizedData.licenses && normalizedData.licenses.length > 0 && (
121
- <>
122
- <div style={{ marginTop: '1rem', marginBottom: '0.5rem' }}>
123
- <strong>{t('licenses', 'Licenses')}</strong>
124
- </div>
125
- {normalizedData.licenses.map((license, index) => (
126
- <div key={index} style={{ marginBottom: '0.5rem', paddingLeft: '1rem' }}>
127
- <HealthWorkerInfo
128
- label={`${license.license_type} ${t('license', 'License')}`}
129
- value={license.external_reference_id || '--'}
130
- />
131
- <HealthWorkerInfo
68
+ <div className={styles.healthWorkerSummary}>
69
+ <h4 className={styles.healthWorkerName}>{membership.full_name || '--'}</h4>
70
+ <div className={styles.healthWorkerSubtitle}>
71
+ {membership.specialty || t('practitioner', 'Practitioner')}
72
+ {membership.licensing_body && ` · ${membership.licensing_body}`}
73
+ </div>
74
+ <div className={styles.tagRow}>
75
+ <Tag type={membership.is_active === 1 ? 'green' : 'gray'} size="md">
76
+ {membership.is_active === 1 ? t('active', 'Active') : t('inactive', 'Inactive')}
77
+ </Tag>
78
+ <Tag type={isLicenseValid ? 'green' : 'red'} size="md">
79
+ {isLicenseValid ? t('licenseValid', 'License valid') : t('licenseExpired', 'License expired')}
80
+ </Tag>
81
+ </div>
82
+ </div>
83
+ </div>
84
+
85
+ <SectionHeading>{t('identity', 'Identity')}</SectionHeading>
86
+ <InfoRow label={t('providerUniqueIdentifier', 'Provider unique identifier')} value={membership.id} />
87
+ <InfoRow label={t('registrationId', 'Registration ID')} value={membership.registration_id} />
88
+ <InfoRow label={t('externalReferenceId', 'External reference ID')} value={membership.external_reference_id} />
89
+ <InfoRow label={t('gender', 'Gender')} value={membership.gender} />
90
+ {identifiers?.identification_number && (
91
+ <InfoRow
92
+ label={t('identificationNumber', 'Identification number')}
93
+ value={`${identifiers.identification_type ?? ''} ${identifiers.identification_number}`.trim()}
94
+ />
95
+ )}
96
+
97
+ <SectionHeading>{t('licensing', 'Licensing & qualifications')}</SectionHeading>
98
+ <InfoRow label={t('licensingBody', 'Licensing body')} value={membership.licensing_body} />
99
+ <InfoRow label={t('specialty', 'Specialty')} value={membership.specialty} />
100
+ {professional_details?.professional_cadre && (
101
+ <InfoRow
102
+ label={t('professionalCadre', 'Professional cadre')}
103
+ value={professional_details.professional_cadre}
104
+ />
105
+ )}
106
+ {professional_details?.practice_type && (
107
+ <InfoRow label={t('practiceType', 'Practice type')} value={professional_details.practice_type} />
108
+ )}
109
+ {professional_details?.educational_qualifications && (
110
+ <InfoRow
111
+ label={t('qualification', 'Qualification')}
112
+ value={professional_details.educational_qualifications}
113
+ />
114
+ )}
115
+
116
+ {licenses && licenses.length > 0 && (
117
+ <>
118
+ <SectionHeading>{t('licenses', 'Licenses')}</SectionHeading>
119
+ <div className={styles.licenseList}>
120
+ {licenses.map((license) => {
121
+ const expired = license.license_end && new Date(license.license_end) < new Date();
122
+ return (
123
+ <div key={license.id} className={styles.licenseCard}>
124
+ <div className={styles.licenseHeader}>
125
+ <span className={styles.licenseType}>{license.license_type}</span>
126
+ <Tag type={expired ? 'red' : 'green'} size="sm">
127
+ {expired ? t('expired', 'Expired') : t('valid', 'Valid')}
128
+ </Tag>
129
+ </div>
130
+ <InfoRow label={t('licenseNumber', 'License no.')} value={license.external_reference_id} />
131
+ <InfoRow
132
132
  label={t('validity', 'Validity')}
133
- value={`${formatDateTime(license.license_start)} - ${formatDateTime(license.license_end)}`}
133
+ value={`${formatDateTime(license.license_start)} ${formatDateTime(license.license_end)}`}
134
134
  />
135
135
  </div>
136
- ))}
137
- </>
138
- )}
139
-
140
- {normalizedData.licenseStartDate && normalizedData.licenseEndDate && (
141
- <HealthWorkerInfo
142
- label={t('licenseValidity', 'License Validity Period')}
143
- value={`${formatDateTime(normalizedData.licenseStartDate)} - ${formatDateTime(
144
- normalizedData.licenseEndDate,
145
- )}`}
146
- />
147
- )}
148
-
149
- <HealthWorkerInfo
150
- label={t('licenseValid', 'License Validity')}
151
- value={
152
- <Tag type={isLicenseValid ? 'green' : 'red'}>
153
- {isLicenseValid ? t('licenseValid', 'License Valid') : t('licenseExpired', 'License Expired')}
154
- </Tag>
155
- }
156
- />
157
- </div>
158
- </div>
136
+ );
137
+ })}
138
+ </div>
139
+ </>
140
+ )}
141
+
142
+ <SectionHeading>{t('contact', 'Contact')}</SectionHeading>
143
+ <InfoRow label={t('phone', 'Phone')} value={contacts?.phone} />
144
+ <InfoRow label={t('email', 'Email')} value={contacts?.email} />
145
+ {contacts?.postal_address && (
146
+ <InfoRow label={t('postalAddress', 'Postal address')} value={contacts.postal_address} />
147
+ )}
159
148
  </div>
149
+
160
150
  <div className="cds--modal-footer">
161
151
  <Button kind="secondary" onClick={close}>
162
152
  {t('cancel', 'Cancel')}