@kenyaemr/esm-reports-app 5.4.4-pre.483

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 (252) hide show
  1. package/.turbo/turbo-build.log +7 -0
  2. package/dist/1072.js +1 -0
  3. package/dist/1072.js.map +1 -0
  4. package/dist/1148.js +1 -0
  5. package/dist/1148.js.map +1 -0
  6. package/dist/1278.js +1 -0
  7. package/dist/1278.js.map +1 -0
  8. package/dist/1442.js +1 -0
  9. package/dist/1442.js.map +1 -0
  10. package/dist/15.js +1 -0
  11. package/dist/15.js.map +1 -0
  12. package/dist/1620.js +1 -0
  13. package/dist/1620.js.map +1 -0
  14. package/dist/1789.js +1 -0
  15. package/dist/1789.js.map +1 -0
  16. package/dist/1831.js +1 -0
  17. package/dist/1831.js.map +1 -0
  18. package/dist/1844.js +1 -0
  19. package/dist/1844.js.map +1 -0
  20. package/dist/1854.js +1 -0
  21. package/dist/1854.js.map +1 -0
  22. package/dist/1919.js +1 -0
  23. package/dist/1919.js.map +1 -0
  24. package/dist/1946.js +1 -0
  25. package/dist/1946.js.map +1 -0
  26. package/dist/2146.js +1 -0
  27. package/dist/2146.js.map +1 -0
  28. package/dist/2152.js +1 -0
  29. package/dist/2152.js.map +1 -0
  30. package/dist/2202.js +1 -0
  31. package/dist/2202.js.map +1 -0
  32. package/dist/2394.js +1 -0
  33. package/dist/2394.js.map +1 -0
  34. package/dist/2534.js +1 -0
  35. package/dist/2534.js.map +1 -0
  36. package/dist/2537.js +1 -0
  37. package/dist/2537.js.map +1 -0
  38. package/dist/2556.js +1 -0
  39. package/dist/2556.js.map +1 -0
  40. package/dist/2648.js +1 -0
  41. package/dist/2648.js.map +1 -0
  42. package/dist/2842.js +1 -0
  43. package/dist/2842.js.map +1 -0
  44. package/dist/2916.js +1 -0
  45. package/dist/2916.js.map +1 -0
  46. package/dist/2924.js +1 -0
  47. package/dist/2924.js.map +1 -0
  48. package/dist/2947.js +1 -0
  49. package/dist/2947.js.map +1 -0
  50. package/dist/3032.js +1 -0
  51. package/dist/3032.js.map +1 -0
  52. package/dist/310.js +1 -0
  53. package/dist/310.js.map +1 -0
  54. package/dist/3285.js +1 -0
  55. package/dist/3285.js.map +1 -0
  56. package/dist/3335.js +1 -0
  57. package/dist/3335.js.map +1 -0
  58. package/dist/335.js +1 -0
  59. package/dist/335.js.map +1 -0
  60. package/dist/3378.js +1 -0
  61. package/dist/3378.js.map +1 -0
  62. package/dist/3504.js +1 -0
  63. package/dist/3504.js.map +1 -0
  64. package/dist/356.js +1 -0
  65. package/dist/356.js.map +1 -0
  66. package/dist/3569.js +1 -0
  67. package/dist/3569.js.map +1 -0
  68. package/dist/3723.js +1 -0
  69. package/dist/3723.js.map +1 -0
  70. package/dist/3745.js +1 -0
  71. package/dist/3745.js.map +1 -0
  72. package/dist/3813.js +1 -0
  73. package/dist/3813.js.map +1 -0
  74. package/dist/393.js +6 -0
  75. package/dist/393.js.map +1 -0
  76. package/dist/3953.js +1 -0
  77. package/dist/3953.js.map +1 -0
  78. package/dist/3989.js +1 -0
  79. package/dist/3989.js.map +1 -0
  80. package/dist/3997.js +1 -0
  81. package/dist/3997.js.map +1 -0
  82. package/dist/4178.js +1 -0
  83. package/dist/4178.js.map +1 -0
  84. package/dist/4487.js +11 -0
  85. package/dist/4487.js.map +1 -0
  86. package/dist/4588.js +1 -0
  87. package/dist/4588.js.map +1 -0
  88. package/dist/4637.js +1 -0
  89. package/dist/4637.js.map +1 -0
  90. package/dist/466.js +1 -0
  91. package/dist/466.js.map +1 -0
  92. package/dist/4817.js +1 -0
  93. package/dist/4817.js.map +1 -0
  94. package/dist/4962.js +1 -0
  95. package/dist/4962.js.map +1 -0
  96. package/dist/500.js +1 -0
  97. package/dist/500.js.map +1 -0
  98. package/dist/5055.js +1 -0
  99. package/dist/5055.js.map +1 -0
  100. package/dist/5086.js +1 -0
  101. package/dist/5086.js.map +1 -0
  102. package/dist/510.js +1 -0
  103. package/dist/510.js.map +1 -0
  104. package/dist/5117.js +1 -0
  105. package/dist/5117.js.map +1 -0
  106. package/dist/5279.js +1 -0
  107. package/dist/5279.js.map +1 -0
  108. package/dist/5366.js +1 -0
  109. package/dist/5366.js.map +1 -0
  110. package/dist/5598.js +1 -0
  111. package/dist/5598.js.map +1 -0
  112. package/dist/5676.js +1 -0
  113. package/dist/5676.js.map +1 -0
  114. package/dist/5731.js +5 -0
  115. package/dist/5731.js.map +1 -0
  116. package/dist/5792.js +1 -0
  117. package/dist/5792.js.map +1 -0
  118. package/dist/5809.js +1 -0
  119. package/dist/5809.js.map +1 -0
  120. package/dist/5825.js +1 -0
  121. package/dist/5825.js.map +1 -0
  122. package/dist/5932.js +1 -0
  123. package/dist/5932.js.map +1 -0
  124. package/dist/5941.js +1 -0
  125. package/dist/5941.js.map +1 -0
  126. package/dist/5967.js +1 -0
  127. package/dist/5967.js.map +1 -0
  128. package/dist/6104.js +1 -0
  129. package/dist/6104.js.map +1 -0
  130. package/dist/6273.js +1 -0
  131. package/dist/6273.js.map +1 -0
  132. package/dist/6337.js +1 -0
  133. package/dist/6337.js.map +1 -0
  134. package/dist/6357.js +1 -0
  135. package/dist/6357.js.map +1 -0
  136. package/dist/6427.js +1 -0
  137. package/dist/6427.js.map +1 -0
  138. package/dist/6478.js +7 -0
  139. package/dist/6478.js.map +1 -0
  140. package/dist/6508.js +1 -0
  141. package/dist/6508.js.map +1 -0
  142. package/dist/6561.js +1 -0
  143. package/dist/6561.js.map +1 -0
  144. package/dist/6567.js +1 -0
  145. package/dist/6567.js.map +1 -0
  146. package/dist/6634.js +1 -0
  147. package/dist/6634.js.map +1 -0
  148. package/dist/689.js +1 -0
  149. package/dist/689.js.map +1 -0
  150. package/dist/6998.js +1 -0
  151. package/dist/6998.js.map +1 -0
  152. package/dist/7022.js +1 -0
  153. package/dist/7022.js.map +1 -0
  154. package/dist/7085.js +1 -0
  155. package/dist/7085.js.map +1 -0
  156. package/dist/7209.js +1 -0
  157. package/dist/7209.js.map +1 -0
  158. package/dist/7360.js +1 -0
  159. package/dist/7360.js.map +1 -0
  160. package/dist/740.js +1 -0
  161. package/dist/740.js.map +1 -0
  162. package/dist/7454.js +1 -0
  163. package/dist/7454.js.map +1 -0
  164. package/dist/7478.js +1 -0
  165. package/dist/7478.js.map +1 -0
  166. package/dist/7782.js +1 -0
  167. package/dist/7782.js.map +1 -0
  168. package/dist/781.js +1 -0
  169. package/dist/781.js.map +1 -0
  170. package/dist/8028.js +1 -0
  171. package/dist/8028.js.map +1 -0
  172. package/dist/8196.js +1 -0
  173. package/dist/8196.js.map +1 -0
  174. package/dist/820.js +1 -0
  175. package/dist/820.js.map +1 -0
  176. package/dist/8240.js +1 -0
  177. package/dist/8240.js.map +1 -0
  178. package/dist/8472.js +1 -0
  179. package/dist/8472.js.map +1 -0
  180. package/dist/8607.js +1 -0
  181. package/dist/8607.js.map +1 -0
  182. package/dist/8762.js +1 -0
  183. package/dist/8762.js.map +1 -0
  184. package/dist/8924.js +1 -0
  185. package/dist/8924.js.map +1 -0
  186. package/dist/9011.js +1 -0
  187. package/dist/9011.js.map +1 -0
  188. package/dist/9060.js +1 -0
  189. package/dist/9060.js.map +1 -0
  190. package/dist/9061.js +1 -0
  191. package/dist/9061.js.map +1 -0
  192. package/dist/9087.js +1 -0
  193. package/dist/9087.js.map +1 -0
  194. package/dist/9168.js +1 -0
  195. package/dist/9168.js.map +1 -0
  196. package/dist/9425.js +1 -0
  197. package/dist/9520.js +1 -0
  198. package/dist/9520.js.map +1 -0
  199. package/dist/9547.js +1 -0
  200. package/dist/9547.js.map +1 -0
  201. package/dist/9612.js +1 -0
  202. package/dist/9612.js.map +1 -0
  203. package/dist/9771.js +1 -0
  204. package/dist/9771.js.map +1 -0
  205. package/dist/9774.js +1 -0
  206. package/dist/9774.js.map +1 -0
  207. package/dist/9806.js +1 -0
  208. package/dist/9806.js.map +1 -0
  209. package/dist/9885.js +1 -0
  210. package/dist/9885.js.map +1 -0
  211. package/dist/992.js +1 -0
  212. package/dist/992.js.map +1 -0
  213. package/dist/kenyaemr-esm-reports-app.js +6 -0
  214. package/dist/kenyaemr-esm-reports-app.js.buildmanifest.json +2960 -0
  215. package/dist/kenyaemr-esm-reports-app.js.map +1 -0
  216. package/dist/main.js +6 -0
  217. package/dist/main.js.map +1 -0
  218. package/dist/routes.json +1 -0
  219. package/package.json +49 -0
  220. package/rspack.config.js +1 -0
  221. package/src/components/group-reports.component.tsx +98 -0
  222. package/src/components/group-reports.scss +13 -0
  223. package/src/components/header/header.component.tsx +20 -0
  224. package/src/components/header/header.scss +5 -0
  225. package/src/components/report-download-menu/report-download-menu.component.tsx +71 -0
  226. package/src/components/report-history/report-history.component.tsx +334 -0
  227. package/src/components/report-history/report-history.scss +175 -0
  228. package/src/components/report-table/report-panel.component.tsx +20 -0
  229. package/src/components/report-table/report-table.component.tsx +162 -0
  230. package/src/components/report-table/report-table.scss +10 -0
  231. package/src/components/report-view/report-view.component.tsx +183 -0
  232. package/src/components/report-view/report-view.scss +111 -0
  233. package/src/components/utils.tsx +65 -0
  234. package/src/config-schema.ts +1 -0
  235. package/src/declarations.d.ts +2 -0
  236. package/src/hooks/useReportData.tsx +20 -0
  237. package/src/hooks/useReportDefinition.tsx +33 -0
  238. package/src/hooks/useReportRequests.tsx +26 -0
  239. package/src/hooks/useReports.tsx +30 -0
  240. package/src/index.ts +20 -0
  241. package/src/root.component.tsx +24 -0
  242. package/src/routes.json +40 -0
  243. package/src/types/index.ts +96 -0
  244. package/src/usePrintPreview.tsx +17 -0
  245. package/src/workspaces/report-request.workspace.scss +61 -0
  246. package/src/workspaces/report-request.workspaces.tsx +266 -0
  247. package/translations/am.json +49 -0
  248. package/translations/en.json +49 -0
  249. package/translations/fr.json +50 -0
  250. package/translations/sw.json +49 -0
  251. package/tsconfig.json +5 -0
  252. package/webpack.config.js +1 -0
