@openmrs/esm-reports-app 4.0.6-pre.161

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 (103) hide show
  1. package/.turbo/turbo-build.log +36 -0
  2. package/README.md +23 -0
  3. package/dist/126.js +1 -0
  4. package/dist/126.js.map +1 -0
  5. package/dist/144.js +2 -0
  6. package/dist/144.js.LICENSE.txt +19 -0
  7. package/dist/144.js.map +1 -0
  8. package/dist/156.js +1 -0
  9. package/dist/156.js.map +1 -0
  10. package/dist/202.js +2 -0
  11. package/dist/202.js.LICENSE.txt +35 -0
  12. package/dist/202.js.map +1 -0
  13. package/dist/216.js +1 -0
  14. package/dist/216.js.map +1 -0
  15. package/dist/297.js +2 -0
  16. package/dist/297.js.LICENSE.txt +32 -0
  17. package/dist/297.js.map +1 -0
  18. package/dist/300.js +1 -0
  19. package/dist/304.js +1 -0
  20. package/dist/304.js.map +1 -0
  21. package/dist/322.js +1 -0
  22. package/dist/322.js.map +1 -0
  23. package/dist/336.js +1 -0
  24. package/dist/336.js.map +1 -0
  25. package/dist/353.js +1 -0
  26. package/dist/353.js.map +1 -0
  27. package/dist/367.js +2 -0
  28. package/dist/367.js.LICENSE.txt +5 -0
  29. package/dist/367.js.map +1 -0
  30. package/dist/41.js +2 -0
  31. package/dist/41.js.LICENSE.txt +9 -0
  32. package/dist/41.js.map +1 -0
  33. package/dist/549.js +1 -0
  34. package/dist/549.js.map +1 -0
  35. package/dist/553.js +2 -0
  36. package/dist/553.js.LICENSE.txt +14 -0
  37. package/dist/553.js.map +1 -0
  38. package/dist/640.js +1 -0
  39. package/dist/640.js.map +1 -0
  40. package/dist/699.js +1 -0
  41. package/dist/699.js.map +1 -0
  42. package/dist/855.js +2 -0
  43. package/dist/855.js.LICENSE.txt +9 -0
  44. package/dist/855.js.map +1 -0
  45. package/dist/926.js +1 -0
  46. package/dist/926.js.map +1 -0
  47. package/dist/947.js +1 -0
  48. package/dist/947.js.map +1 -0
  49. package/dist/972.js +2 -0
  50. package/dist/972.js.LICENSE.txt +14 -0
  51. package/dist/972.js.map +1 -0
  52. package/dist/main.js +1 -0
  53. package/dist/main.js.map +1 -0
  54. package/dist/openmrs-esm-reports-app.js +1 -0
  55. package/dist/openmrs-esm-reports-app.js.buildmanifest.json +657 -0
  56. package/dist/openmrs-esm-reports-app.js.map +1 -0
  57. package/dist/routes.json +1 -0
  58. package/jest.config.js +3 -0
  59. package/package.json +57 -0
  60. package/src/components/edit-scheduled-report/edit-scheduled-report-form.component.tsx +149 -0
  61. package/src/components/edit-scheduled-report/edit-scheduled-report-form.scss +60 -0
  62. package/src/components/next-report-execution.component.tsx +31 -0
  63. package/src/components/overlay.component.tsx +53 -0
  64. package/src/components/overlay.scss +95 -0
  65. package/src/components/overview.component.tsx +356 -0
  66. package/src/components/pagination-constants.ts +3 -0
  67. package/src/components/report-overview-button.component.tsx +27 -0
  68. package/src/components/report-parameter-input.component.tsx +121 -0
  69. package/src/components/report-schedule-description.component.tsx +21 -0
  70. package/src/components/report-status.component.tsx +68 -0
  71. package/src/components/report-statuses-constants.ts +11 -0
  72. package/src/components/reports.resource.tsx +224 -0
  73. package/src/components/reports.scss +90 -0
  74. package/src/components/run-report/cancel-report-modal.component.tsx +129 -0
  75. package/src/components/run-report/run-report-form.component.tsx +241 -0
  76. package/src/components/run-report/run-report-form.scss +87 -0
  77. package/src/components/scheduled-overview-cell-content.component.tsx +85 -0
  78. package/src/components/scheduled-overview.component.tsx +118 -0
  79. package/src/components/scheduled-report-status.component.tsx +25 -0
  80. package/src/components/simple-cron-editor/commons.ts +47 -0
  81. package/src/components/simple-cron-editor/cron-date-picker.component.tsx +67 -0
  82. package/src/components/simple-cron-editor/cron-day-of-month-select.component.tsx +74 -0
  83. package/src/components/simple-cron-editor/cron-day-of-week-select.component.tsx +67 -0
  84. package/src/components/simple-cron-editor/cron-time-picker.component.tsx +60 -0
  85. package/src/components/simple-cron-editor/simple-cron-editor.component.tsx +342 -0
  86. package/src/components/simple-cron-editor/simple-cron-editor.scss +34 -0
  87. package/src/constants.ts +3 -0
  88. package/src/declarations.d.ts +6 -0
  89. package/src/hooks/useOverlay.tsx +45 -0
  90. package/src/index.ts +73 -0
  91. package/src/reports-link.tsx +22 -0
  92. package/src/reports.component.tsx +21 -0
  93. package/src/root.scss +15 -0
  94. package/src/routes.json +40 -0
  95. package/src/setup-tests.ts +1 -0
  96. package/src/types/report-definition.ts +12 -0
  97. package/src/types/report-design.ts +4 -0
  98. package/src/types/report-request.ts +8 -0
  99. package/src/utils/openmrs-cron-utils.ts +26 -0
  100. package/src/utils/time-utils.ts +43 -0
  101. package/translations/en.json +86 -0
  102. package/tsconfig.json +5 -0
  103. package/webpack.config.js +1 -0