@@ -0,0 +1,334 @@
1
+ import React, { useMemo } from 'react';
2
+ import { useNavigate, useParams } from 'react-router-dom';
3
+ import { useTranslation } from 'react-i18next';
4
+ import {
5
+ Button,
6
+ DataTable,
7
+ InlineNotification,
8
+ Pagination,
9
+ SkeletonText,
10
+ Table,
11
+ TableBody,
12
+ TableCell,
13
+ TableContainer,
14
+ TableHead,
15
+ TableHeader,
16
+ TableRow,
17
+ TableToolbar,
18
+ TableToolbarContent,
19
+ TableToolbarSearch,
20
+ Tag,
21
+ Tile,
22
+ type DataTableHeader,
23
+ } from '@carbon/react';
24
+ import { ArrowLeft, Play, View } from '@carbon/react/icons';
25
+ import '@carbon/charts/styles.css';
26
+ import {
27
+ ErrorState,
28
+ formatDatetime,
29
+ launchWorkspace2,
30
+ parseDate,
31
+ useDebounce,
32
+ usePagination,
33
+ } from '@openmrs/esm-framework';
34
+
35
+ import { useReportDefinition } from '../../hooks/useReportDefinition';
36
+ import { useReportRequests } from '../../hooks/useReportRequests';
37
+ import { type ReportRequest } from '../../types';
38
+ import ReportDownloadMenu from '../report-download-menu/report-download-menu.component';
39
+ import styles from './report-history.scss';
40
+
41
+ const PAGE_SIZE = 10;
42
+ const COMPLETED = 'COMPLETED';
43
+ const FAILED = 'FAILED';
44
+
45
+ const statusTagType = (status: string): 'green' | 'blue' | 'red' | 'gray' => {
46
+ switch (status) {
47
+ case COMPLETED:
48
+ return 'green';
49
+ case FAILED:
50
+ return 'red';
51
+ case 'PROCESSING':
52
+ case 'SCHEDULED':
53
+ case 'REQUESTED':
54
+ return 'blue';
55
+ default:
56
+ return 'gray';
57
+ }
58
+ };
59
+
60
+ const durationMs = (request: ReportRequest): number | null => {
61
+ if (!request.evaluateStartDatetime || !request.evaluateCompleteDatetime) {
62
+ return null;
63
+ }
64
+ return parseDate(request.evaluateCompleteDatetime).getTime() - parseDate(request.evaluateStartDatetime).getTime();
65
+ };
66
+
67
+ const formatDuration = (ms: number | null): string => {
68
+ if (ms === null) {
69
+ return '--';
70
+ }
71
+ const totalSeconds = Math.floor(ms / 1000);
72
+ const hours = `${Math.floor(totalSeconds / 3600)}`.padStart(2, '0');
73
+ const minutes = `${Math.floor((totalSeconds % 3600) / 60)}`.padStart(2, '0');
74
+ const seconds = `${totalSeconds % 60}`.padStart(2, '0');
75
+ const hundredths = `${Math.floor((ms % 1000) / 10)}`.padStart(2, '0');
76
+ return `${hours}:${minutes}:${seconds}.${hundredths}`;
77
+ };
78
+
79
+ const ReportHistory: React.FC = () => {
80
+ const { t } = useTranslation();
81
+ const navigate = useNavigate();
82
+ const { reportUuid = '' } = useParams();
83
+
84
+ const { report, isLoading: isLoadingReport, error: reportError } = useReportDefinition(reportUuid);
85
+ const { requests, isLoading: isLoadingRequests, error: requestsError } = useReportRequests(reportUuid);
86
+
87
+ const [searchTerm, setSearchTerm] = React.useState('');
88
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
89
+
90
+ const sortedRequests = useMemo(
91
+ () => [...requests].sort((a, b) => parseDate(b.requestDate).getTime() - parseDate(a.requestDate).getTime()),
92
+ [requests],
93
+ );
94
+
95
+ const metrics = useMemo(() => {
96
+ const total = sortedRequests.length;
97
+ const completed = sortedRequests.filter((request) => request.status === COMPLETED);
98
+ const failed = sortedRequests.filter((request) => request.status === FAILED);
99
+ const durations = sortedRequests.map(durationMs).filter((value): value is number => value !== null);
100
+ const avgMs = durations.length ? durations.reduce((sum, value) => sum + value, 0) / durations.length : null;
101
+ const successRate = total ? (completed.length / total) * 100 : 0;
102
+
103
+ return {
104
+ total,
105
+ completed: completed.length,
106
+ failed: failed.length,
107
+ pending: total - completed.length - failed.length,
108
+ successRate,
109
+ avgSeconds: avgMs !== null ? avgMs / 1000 : null,
110
+ lastRunBy: sortedRequests[0]?.requestedBy?.display ?? '--',
111
+ };
112
+ }, [sortedRequests]);
113
+
114
+ const filteredRequests = useMemo(() => {
115
+ if (!debouncedSearchTerm) {
116
+ return sortedRequests;
117
+ }
118
+ const term = debouncedSearchTerm.toLowerCase();
119
+ return sortedRequests.filter((request) =>
120
+ [request.requestedBy?.display, request.status, request.requestDate]
121
+ .filter(Boolean)
122
+ .some((value) => String(value).toLowerCase().includes(term)),
123
+ );
124
+ }, [sortedRequests, debouncedSearchTerm]);
125
+
126
+ const { results, currentPage, goTo } = usePagination(filteredRequests, PAGE_SIZE);
127
+
128
+ const headers: Array<DataTableHeader> = useMemo(
129
+ () => [
130
+ { key: 'requestDate', header: t('requestedDateTime', 'Requested Date & Time') },
131
+ { key: 'requestedBy', header: t('requestedBy', 'Requested By') },
132
+ { key: 'status', header: t('status', 'Status') },
133
+ { key: 'timeTaken', header: t('timeTaken', 'Time Taken') },
134
+ ],
135
+ [t],
136
+ );
137
+
138
+ const rows = useMemo(
139
+ () =>
140
+ results.map((request) => ({
141
+ id: String(request.id),
142
+ requestDate: formatDatetime(parseDate(request.requestDate), { mode: 'standard' }),
143
+ requestedBy: request.requestedBy?.display ?? t('selfServicePortal', 'Self-Service Portal'),
144
+ status: request.status,
145
+ timeTaken: formatDuration(durationMs(request)),
146
+ })),
147
+ [results, t],
148
+ );
149
+
150
+ const handleRunReport = () => {
151
+ launchWorkspace2('report-request-workspace', { reportUuid });
152
+ };
153
+
154
+ if (isLoadingReport || isLoadingRequests) {
155
+ return (
156
+ <div className={styles.loadingState}>
157
+ <SkeletonText heading width="40%" />
158
+ <SkeletonText paragraph lineCount={3} />
159
+ </div>
160
+ );
161
+ }
162
+
163
+ if (reportError || requestsError) {
164
+ return (
165
+ <div className={styles.errorState}>
166
+ <ErrorState error={reportError ?? requestsError} headerTitle={t('reportHistory', 'Report History')} />
167
+ </div>
168
+ );
169
+ }
170
+
171
+ return (
172
+ <div className={styles.container}>
173
+ <div className={styles.breadcrumb}>
174
+ <button type="button" className={styles.backButton} onClick={() => navigate('/')}>
175
+ <ArrowLeft size={16} />
176
+ {t('backToAllReports', 'Back to all reports')}
177
+ </button>
178
+ <span>/</span>
179
+ <span>{t('reportHistory', 'Report History')}</span>
180
+ </div>
181
+
182
+ <div className={styles.titleRow}>
183
+ <div className={styles.titleGroup}>
184
+ <div className={styles.titleLine}>
185
+ <h2 className={styles.title}>{report?.name}</h2>
186
+ {report?.type && <Tag type="blue">{t('reportType', 'Report')}</Tag>}
187
+ </div>
188
+ {report?.description && <p className={styles.description}>{report.description}</p>}
189
+ </div>
190
+ <Button kind="primary" renderIcon={Play} onClick={handleRunReport}>
191
+ {t('runReportNow', 'Run Report Now')}
192
+ </Button>
193
+ </div>
194
+
195
+ <div className={styles.tiles}>
196
+ <Tile className={styles.tile}>
197
+ <p className={styles.tileLabel}>{t('totalRuns', 'Total Runs')}</p>
198
+ <div className={styles.tileValueRow}>
199
+ <span className={styles.tileValue}>{metrics.total}</span>
200
+ </div>
201
+ </Tile>
202
+ <Tile className={styles.tile}>
203
+ <p className={styles.tileLabel}>{t('successRate', 'Success Rate')}</p>
204
+ <div className={styles.tileValueRow}>
205
+ <span className={styles.tileValue}>{metrics.successRate.toFixed(1)}%</span>
206
+ </div>
207
+ </Tile>
208
+ <Tile className={styles.tile}>
209
+ <p className={styles.tileLabel}>{t('avgGenerationTime', 'Avg. Generation Time')}</p>
210
+ <div className={styles.tileValueRow}>
211
+ <span className={styles.tileValue}>
212
+ {metrics.avgSeconds !== null ? `${metrics.avgSeconds.toFixed(2)}s` : '--'}
213
+ </span>
214
+ </div>
215
+ </Tile>
216
+ <Tile className={styles.tile}>
217
+ <p className={styles.tileLabel}>{t('lastRunBy', 'Last Run By')}</p>
218
+ <div className={styles.lastRunBy}>
219
+ <span className={styles.tileValue} style={{ fontSize: '1rem' }}>
220
+ {metrics.lastRunBy}
221
+ </span>
222
+ </div>
223
+ </Tile>
224
+ </div>
225
+
226
+ <div className={styles.tableSection}>
227
+ <DataTable size="sm" rows={rows} headers={headers} isSortable useZebraStyles>
228
+ {({
229
+ rows: tableRows,
230
+ headers: tableHeaders,
231
+ getHeaderProps,
232
+ getRowProps,
233
+ getTableProps,
234
+ getToolbarProps,
235
+ }) => (
236
+ <TableContainer
237
+ title={t('reportExecutionHistory', 'Report Execution History')}
238
+ description={t('reportExecutionHistoryDescription', 'A list of all the runs of this report.')}>
239
+ <TableToolbar {...getToolbarProps()}>
240
+ <TableToolbarContent>
241
+ <TableToolbarSearch
242
+ persistent
243
+ placeholder={t('searchHistory', 'Search history')}
244
+ onChange={(event: React.ChangeEvent<HTMLInputElement> | '', value?: string) => {
245
+ setSearchTerm(typeof event === 'string' ? value ?? '' : event.target.value);
246
+ goTo(1);
247
+ }}
248
+ />
249
+ </TableToolbarContent>
250
+ </TableToolbar>
251
+ <Table {...getTableProps()} aria-label={t('reportExecutionHistory', 'Report Execution History')}>
252
+ <TableHead>
253
+ <TableRow>
254
+ {tableHeaders.map((header) => (
255
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
256
+ ))}
257
+ <TableHeader aria-label={t('actions', 'Actions')} />
258
+ </TableRow>
259
+ </TableHead>
260
+ <TableBody>
261
+ {tableRows.map((row, index) => {
262
+ const request = results[index];
263
+ const isCompleted = request?.status === COMPLETED;
264
+ return (
265
+ <TableRow {...getRowProps({ row })} key={row.id}>
266
+ {row.cells.map((cell) => {
267
+ if (cell.info.header === 'status') {
268
+ return (
269
+ <TableCell key={cell.id}>
270
+ <Tag type={statusTagType(cell.value)} size="sm">
271
+ {cell.value}
272
+ </Tag>
273
+ </TableCell>
274
+ );
275
+ }
276
+ if (cell.info.header === 'timeTaken' && !isCompleted) {
277
+ return (
278
+ <TableCell key={cell.id}>
279
+ <span className={styles.mutedText}>{t('processing', 'Processing...')}</span>
280
+ </TableCell>
281
+ );
282
+ }
283
+ return <TableCell key={cell.id}>{cell.value}</TableCell>;
284
+ })}
285
+ <TableCell>
286
+ <div className={styles.actionsCell}>
287
+ {isCompleted ? (
288
+ <>
289
+ <button
290
+ type="button"
291
+ className={styles.actionLink}
292
+ onClick={() => navigate(`/report/${reportUuid}/requests/${request.id}`)}>
293
+ <View size={16} /> {t('view', 'View')}
294
+ </button>
295
+ <ReportDownloadMenu request={request} reportName={report?.name} />
296
+ </>
297
+ ) : (
298
+ <span className={styles.mutedText}>{t('availableSoon', 'Available soon')}</span>
299
+ )}
300
+ </div>
301
+ </TableCell>
302
+ </TableRow>
303
+ );
304
+ })}
305
+ </TableBody>
306
+ </Table>
307
+ </TableContainer>
308
+ )}
309
+ </DataTable>
310
+ {filteredRequests.length === 0 ? (
311
+ <InlineNotification
312
+ lowContrast
313
+ kind="info"
314
+ title={t('noRuns', 'No runs yet')}
315
+ subtitle={t('noRunsSubtitle', 'This report has not been requested yet.')}
316
+ />
317
+ ) : (
318
+ <Pagination
319
+ page={currentPage}
320
+ pageSize={PAGE_SIZE}
321
+ pageSizes={[PAGE_SIZE]}
322
+ totalItems={filteredRequests.length}
323
+ itemRangeText={(min, max, total) =>
324
+ t('showingRuns', 'Showing {{min}} to {{max}} of {{total}} runs', { min, max, total })
325
+ }
326
+ onChange={({ page }) => goTo(page)}
327
+ />
328
+ )}
329
+ </div>
330
+ </div>
331
+ );
332
+ };
333
+
334
+ export default ReportHistory;
@@ -0,0 +1,175 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+
5
+ .container {
6
+ display: flex;
7
+ flex-direction: column;
8
+ gap: layout.$spacing-06;
9
+ background-color: colors.$white;
10
+ padding: layout.$spacing-05 layout.$spacing-07;
11
+ margin-top: layout.$spacing-02;
12
+ border: 1px solid colors.$gray-10;
13
+ }
14
+
15
+ .breadcrumb {
16
+ display: flex;
17
+ align-items: center;
18
+ gap: layout.$spacing-03;
19
+ @include type.type-style('body-compact-01');
20
+ color: colors.$gray-70;
21
+ }
22
+
23
+ .backButton {
24
+ display: inline-flex;
25
+ align-items: center;
26
+ gap: layout.$spacing-02;
27
+ background: none;
28
+ border: none;
29
+ cursor: pointer;
30
+ padding: 0;
31
+ color: colors.$blue-60;
32
+ @include type.type-style('body-compact-01');
33
+
34
+ &:hover {
35
+ text-decoration: underline;
36
+ }
37
+ }
38
+
39
+ .titleRow {
40
+ display: flex;
41
+ align-items: flex-start;
42
+ justify-content: space-between;
43
+ gap: layout.$spacing-05;
44
+ }
45
+
46
+ .titleGroup {
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: layout.$spacing-03;
50
+ }
51
+
52
+ .titleLine {
53
+ display: flex;
54
+ align-items: center;
55
+ gap: layout.$spacing-04;
56
+ }
57
+
58
+ .title {
59
+ @include type.type-style('heading-05');
60
+ color: colors.$gray-100;
61
+ margin: 0;
62
+ }
63
+
64
+ .description {
65
+ @include type.type-style('body-01');
66
+ color: colors.$gray-70;
67
+ max-width: 42rem;
68
+ margin: 0;
69
+ }
70
+
71
+ .tiles {
72
+ display: grid;
73
+ grid-template-columns: repeat(4, 1fr);
74
+ gap: layout.$spacing-05;
75
+ }
76
+
77
+ .tile {
78
+ min-height: 6rem;
79
+ }
80
+
81
+ .tileLabel {
82
+ @include type.type-style('label-01');
83
+ text-transform: uppercase;
84
+ letter-spacing: 0.04em;
85
+ color: colors.$gray-70;
86
+ }
87
+
88
+ .tileValueRow {
89
+ display: flex;
90
+ align-items: baseline;
91
+ gap: layout.$spacing-03;
92
+ margin-top: layout.$spacing-03;
93
+ }
94
+
95
+ .tileValue {
96
+ @include type.type-style('heading-05');
97
+ color: colors.$gray-100;
98
+ }
99
+
100
+ .trendUp {
101
+ @include type.type-style('label-01');
102
+ color: colors.$green-60;
103
+ }
104
+
105
+ .trendDown {
106
+ @include type.type-style('label-01');
107
+ color: colors.$red-60;
108
+ }
109
+
110
+ .lastRunBy {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: layout.$spacing-03;
114
+ margin-top: layout.$spacing-03;
115
+ }
116
+
117
+ .tableSection {
118
+ border: 1px solid colors.$gray-20;
119
+
120
+ :global(.cds--table-toolbar) {
121
+ position: relative !important;
122
+ }
123
+ }
124
+
125
+ .actionsCell {
126
+ display: flex;
127
+ align-items: center;
128
+ gap: layout.$spacing-05;
129
+ justify-content: flex-end;
130
+ }
131
+
132
+ .actionLink {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: layout.$spacing-02;
136
+ color: colors.$blue-60;
137
+ text-decoration: none;
138
+ white-space: nowrap;
139
+ background: none;
140
+ border: none;
141
+ padding: 0;
142
+ cursor: pointer;
143
+
144
+ &:hover {
145
+ text-decoration: underline;
146
+ }
147
+ }
148
+
149
+ .mutedText {
150
+ color: colors.$gray-50;
151
+ font-style: italic;
152
+ }
153
+
154
+ .charts {
155
+ display: grid;
156
+ grid-template-columns: 1fr 1fr;
157
+ gap: layout.$spacing-05;
158
+ }
159
+
160
+ .chartTile {
161
+ padding: layout.$spacing-05;
162
+ }
163
+
164
+ .chartTitle {
165
+ @include type.type-style('label-01');
166
+ text-transform: uppercase;
167
+ letter-spacing: 0.04em;
168
+ color: colors.$gray-70;
169
+ margin-bottom: layout.$spacing-04;
170
+ }
171
+
172
+ .errorState,
173
+ .loadingState {
174
+ padding: layout.$spacing-07;
175
+ }
@@ -0,0 +1,20 @@
1
+ import React from 'react';
2
+ import { ContentSwitcher, Switch } from '@carbon/react';
3
+ import { type SwitchEventHandlersParams } from '@carbon/react/lib/components/Switch/Switch';
4
+ import { useTranslation } from 'react-i18next';
5
+
6
+ type ReportPanelProps = {
7
+ onSwitchChange?: (params: SwitchEventHandlersParams) => void;
8
+ };
9
+
10
+ const ReportPanel: React.FC<ReportPanelProps> = ({ onSwitchChange }) => {
11
+ const { t } = useTranslation();
12
+ return (
13
+ <ContentSwitcher size="lg" onChange={onSwitchChange ?? (() => undefined)} selectedIndex={0}>
14
+ <Switch name="indicators" text={t('indicators', 'Indicators')} />
15
+ <Switch name="patientFollowUpReports " text={t('patientFollowUpReports', 'Patient Follow-up Reports')} />
16
+ </ContentSwitcher>
17
+ );
18
+ };
19
+
20
+ export default ReportPanel;
@@ -0,0 +1,162 @@
1
+ import React, { useCallback, useState } from 'react';
2
+ import {
3
+ Table,
4
+ TableHead,
5
+ TableRow,
6
+ TableHeader,
7
+ TableBody,
8
+ TableCell,
9
+ DataTable,
10
+ TableContainer,
11
+ OverflowMenu,
12
+ OverflowMenuItem,
13
+ TableToolbar,
14
+ TableToolbarContent,
15
+ TableToolbarSearch,
16
+ Tag,
17
+ Pagination,
18
+ } from '@carbon/react';
19
+
20
+ import styles from './report-table.scss';
21
+ import { usePaginationInfo, usePagination, useDebounce, launchWorkspace2, navigate } from '@openmrs/esm-framework';
22
+ import { useTranslation } from 'react-i18next';
23
+
24
+ type ReportTableProps = {
25
+ tableRows: Array<any>;
26
+ tableHeaders: Array<any>;
27
+ tableTitle: string;
28
+ tableDescription: string;
29
+ };
30
+
31
+ const ReportTable: React.FC<ReportTableProps> = ({ tableRows, tableHeaders, tableTitle, tableDescription }) => {
32
+ const [pageSize, setPageSize] = useState(10);
33
+ const [searchTerm, setSearchTerm] = useState('');
34
+ const debouncedSearchTerm = useDebounce(searchTerm, 300);
35
+ const { t } = useTranslation();
36
+ const filteredRows = tableRows.filter((row) =>
37
+ Object.values(row).some((value) => {
38
+ if (value === null || value === undefined) {
39
+ return false;
40
+ }
41
+ const searchableValue = typeof value === 'object' ? JSON.stringify(value) : String(value);
42
+ return searchableValue.toLowerCase().includes(debouncedSearchTerm.toLowerCase());
43
+ }),
44
+ );
45
+ const { results, currentPage, goTo } = usePagination(filteredRows ?? [], pageSize);
46
+ const { pageSizes } = usePaginationInfo(pageSize, filteredRows.length, currentPage, filteredRows.length);
47
+
48
+ const handleSearchChange = useCallback(
49
+ (event: React.ChangeEvent<HTMLInputElement> | '', value?: string) => {
50
+ const searchValue = typeof event === 'string' ? value ?? '' : event.target.value;
51
+ setSearchTerm(searchValue);
52
+ if (currentPage !== 1) {
53
+ goTo(1);
54
+ }
55
+ },
56
+ [currentPage, goTo],
57
+ );
58
+
59
+ const handleRequestReport = useCallback((report: Record<string, unknown>) => {
60
+ launchWorkspace2('report-request-workspace', { reportUuid: report.id });
61
+ }, []);
62
+
63
+ return (
64
+ <div className={styles.container}>
65
+ <DataTable rows={results} headers={tableHeaders} isSortable useZebraStyles>
66
+ {({
67
+ rows,
68
+ headers,
69
+ getHeaderProps,
70
+ getRowProps,
71
+ getBatchActionProps,
72
+ getToolbarProps,
73
+ getTableProps,
74
+ getTableContainerProps,
75
+ }) => {
76
+ const batchActionProps = getBatchActionProps();
77
+
78
+ return (
79
+ <TableContainer title={tableTitle} description={tableDescription} {...getTableContainerProps()}>
80
+ <TableToolbar {...getToolbarProps()}>
81
+ <TableToolbarContent aria-hidden={batchActionProps.shouldShowBatchActions}>
82
+ <TableToolbarSearch
83
+ persistent
84
+ placeholder={t('searchForReports', 'Search for reports by name, description, or category')}
85
+ onChange={handleSearchChange}
86
+ />
87
+ </TableToolbarContent>
88
+ </TableToolbar>
89
+
90
+ <Table {...getTableProps()} aria-label={t('reportsTable', 'Reports table')}>
91
+ <TableHead>
92
+ <TableRow>
93
+ {headers.map((header) => (
94
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
95
+ ))}
96
+ <TableHeader aria-label={t('rowActions', 'Row actions')} />
97
+ </TableRow>
98
+ </TableHead>
99
+
100
+ <TableBody>
101
+ {rows.map((row, index) => {
102
+ return (
103
+ <React.Fragment key={row.id}>
104
+ <TableRow {...getRowProps({ row })}>
105
+ {row.cells.map((cell) => {
106
+ if (cell.info.header === 'status') {
107
+ return (
108
+ <TableCell key={cell.id}>
109
+ <Tag size="sm">{cell.value}</Tag>
110
+ </TableCell>
111
+ );
112
+ }
113
+ return <TableCell key={cell.id}>{cell.value}</TableCell>;
114
+ })}
115
+ <TableCell className="cds--table-column-menu">
116
+ <OverflowMenu flipped aria-label="overflow-menu">
117
+ <OverflowMenuItem
118
+ onClick={() => handleRequestReport(results[index])}
119
+ itemText={t('requestReport', 'Request Report')}
120
+ />
121
+ <OverflowMenuItem
122
+ onClick={() =>
123
+ navigate({ to: `\${openmrsSpaBase}/reporting/report/${results[index].id}` })
124
+ }
125
+ itemText={t('viewHistory', 'View History')}
126
+ />
127
+ </OverflowMenu>
128
+ </TableCell>
129
+ </TableRow>
130
+ </React.Fragment>
131
+ );
132
+ })}
133
+ </TableBody>
134
+ </Table>
135
+ </TableContainer>
136
+ );
137
+ }}
138
+ </DataTable>
139
+ {results.length > 0 && (
140
+ <Pagination
141
+ itemsPerPageText={t('itemsPerPage', 'Items per page:')}
142
+ forwardText={t('nextPage', 'Next page')}
143
+ backwardText={t('previousPage', 'Previous page')}
144
+ itemRangeText={(min, max, total) =>
145
+ t('minMaxItems', '{{min}}-{{max}} of {{total}} items', { min, max, total })
146
+ }
147
+ pageRangeText={(_current, total) => t('pageRangeText', 'of {{count}} pages', { count: total })}
148
+ page={currentPage}
149
+ pageSize={pageSize}
150
+ pageSizes={pageSizes ?? [10, 20, 30]}
151
+ totalItems={tableRows?.length}
152
+ onChange={({ page, pageSize }) => {
153
+ goTo(page);
154
+ setPageSize(pageSize);
155
+ }}
156
+ />
157
+ )}
158
+ </div>
159
+ );
160
+ };
161
+
162
+ export default ReportTable;
@@ -0,0 +1,10 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+
4
+ .container {
5
+ border: 1px solid colors.$gray-20;
6
+
7
+ :global(.cds--table-toolbar) {
8
+ position: relative !important;
9
+ }
10
+ }