@@ -0,0 +1,342 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { Select, SelectItem, TextInput } from '@carbon/react';
3
+ import { useTranslation } from 'react-i18next';
4
+ import type { Time } from '../../utils/time-utils';
5
+ import CronDatePicker from './cron-date-picker.component';
6
+ import CronTimePicker from './cron-time-picker.component';
7
+ import CronDayOfWeekSelect from './cron-day-of-week-select.component';
8
+ import type { CronField } from './commons';
9
+ import {
10
+ DAYS_OF_MONTH,
11
+ DAYS_OF_WEEK,
12
+ SCHEDULE_TYPE_DEFAULT_LABELS,
13
+ SCHEDULE_TYPES,
14
+ ST_ADVANCED,
15
+ ST_EVERY_DAY,
16
+ ST_EVERY_MONTH,
17
+ ST_EVERY_WEEK,
18
+ ST_ONCE,
19
+ } from './commons';
20
+ import CronDayOfMonthSelect from './cron-day-of-month-select.component';
21
+ import styles from './simple-cron-editor.scss';
22
+ import { parseOpenMRSCron } from '../../utils/openmrs-cron-utils';
23
+ import dayjs from 'dayjs';
24
+ import utc from 'dayjs/plugin/utc';
25
+
26
+ dayjs.extend(utc);
27
+
28
+ interface SimpleCronEditorProps {
29
+ initialCron: string;
30
+ onChange: (cron: string, isValid: boolean) => void;
31
+ }
32
+
33
+ interface EditorState {
34
+ scheduleType: string;
35
+ date: Date | null;
36
+ time: Time | null;
37
+ selectedDaysOfWeek: CronField[];
38
+ selectedDayOfMonth: CronField | null;
39
+ cron: string | null;
40
+ }
41
+
42
+ const EMPTY_EDITOR_STATE: EditorState = {
43
+ scheduleType: ST_ONCE,
44
+ date: null,
45
+ time: null,
46
+ selectedDaysOfWeek: [],
47
+ selectedDayOfMonth: null,
48
+ cron: null,
49
+ };
50
+
51
+ function getEditorState(initialCron: string): EditorState {
52
+ const scheduleType = detectSchedulingType(initialCron);
53
+ const openMRSCron = parseOpenMRSCron(initialCron);
54
+
55
+ if (scheduleType == ST_ADVANCED) {
56
+ return {
57
+ scheduleType,
58
+ date: undefined,
59
+ selectedDayOfMonth: undefined,
60
+ selectedDaysOfWeek: [],
61
+ time: undefined,
62
+ cron: initialCron,
63
+ };
64
+ } else if (scheduleType == ST_ONCE) {
65
+ return {
66
+ scheduleType,
67
+ date: dayjs()
68
+ .utc()
69
+ .set('year', parseInt(openMRSCron.year))
70
+ .set('month', parseInt(openMRSCron.month) - 1)
71
+ .set('date', parseInt(openMRSCron.day))
72
+ .toDate(),
73
+ selectedDayOfMonth: undefined,
74
+ selectedDaysOfWeek: [],
75
+ time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
76
+ cron: null,
77
+ };
78
+ } else if (scheduleType == ST_EVERY_DAY) {
79
+ return {
80
+ scheduleType,
81
+ date: undefined,
82
+ selectedDayOfMonth: undefined,
83
+ selectedDaysOfWeek: [],
84
+ time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
85
+ cron: null,
86
+ };
87
+ } else if (scheduleType == ST_EVERY_WEEK) {
88
+ return {
89
+ scheduleType,
90
+ date: undefined,
91
+ selectedDayOfMonth: undefined,
92
+ selectedDaysOfWeek: openMRSCron.dayOfWeek
93
+ .split(',')
94
+ .map((dayId) => parseInt(dayId))
95
+ .map((dayId) => DAYS_OF_WEEK[dayId]),
96
+ time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
97
+ cron: null,
98
+ };
99
+ } else if (scheduleType == ST_EVERY_MONTH) {
100
+ return {
101
+ scheduleType,
102
+ date: undefined,
103
+ selectedDayOfMonth: DAYS_OF_MONTH.find((dayOfMonth) => dayOfMonth.value === openMRSCron.day),
104
+ selectedDaysOfWeek: [],
105
+ time: { hours: parseInt(openMRSCron.hours), minutes: parseInt(openMRSCron.minutes) },
106
+ cron: null,
107
+ };
108
+ }
109
+
110
+ return EMPTY_EDITOR_STATE;
111
+ }
112
+
113
+ function detectSchedulingType(expression: string) {
114
+ if (!expression) {
115
+ return null;
116
+ }
117
+ const onceRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s\\d{1,2}\\s\\d{1,2}\\s[?]\\s\\d{4}');
118
+ if (onceRegexp.test(expression)) {
119
+ return ST_ONCE;
120
+ }
121
+ const everyDayRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s[*]\\s[*]\\s[?]');
122
+ if (everyDayRegexp.test(expression)) {
123
+ return ST_EVERY_DAY;
124
+ }
125
+ const everyWeekRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s[?]\\s[*]\\s[1-7*,]*');
126
+ if (everyWeekRegexp.test(expression)) {
127
+ return ST_EVERY_WEEK;
128
+ }
129
+ const everyMonthRegexp = new RegExp('^0\\s\\d{1,2}\\s\\d{1,2}\\s(1|L)\\s[*]\\s[?]');
130
+ if (everyMonthRegexp.test(expression)) {
131
+ return ST_EVERY_MONTH;
132
+ }
133
+ return ST_ADVANCED;
134
+ }
135
+
136
+ const SimpleCronEditor: React.FC<SimpleCronEditorProps> = ({ initialCron, onChange }) => {
137
+ const { t } = useTranslation();
138
+ const initialScheduleType = detectSchedulingType(initialCron);
139
+
140
+ const [editorState, setEditorState] = useState<EditorState>(getEditorState(initialCron));
141
+ const [invalid, setInvalid] = useState(false);
142
+ const [cron, setCron] = useState(initialCron);
143
+
144
+ useEffect(() => {
145
+ setEditorState(getEditorState(initialCron));
146
+ }, [initialCron]);
147
+
148
+ useEffect(() => {
149
+ buildCron();
150
+ }, [editorState]);
151
+
152
+ useEffect(() => {
153
+ onChange(cron, !invalid);
154
+ }, [cron]);
155
+
156
+ const buildCron = () => {
157
+ if (!validateEditor()) {
158
+ setCron(null);
159
+ return;
160
+ }
161
+
162
+ const selectedScheduleType = editorState.scheduleType;
163
+ const selectedTime = editorState.time;
164
+
165
+ if (selectedScheduleType == ST_ADVANCED) {
166
+ setCron(editorState.cron);
167
+ } else if (selectedScheduleType == ST_ONCE) {
168
+ setCron(
169
+ `0 ${selectedTime.minutes} ${selectedTime.hours} ${editorState.date.getUTCDate()} ${
170
+ editorState.date.getUTCMonth() + 1
171
+ } ? ${editorState.date.getUTCFullYear()}`,
172
+ );
173
+ } else if (selectedScheduleType == ST_EVERY_DAY) {
174
+ setCron(`0 ${selectedTime.minutes} ${selectedTime.hours} * * ?`);
175
+ } else if (selectedScheduleType == ST_EVERY_WEEK) {
176
+ setCron(
177
+ `0 ${selectedTime.minutes} ${selectedTime.hours} ? * ${editorState.selectedDaysOfWeek.map(
178
+ (dayOfWeek) => dayOfWeek.value,
179
+ )}`,
180
+ );
181
+ } else if (selectedScheduleType == ST_EVERY_MONTH) {
182
+ setCron(`0 ${selectedTime.minutes} ${selectedTime.hours} ${editorState.selectedDayOfMonth.value} * ?`);
183
+ }
184
+ };
185
+
186
+ const validateEditor = (): boolean => {
187
+ const selectedScheduleType = editorState.scheduleType;
188
+ const selectedTime = editorState.time;
189
+
190
+ if (selectedScheduleType == ST_ADVANCED) {
191
+ if (!editorState.cron) {
192
+ return validationFailed();
193
+ }
194
+ } else if (selectedScheduleType == ST_ONCE) {
195
+ if (!selectedTime || !(editorState.date instanceof Date)) {
196
+ return validationFailed();
197
+ }
198
+ } else if (selectedScheduleType == ST_EVERY_DAY) {
199
+ if (!selectedTime) {
200
+ return validationFailed();
201
+ }
202
+ } else if (selectedScheduleType == ST_EVERY_WEEK) {
203
+ if (!selectedTime || !editorState.selectedDaysOfWeek || editorState.selectedDaysOfWeek.length == 0) {
204
+ return validationFailed();
205
+ }
206
+ } else if (selectedScheduleType == ST_EVERY_MONTH) {
207
+ if (!selectedTime || !editorState.selectedDayOfMonth) {
208
+ return validationFailed();
209
+ }
210
+ }
211
+
212
+ return validationSuccess();
213
+ };
214
+
215
+ const validationFailed = (): boolean => {
216
+ setInvalid(true);
217
+ return false;
218
+ };
219
+
220
+ const validationSuccess = (): boolean => {
221
+ setInvalid(false);
222
+ return true;
223
+ };
224
+
225
+ const renderScheduleTypeSelect = () => {
226
+ return (
227
+ <div className={styles.cronEditorField}>
228
+ <Select
229
+ hideLabel={true}
230
+ onChange={(event) => {
231
+ setEditorState((state) => ({ ...state, scheduleType: event.target.value }));
232
+ }}
233
+ value={editorState.scheduleType}
234
+ >
235
+ {SCHEDULE_TYPES.filter(
236
+ (scheduleType) =>
237
+ (initialScheduleType != ST_ADVANCED && scheduleType != ST_ADVANCED) || initialScheduleType == ST_ADVANCED,
238
+ ).map((scheduleType) => (
239
+ <SelectItem
240
+ key={scheduleType}
241
+ text={t('scheduleType_' + scheduleType, SCHEDULE_TYPE_DEFAULT_LABELS[scheduleType] || scheduleType)}
242
+ value={scheduleType}
243
+ >
244
+ {scheduleType}
245
+ </SelectItem>
246
+ ))}
247
+ </Select>
248
+ </div>
249
+ );
250
+ };
251
+
252
+ const renderDatePicker = () => {
253
+ return (
254
+ <div className={styles.cronEditorField}>
255
+ <CronDatePicker
256
+ value={editorState.date}
257
+ onChange={(selectedDate) => {
258
+ setEditorState((state) => ({ ...state, date: selectedDate }));
259
+ }}
260
+ />
261
+ </div>
262
+ );
263
+ };
264
+
265
+ const renderDayOfWeekSelect = () => {
266
+ return (
267
+ <div className={styles.cronEditorField}>
268
+ <CronDayOfWeekSelect
269
+ value={editorState.selectedDaysOfWeek}
270
+ onChange={(selectedDaysOfWeek) => {
271
+ setEditorState((state) => ({ ...state, selectedDaysOfWeek }));
272
+ }}
273
+ />
274
+ </div>
275
+ );
276
+ };
277
+
278
+ const renderTimePicker = () => {
279
+ return (
280
+ <div className={styles.cronTimePickerField}>
281
+ <CronTimePicker
282
+ value={editorState.time}
283
+ onChange={(selectedTime) => {
284
+ setEditorState((state) => ({ ...state, time: selectedTime }));
285
+ }}
286
+ />
287
+ </div>
288
+ );
289
+ };
290
+
291
+ const renderDayOfMonthSelect = () => {
292
+ return (
293
+ <div className={styles.cronEditorField}>
294
+ <CronDayOfMonthSelect
295
+ value={editorState.selectedDayOfMonth}
296
+ onChange={(selectedDayOfMonth) => {
297
+ setEditorState((state) => ({ ...state, selectedDayOfMonth }));
298
+ }}
299
+ />
300
+ </div>
301
+ );
302
+ };
303
+
304
+ const renderCronInput = () => {
305
+ return (
306
+ <div className={styles.cronEditorField}>
307
+ <TextInput
308
+ type="text"
309
+ readOnly={true}
310
+ hideLabel={true}
311
+ value={editorState.cron}
312
+ onChange={(event) => {
313
+ setEditorState((state) => ({ ...state, cron: event.target.value }));
314
+ }}
315
+ />
316
+ </div>
317
+ );
318
+ };
319
+
320
+ return (
321
+ <div className={styles.cronContainer}>
322
+ {renderScheduleTypeSelect()}
323
+ {editorState.scheduleType != ST_ADVANCED && editorState.scheduleType != ST_EVERY_DAY && (
324
+ <div className={styles.cronEditorFieldSeparator}>
325
+ <span>{t('on', 'on')}</span>
326
+ </div>
327
+ )}
328
+ {editorState.scheduleType == ST_ONCE && renderDatePicker()}
329
+ {editorState.scheduleType == ST_EVERY_WEEK && renderDayOfWeekSelect()}
330
+ {editorState.scheduleType == ST_EVERY_MONTH && renderDayOfMonthSelect()}
331
+ {editorState.scheduleType != ST_ADVANCED && (
332
+ <div className={styles.cronEditorFieldSeparator}>
333
+ <span>{t('at', 'at')}</span>
334
+ </div>
335
+ )}
336
+ {editorState.scheduleType != ST_ADVANCED && renderTimePicker()}
337
+ {editorState.scheduleType == ST_ADVANCED && renderCronInput()}
338
+ </div>
339
+ );
340
+ };
341
+
342
+ export default SimpleCronEditor;
@@ -0,0 +1,34 @@
1
+ @use '@carbon/colors';
2
+ @use '@carbon/layout';
3
+ @use '@carbon/type';
4
+ @use '@openmrs/esm-styleguide/src/vars' as *;
5
+
6
+ .dangerLabel01 {
7
+ @include type.type-style('label-01');
8
+ color: $danger;
9
+ }
10
+
11
+ .cronEditor {
12
+ display: inline;
13
+ }
14
+
15
+ .cronEditorField {
16
+ float: left;
17
+ }
18
+
19
+ .cronTimePickerField {
20
+ float: left;
21
+ width: min-content;
22
+ }
23
+
24
+ .cronEditorFieldSeparator {
25
+ float: left;
26
+ height: layout.$spacing-08;
27
+ margin-top: 1.25rem;
28
+ margin-left: 1.25rem;
29
+ margin-right: 1.25rem;
30
+ }
31
+
32
+ .cronContainer {
33
+ padding: layout.$spacing-05;
34
+ }
@@ -0,0 +1,3 @@
1
+ export const basePath = '/reports';
2
+ export const spaBasePath = `${window.spaBase}${basePath}`;
3
+ export const PRIVILEGE_SYSTEM_DEVELOPER = 'System Developer';
@@ -0,0 +1,6 @@
1
+ declare module '@carbon/react';
2
+ declare module '*.css';
3
+ declare module '*.scss';
4
+ declare module '*.png';
5
+
6
+ declare type SideNavProps = object;
@@ -0,0 +1,45 @@
1
+ import { getGlobalStore } from '@openmrs/esm-framework';
2
+ import { useCallback, useEffect, useState } from 'react';
3
+
4
+ export interface OverlayStore {
5
+ isOverlayOpen: boolean;
6
+ component?: any;
7
+ header: string;
8
+ }
9
+
10
+ const initialState: OverlayStore = { isOverlayOpen: false, header: '' };
11
+
12
+ const getOverlayStore = () => {
13
+ return getGlobalStore('reports-store', initialState);
14
+ };
15
+
16
+ export const launchOverlay = (headerTitle: string, componentToRender: any) => {
17
+ const store = getOverlayStore();
18
+ store.setState({ isOverlayOpen: true, component: componentToRender, header: headerTitle });
19
+ };
20
+
21
+ export const closeOverlay = (): void => {
22
+ const store = getOverlayStore();
23
+ store.setState({ component: null, isOverlayOpen: false });
24
+ };
25
+
26
+ export const useOverlay = () => {
27
+ const [overlay, setOverlay] = useState<OverlayStore>();
28
+
29
+ const update = useCallback((state: OverlayStore) => {
30
+ setOverlay(state);
31
+ }, []);
32
+
33
+ useEffect(() => {
34
+ update(getOverlayStore().getState());
35
+ getOverlayStore().subscribe(update);
36
+ }, [update]);
37
+
38
+ const { isOverlayOpen, component, header } = overlay ?? {};
39
+
40
+ return {
41
+ isOverlayOpen,
42
+ component,
43
+ header,
44
+ };
45
+ };
package/src/index.ts ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * This is the entrypoint file of the application. It communicates the
3
+ * important features of this microfrontend to the app shell. It
4
+ * connects the app shell to the React application(s) that make up this
5
+ * microfrontend.
6
+ */
7
+ import { getAsyncLifecycle, registerBreadcrumbs } from '@openmrs/esm-framework';
8
+
9
+ const moduleName = '@openmrs/esm-reports-app';
10
+
11
+ const options = {
12
+ featureName: 'reports',
13
+ moduleName,
14
+ };
15
+
16
+ /**
17
+ * This tells the app shell how to obtain translation files: that they
18
+ * are JSON files in the directory `../translations` (which you should
19
+ * see in the directory structure).
20
+ */
21
+ export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
22
+
23
+ /**
24
+ * This function performs any setup that should happen at microfrontend
25
+ * load-time (such as defining the config schema) and then returns an
26
+ * object which describes how the React application(s) should be
27
+ * rendered.
28
+ */
29
+ export function startupApp() {
30
+ registerBreadcrumbs([
31
+ {
32
+ title: 'Home',
33
+ path: `${window.openmrsBase}`,
34
+ },
35
+ {
36
+ path: `${window.spaBase}/system-administration`,
37
+ title: () => Promise.resolve(window.i18next.t('systemAdmin', 'System Administration')),
38
+ parent: `${window.spaBase}/home`,
39
+ },
40
+ {
41
+ title: () => Promise.resolve(window.i18next.t('reports', 'Reports')),
42
+ path: `${window.spaBase}/reports`,
43
+ parent: `${window.spaBase}/system-administration`,
44
+ },
45
+ {
46
+ title: () => Promise.resolve(window.i18next.t('scheduledReports', 'Scheduled Reports')),
47
+ path: `${window.spaBase}/reports/scheduled-overview`,
48
+ parent: `${window.spaBase}/reports`,
49
+ },
50
+ ]);
51
+ }
52
+
53
+ /**
54
+ * This named export tells the app shell that the default export of `root.component.tsx`
55
+ * should be rendered when the route matches `root`. The full route
56
+ * will be `openmrsSpaBase() + 'root'`, which is usually
57
+ * `/openmrs/spa/root`.
58
+ */
59
+
60
+ export const root = getAsyncLifecycle(() => import('./reports.component'), options);
61
+
62
+ export const reportsLink = getAsyncLifecycle(() => import('./reports-link'), options);
63
+
64
+ export const overview = getAsyncLifecycle(() => import('./components/overview.component'), options);
65
+
66
+ export const scheduledOverview = getAsyncLifecycle(() => import('./components/scheduled-overview.component'), options);
67
+
68
+ export const runReport = getAsyncLifecycle(() => import('./components/run-report/run-report-form.component'), options);
69
+
70
+ export const cancelReportModal = getAsyncLifecycle(
71
+ () => import('./components/run-report/cancel-report-modal.component'),
72
+ options,
73
+ );
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { useTranslation } from 'react-i18next';
3
+ import { spaBasePath } from './constants';
4
+ import { ClickableTile, Layer } from '@carbon/react';
5
+ import { ArrowRight } from '@carbon/react/icons';
6
+
7
+ export default function ReportsLink() {
8
+ const { t } = useTranslation();
9
+ return (
10
+ <Layer>
11
+ <ClickableTile href={`${spaBasePath}`}>
12
+ <div>
13
+ <div className="heading">{t('manageReports', 'Manage Reports')}</div>
14
+ <div className="content">{t('reports', 'Reports')}</div>
15
+ </div>
16
+ <div className="iconWrapper">
17
+ <ArrowRight size={16} />
18
+ </div>
19
+ </ClickableTile>
20
+ </Layer>
21
+ );
22
+ }
@@ -0,0 +1,21 @@
1
+ import React from 'react';
2
+ import { BrowserRouter, Route, Routes } from 'react-router-dom';
3
+ import styles from './root.scss';
4
+ import OverviewComponent from './components/overview.component';
5
+ import ScheduledOverviewComponent from './components/scheduled-overview.component';
6
+ import { spaBasePath } from './constants';
7
+
8
+ const RootComponent: React.FC = () => {
9
+ return (
10
+ <div className={styles.container}>
11
+ <BrowserRouter basename={spaBasePath}>
12
+ <Routes>
13
+ <Route path="/" element={<OverviewComponent />} />
14
+ <Route path="/scheduled-overview" element={<ScheduledOverviewComponent />} />
15
+ </Routes>
16
+ </BrowserRouter>
17
+ </div>
18
+ );
19
+ };
20
+
21
+ export default RootComponent;
package/src/root.scss ADDED
@@ -0,0 +1,15 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+
4
+ .container {
5
+ padding: layout.$spacing-07;
6
+ }
7
+
8
+ .welcome {
9
+ @include type.type-style('heading-04');
10
+ margin: layout.$spacing-05 0;
11
+ }
12
+
13
+ .explainer {
14
+ margin-bottom: 2rem;
15
+ }
@@ -0,0 +1,40 @@
1
+ {
2
+ "$schema": "https://json.openmrs.org/routes.schema.json",
3
+ "backendDependencies": {
4
+ "fhir2": ">=1.2",
5
+ "webservices.rest": "^2.24.0",
6
+ "reportingrest": ">=1.14.0",
7
+ "reporting": ">=1.26.0"
8
+ },
9
+ "extensions": [
10
+ {
11
+ "name": "admin-report-link",
12
+ "slot": "system-admin-page-card-link-slot",
13
+ "component": "reportsLink"
14
+ },
15
+ {
16
+ "name": "run-report",
17
+ "component": "runReport"
18
+ }
19
+ ],
20
+ "pages": [
21
+ {
22
+ "component": "root",
23
+ "route": "reports"
24
+ },
25
+ {
26
+ "component": "overview",
27
+ "route": "overview"
28
+ },
29
+ {
30
+ "component": "scheduled-overview",
31
+ "route": "scheduled-overview"
32
+ }
33
+ ],
34
+ "modals": [
35
+ {
36
+ "name": "cancel-report-modal",
37
+ "component": "cancelReportModal"
38
+ }
39
+ ]
40
+ }
@@ -0,0 +1 @@
1
+ import '@testing-library/jest-dom/extend-expect';
@@ -0,0 +1,12 @@
1
+ export interface ReportDefinition {
2
+ uuid: string;
3
+ name: string;
4
+ description: string;
5
+ parameters: ReportParameter[];
6
+ }
7
+
8
+ interface ReportParameter {
9
+ name: string;
10
+ label: string;
11
+ type: string;
12
+ }
@@ -0,0 +1,4 @@
1
+ export interface ReportDesign {
2
+ name: string;
3
+ uuid: string;
4
+ }
@@ -0,0 +1,8 @@
1
+ export interface ReportRequest {
2
+ uuid: string;
3
+ schedule: string;
4
+ renderingMode: {
5
+ argument: string;
6
+ };
7
+ parameterMappings: Record<string, string>;
8
+ }
@@ -0,0 +1,26 @@
1
+ export interface OpenMRSCron {
2
+ seconds: string;
3
+ minutes: string;
4
+ hours: string;
5
+ day: string;
6
+ month: string;
7
+ dayOfWeek: string;
8
+ year: string;
9
+ }
10
+
11
+ export function parseOpenMRSCron(expression: string): OpenMRSCron {
12
+ if (!expression) {
13
+ return null;
14
+ }
15
+
16
+ const tokens = expression.split(' ');
17
+ return {
18
+ seconds: tokens[0],
19
+ minutes: tokens[1],
20
+ hours: tokens[2],
21
+ day: tokens[3],
22
+ month: tokens[4],
23
+ dayOfWeek: tokens[5],
24
+ year: tokens[6],
25
+ };
26
+ }