@ampath/esm-reports-app 1.0.0-next.41 → 1.0.0-next.45

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 (183) hide show
  1. package/dist/143.css +1 -0
  2. package/dist/143.css.map +1 -0
  3. package/dist/143.js +1 -0
  4. package/dist/143.js.map +1 -0
  5. package/dist/197.js +1 -1
  6. package/dist/282.js +1 -1
  7. package/dist/282.js.map +1 -1
  8. package/dist/295.css +1 -0
  9. package/dist/295.css.map +1 -0
  10. package/dist/295.js +1 -0
  11. package/dist/295.js.map +1 -0
  12. package/dist/300.js +1 -1
  13. package/dist/311.js +1 -1
  14. package/dist/311.js.map +1 -1
  15. package/dist/335.js +1 -1
  16. package/dist/353.js +1 -1
  17. package/dist/353.js.map +1 -1
  18. package/dist/410.js +1 -0
  19. package/dist/410.js.map +1 -0
  20. package/dist/427.js +2 -0
  21. package/dist/427.js.LICENSE.txt +35 -0
  22. package/dist/427.js.map +1 -0
  23. package/dist/441.js +1 -0
  24. package/dist/441.js.map +1 -0
  25. package/dist/478.js +1 -1
  26. package/dist/478.js.map +1 -1
  27. package/dist/540.js +1 -1
  28. package/dist/540.js.map +1 -1
  29. package/dist/55.js +1 -1
  30. package/dist/652.js +1 -1
  31. package/dist/961.js +1 -1
  32. package/dist/961.js.map +1 -1
  33. package/dist/99.js +1 -1
  34. package/dist/ampath-esm-reports-app.js +1 -1
  35. package/dist/ampath-esm-reports-app.js.map +1 -1
  36. package/dist/routes.json +1 -1
  37. package/package.json +2 -2
  38. package/src/common/report-error/index.ts +1 -0
  39. package/src/common/report-error/show-report-error.ts +25 -0
  40. package/src/common/report-filters/date-range.ts +137 -0
  41. package/src/common/report-filters/index.ts +4 -0
  42. package/src/common/report-filters/report-filters.component.tsx +281 -151
  43. package/src/common/report-filters/report-filters.scss +180 -55
  44. package/src/common/report-filters/types.ts +66 -0
  45. package/src/common/report-filters/use-restore-period.ts +43 -0
  46. package/src/common/report-page/index.ts +2 -0
  47. package/src/common/report-page/report-page.component.tsx +32 -0
  48. package/src/common/report-page/report-page.scss +45 -0
  49. package/src/common/report-page/use-page-scale.ts +50 -0
  50. package/src/common/report-placeholder/has-report-data.ts +15 -0
  51. package/src/common/report-placeholder/index.ts +2 -0
  52. package/src/common/report-placeholder/report-placeholder.component.tsx +56 -0
  53. package/src/common/report-placeholder/report-placeholder.scss +46 -0
  54. package/src/common/report-register/index.ts +5 -0
  55. package/src/common/report-register/register-columns.ts +34 -0
  56. package/src/common/report-register/register-layout.component.tsx +237 -0
  57. package/src/common/report-register/register-layout.scss +140 -0
  58. package/src/common/report-register/register-table.scss +62 -0
  59. package/src/common/report-register/report-parents.ts +27 -0
  60. package/src/common/report-register/rows-to-csv.ts +59 -0
  61. package/src/common/report-register/use-patient-list.ts +92 -0
  62. package/src/common/report-skeleton/index.ts +1 -0
  63. package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
  64. package/src/common/report-skeleton/report-skeleton.scss +29 -0
  65. package/src/config-schema.ts +9 -1
  66. package/src/dashboard/report-catalog.ts +173 -0
  67. package/src/dashboard/reports-dasboard.tsx +101 -35
  68. package/src/dashboard/reports-dashboard.module.scss +269 -11
  69. package/src/index.ts +13 -1
  70. package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +50 -48
  71. package/src/reports/moh-240/moh-240-header.component.tsx +54 -56
  72. package/src/reports/moh-240/moh-240.columns.ts +64 -0
  73. package/src/reports/moh-240/moh-240.component.tsx +65 -84
  74. package/src/reports/moh-240/moh-240.scss +36 -7
  75. package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +27 -65
  76. package/src/reports/moh-240/sub-reports/page-summary.component.tsx +2 -2
  77. package/src/reports/moh-505/moh-505-header.component.tsx +59 -57
  78. package/src/reports/moh-505/moh-505.component.tsx +51 -56
  79. package/src/reports/moh-505/moh-505.scss +116 -2
  80. package/src/reports/moh-705B/moh-204b-register.component.tsx +41 -42
  81. package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
  82. package/src/reports/moh-705B/moh-705b.component.tsx +492 -473
  83. package/src/reports/moh-705B/moh-705b.scss +110 -24
  84. package/src/reports/moh-705a/moh-705a.component.tsx +496 -476
  85. package/src/reports/moh-705a/moh-705a.scss +109 -24
  86. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +41 -42
  87. package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
  88. package/src/reports/moh-706/moh-706-header.component.tsx +58 -56
  89. package/src/reports/moh-706/moh-706.component.tsx +84 -55
  90. package/src/reports/moh-706/moh-706.scss +115 -19
  91. package/src/reports/moh-710/moh-710.component.tsx +743 -727
  92. package/src/reports/moh-710/moh-710.scss +140 -23
  93. package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +48 -43
  94. package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
  95. package/src/reports/moh-711/moh-711.component.tsx +94 -106
  96. package/src/reports/moh-711/moh711.scss +95 -64
  97. package/src/reports/moh-711/registers/moh-333-register.component.tsx +92 -94
  98. package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
  99. package/src/reports/moh-711/registers/moh-405-register.component.tsx +88 -89
  100. package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
  101. package/src/reports/moh-711/registers/moh-406-register.component.tsx +85 -88
  102. package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
  103. package/src/reports/moh-711/registers/moh-510-register.component.tsx +38 -44
  104. package/src/reports/moh-711/registers/moh-511-register.component.tsx +42 -43
  105. package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
  106. package/src/reports/moh-711/sections/anc.component.tsx +3 -3
  107. package/src/reports/moh-711/sections/cervical-cancer.component.tsx +3 -3
  108. package/src/reports/moh-711/sections/chanis.component.tsx +2 -3
  109. package/src/reports/moh-711/sections/family-planning.component.tsx +2 -2
  110. package/src/reports/moh-711/sections/gbv.component.tsx +2 -2
  111. package/src/reports/moh-711/sections/maternity.component.tsx +3 -8
  112. package/src/reports/moh-711/sections/medical-social-work.component.tsx +2 -12
  113. package/src/reports/moh-711/sections/other.component.tsx +2 -2
  114. package/src/reports/moh-711/sections/physiotherapy.component.tsx +2 -2
  115. package/src/reports/moh-711/sections/pnc.component.tsx +7 -5
  116. package/src/reports/moh-711/sections/post-abortion.component.tsx +2 -2
  117. package/src/reports/moh-711/sections/rehabilitation.component.tsx +2 -2
  118. package/src/reports/moh-711/sections/report-compiled-by.component.tsx +12 -4
  119. package/src/reports/moh-711/sections/tb-screening.component.tsx +2 -2
  120. package/src/reports/moh-717/moh-717.component.tsx +100 -121
  121. package/src/reports/moh-717/moh717.scss +163 -102
  122. package/src/reports/moh-717/sections/maternity.component.tsx +1 -1
  123. package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +1 -1
  124. package/src/reports/moh-717/sections/outpatient.component.tsx +23 -23
  125. package/src/reports/moh-717/sections/preparedby.component.tsx +7 -2
  126. package/src/reports/moh-717/sections/special-services.component.tsx +1 -1
  127. package/src/reports/moh-731/moh-731.component.tsx +127 -110
  128. package/src/reports/moh-731/moh-731.scss +396 -68
  129. package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
  130. package/src/reports/moh-731/registers/moh-366.component.tsx +76 -83
  131. package/src/reports/moh-731/registers/prep-register.component.tsx +70 -78
  132. package/src/reports/moh-731/registers/prep.columns.ts +34 -0
  133. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1031 -751
  134. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +230 -86
  135. package/src/reports/moh-731/sections/pmtct.component.tsx +185 -64
  136. package/src/reports/moh-740/moh-740.component.scss +159 -79
  137. package/src/reports/moh-740/moh-740.component.tsx +261 -230
  138. package/src/reports/moh-740/moh-740.resource.ts +17 -10
  139. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +77 -96
  140. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +50 -70
  141. package/src/reports/moh-740/registers/moh-740-patient-list.tsx +61 -32
  142. package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
  143. package/src/reports/moh-740/shared/utils/indicator-register-map.ts +3 -3
  144. package/src/reports/moh-740/types/index.ts +3 -3
  145. package/src/reports/moh-745/moh-745.component.tsx +897 -877
  146. package/src/reports/moh-745/moh-745.scss +104 -46
  147. package/src/reports/moh-745/registers/moh-412-register.component.tsx +42 -43
  148. package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
  149. package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
  150. package/src/reports/table-wrapper/table-wrapper.scss +1 -16
  151. package/src/resources/moh-505.resource.ts +18 -35
  152. package/src/resources/moh-705.resource.ts +28 -39
  153. package/src/resources/moh-706.resource.ts +17 -30
  154. package/src/resources/moh-710.resource.ts +7 -17
  155. package/src/resources/moh-711.resource.ts +46 -64
  156. package/src/resources/moh-717.resource.ts +7 -17
  157. package/src/resources/moh-731.resource.ts +26 -32
  158. package/src/resources/moh-745.resource.ts +18 -20
  159. package/src/resources/patient-list-page.ts +66 -0
  160. package/src/root.component.tsx +28 -26
  161. package/src/root.scss +36 -0
  162. package/src/utils/get-base-url.ts +39 -7
  163. package/tsconfig.json +1 -1
  164. package/dist/110.js +0 -1
  165. package/dist/110.js.map +0 -1
  166. package/dist/184.js +0 -2
  167. package/dist/184.js.LICENSE.txt +0 -14
  168. package/dist/184.js.map +0 -1
  169. package/dist/205.js +0 -1
  170. package/dist/205.js.map +0 -1
  171. package/dist/812.js +0 -1
  172. package/dist/812.js.map +0 -1
  173. package/dist/822.js +0 -2
  174. package/dist/822.js.LICENSE.txt +0 -20
  175. package/dist/822.js.map +0 -1
  176. package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -560
  177. package/dist/main.js +0 -1
  178. package/dist/main.js.map +0 -1
  179. package/src/reports/moh-731/registers/moh-366.scss +0 -45
  180. package/src/reports/moh-731/registers/prep-registers.scss +0 -45
  181. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.scss +0 -18
  182. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.scss +0 -18
  183. /package/src/{reports/moh-740/shared/utils → utils}/format-indicator.ts +0 -0
@@ -1,185 +1,315 @@
1
- import { useNavigate } from 'react-router-dom';
2
1
  import React from 'react';
3
- import { Button } from '@carbon/react';
2
+ import { Link, useSearchParams } from 'react-router-dom';
3
+ import {
4
+ Breadcrumb,
5
+ BreadcrumbItem,
6
+ Button,
7
+ ContentSwitcher,
8
+ DatePicker,
9
+ DatePickerInput,
10
+ Layer,
11
+ Select,
12
+ SelectItem,
13
+ Switch,
14
+ } from '@carbon/react';
15
+ import { Calendar, ChartColumn, Download, WarningAlt } from '@carbon/react/icons';
16
+ import classNames from 'classnames';
17
+ import dayjs from 'dayjs';
4
18
 
19
+ import {
20
+ DATE_FORMAT,
21
+ describePeriod,
22
+ firstError,
23
+ formatDate,
24
+ formatMonth,
25
+ monthToDateRange,
26
+ validatePeriod,
27
+ } from './date-range';
28
+ import { type ReportFiltersProps } from './types';
5
29
  import styles from './report-filters.scss';
6
30
 
7
- interface ReportFiltersComponentProps {
8
- reportName: string;
9
- mode?: 'monthly' | 'daily';
10
- isFacility?: false | 'true';
11
- onGenerate?: (filters: { startDate?: string; endDate?: string; month?: string }) => void;
12
- isLoding?: boolean;
13
- }
31
+ const months = Array.from({ length: 12 }, (_, index) => ({
32
+ value: String(index + 1).padStart(2, '0'),
33
+ label: dayjs().month(index).format('MMMM'),
34
+ }));
14
35
 
15
- const ReportFiltersComponent: React.FC<ReportFiltersComponentProps> = ({
36
+ const ReportFiltersComponent: React.FC<ReportFiltersProps> = ({
16
37
  reportName,
17
- mode = 'daily',
18
- isFacility,
38
+ mode = 'both',
39
+ facilities,
40
+ yearsBack = 6,
41
+ isLoading = false,
42
+ isReportGenerated = false,
43
+ generatedPeriod,
19
44
  onGenerate,
20
- isLoding = false,
45
+ onPeriodChange,
21
46
  }) => {
22
- const navigate = useNavigate();
47
+ /* A register's breadcrumb links back here carrying the period the report was
48
+ generated for. Seeding the controls from it matters as much as restoring
49
+ the figures: left on their defaults the bar would compare last month
50
+ against the restored period and warn that figures are stale on arrival. */
51
+ const [params] = useSearchParams();
52
+ const urlStart = params.get('startDate');
53
+ const urlEnd = params.get('endDate');
54
+ const urlMonth = params.get('month');
23
55
 
24
- const getToday = () => {
25
- return new Date().toISOString().split('T')[0];
26
- };
56
+ /* A period covering exactly one calendar month came from the Monthly tab,
57
+ however it is spelled in the URL. */
58
+ const urlWholeMonth =
59
+ urlMonth ??
60
+ (urlStart &&
61
+ urlEnd &&
62
+ dayjs(urlStart).isSame(dayjs(urlStart).startOf('month'), 'day') &&
63
+ dayjs(urlEnd).isSame(dayjs(urlStart).endOf('month'), 'day')
64
+ ? dayjs(urlStart).format('YYYY-MM')
65
+ : null);
27
66
 
28
- const getPreviousMonth = () => {
29
- const date = new Date();
30
- date.setMonth(date.getMonth() - 1);
67
+ const [activeTab, setActiveTab] = React.useState<'monthly' | 'custom'>(
68
+ urlStart && urlEnd && !urlWholeMonth ? 'custom' : 'monthly',
69
+ );
31
70
 
32
- const year = date.getFullYear();
33
- const month = String(date.getMonth() + 1).padStart(2, '0');
71
+ const [startDateString, setStartDateString] = React.useState<string>(() => urlStart ?? formatDate(new Date()));
72
+ const [endDateString, setEndDateString] = React.useState<string>(() => urlEnd ?? formatDate(new Date()));
73
+ const [monthString, setMonthString] = React.useState<string>(
74
+ () => urlWholeMonth ?? formatMonth(dayjs().subtract(1, 'month')),
75
+ );
34
76
 
35
- return `${year}-${month}`;
36
- };
77
+ const [selectedFacility, setSelectedFacility] = React.useState<string>('');
37
78
 
38
- const [activeTab, setActiveTab] = React.useState<'monthly' | 'custom'>('monthly');
79
+ const [selectedYear, selectedMonth] = monthString.split('-');
39
80
 
40
- const [startDateString, setStartDateString] = React.useState<string>(getToday());
41
- const [endDateString, setEndDateString] = React.useState<string>(getToday());
42
- const [monthString, setMonthString] = React.useState<string>(getPreviousMonth());
81
+ /** Recomputed per render so a session open across New Year still offers this year */
82
+ const years = React.useMemo(
83
+ () => Array.from({ length: yearsBack }, (_, index) => String(dayjs().year() - index)),
84
+ [yearsBack],
85
+ );
43
86
 
44
- const [selectedFacility, setSelectedFacility] = React.useState<string>('');
45
- const [facility, setFacility] = React.useState<string>('Select Facility');
46
-
47
- const generateReport = () => {
48
- if (onGenerate) {
49
- const filters: { startDate?: string; endDate?: string; month?: string } = {};
50
-
51
- if (mode === 'daily') {
52
- filters.startDate = startDateString;
53
- filters.endDate = endDateString;
54
- }
55
-
56
- if (mode === 'monthly') {
57
- if (activeTab === 'monthly') {
58
- filters.month = monthString;
59
- } else {
60
- filters.startDate = startDateString;
61
- filters.endDate = endDateString;
87
+ const showsSwitcher = mode === 'both';
88
+ const usesDateRange = mode === 'range' || (showsSwitcher && activeTab === 'custom');
89
+ const usesMonth = mode === 'monthly' || (showsSwitcher && activeTab === 'monthly');
90
+
91
+ const dateRange = React.useMemo(
92
+ () => [dayjs(startDateString).toDate(), dayjs(endDateString).toDate()],
93
+ [startDateString, endDateString],
94
+ );
95
+
96
+ const period = React.useMemo(() => {
97
+ const month = usesDateRange ? undefined : monthString;
98
+ const { startDate, endDate } = month
99
+ ? monthToDateRange(month)
100
+ : { startDate: startDateString, endDate: endDateString };
101
+
102
+ return {
103
+ startDate,
104
+ endDate,
105
+ month,
106
+ facility: facilities ? selectedFacility : undefined,
107
+ label: describePeriod(startDate, endDate, month),
108
+ };
109
+ }, [usesDateRange, monthString, startDateString, endDateString, facilities, selectedFacility]);
110
+
111
+ /* What the report currently on screen was generated for, so a change to the
112
+ filters can be called out rather than silently leaving stale figures up. */
113
+ const [generatedFor, setGeneratedFor] = React.useState<{ key: string; label: string } | null>(null);
114
+
115
+ const errors = React.useMemo(
116
+ () =>
117
+ validatePeriod({
118
+ usesDateRange,
119
+ year: selectedYear,
120
+ month: selectedMonth,
121
+ startDate: startDateString,
122
+ endDate: endDateString,
123
+ }),
124
+ [usesDateRange, selectedYear, selectedMonth, startDateString, endDateString],
125
+ );
126
+ const isValid = Object.keys(errors).length === 0;
127
+
128
+ /* Reported on change so a report can label its form with the chosen period
129
+ before any figures exist for it. */
130
+ React.useEffect(() => {
131
+ onPeriodChange?.(period);
132
+ // eslint-disable-next-line react-hooks/exhaustive-deps
133
+ }, [period]);
134
+
135
+ const periodKey = `${period.startDate}|${period.endDate}|${period.facility ?? ''}`;
136
+
137
+ /* Falls back to the period the caller says is on screen, which is what makes
138
+ the warning work after a reload rather than only after a generate. */
139
+ const shownPeriod =
140
+ generatedFor ??
141
+ (generatedPeriod?.startDate && generatedPeriod?.endDate
142
+ ? {
143
+ key: `${generatedPeriod.startDate}|${generatedPeriod.endDate}|`,
144
+ label: describePeriod(generatedPeriod.startDate, generatedPeriod.endDate),
62
145
  }
63
- }
146
+ : null);
147
+
148
+ const isStale = isReportGenerated && shownPeriod !== null && shownPeriod.key !== periodKey;
149
+
150
+ const handleGenerate = () => {
151
+ if (!isValid) {
152
+ return;
153
+ }
64
154
 
65
- onGenerate(filters);
155
+ setGeneratedFor({ key: periodKey, label: period.label });
156
+ onGenerate(period);
157
+ };
158
+
159
+ /** Only commit a complete range, so re-rendering does not interrupt the second pick */
160
+ const handleDateRangeChange = (dates: Array<Date>) => {
161
+ const [start, end] = dates;
162
+
163
+ if (start && end) {
164
+ setStartDateString(dayjs(start).format(DATE_FORMAT));
165
+ setEndDateString(dayjs(end).format(DATE_FORMAT));
66
166
  }
67
167
  };
68
168
 
169
+ const dateRangePicker = (
170
+ <DatePicker
171
+ datePickerType="range"
172
+ dateFormat="d/m/Y"
173
+ maxDate={new Date()}
174
+ value={dateRange}
175
+ onChange={handleDateRangeChange}
176
+ className={styles.datePicker}
177
+ >
178
+ <DatePickerInput
179
+ id="report-start-date"
180
+ labelText="Start date"
181
+ placeholder="dd/mm/yyyy"
182
+ size="sm"
183
+ invalid={Boolean(errors.startDate)}
184
+ invalidText={errors.startDate}
185
+ />
186
+ <DatePickerInput
187
+ id="report-end-date"
188
+ labelText="End date"
189
+ placeholder="dd/mm/yyyy"
190
+ size="sm"
191
+ invalid={Boolean(errors.endDate)}
192
+ invalidText={errors.endDate}
193
+ />
194
+ </DatePicker>
195
+ );
196
+
69
197
  return (
70
- <>
71
- <div className={styles.titleContainer}>
72
- <Button className={styles.backIcon} onClick={() => navigate('/')}>
73
- Back
74
- </Button>
75
- <h2 className={styles.title}>{reportName}</h2>
76
- </div>
77
- <div className={styles.filtersContainer}>
78
- {mode === 'daily' && (
79
- <div className={styles.dateContainer}>
80
- <label htmlFor="startingMonth">Start Date:</label>
81
- <label htmlFor="endingMonth">End Date:</label>
82
-
83
- <input
84
- id="startingMonth"
85
- type="date"
86
- className={styles.input}
87
- value={startDateString}
88
- onChange={(e) => setStartDateString(e.target.value)}
89
- />
90
-
91
- <input
92
- id="endingMonth"
93
- type="date"
94
- className={styles.input}
95
- value={endDateString}
96
- onChange={(e) => setEndDateString(e.target.value)}
97
- />
98
- </div>
99
- )}
100
-
101
- {mode === 'monthly' && (
102
- <div>
103
- {/* Tabs */}
104
- <div className={styles.tabsContainer}>
105
- <Button
106
- className={activeTab === 'monthly' ? styles.activeTab : styles.tab}
107
- onClick={() => setActiveTab('monthly')}
108
- >
109
- Monthly
110
- </Button>
198
+ <div className={styles.filters}>
199
+ <Breadcrumb noTrailingSlash className={styles.breadcrumb}>
200
+ <BreadcrumbItem>
201
+ <Link to="/">All reports</Link>
202
+ </BreadcrumbItem>
203
+ {/* The report name doubles as the page heading, so it stays an `h3`
204
+ rather than being repeated in a separate title row. */}
205
+ <BreadcrumbItem isCurrentPage>
206
+ <h3 className={styles.title}>{reportName}</h3>
207
+ </BreadcrumbItem>
208
+ </Breadcrumb>
111
209
 
112
- <Button
113
- className={activeTab === 'custom' ? styles.activeTab : styles.tab}
114
- onClick={() => setActiveTab('custom')}
210
+ <Layer className={styles.filtersContainer}>
211
+ <div className={styles.fields}>
212
+ {showsSwitcher && (
213
+ <div className={classNames(styles.field, styles.periodField)}>
214
+ <span className={styles.fieldLabel}>Period type</span>
215
+ <ContentSwitcher
216
+ selectedIndex={activeTab === 'monthly' ? 0 : 1}
217
+ onChange={({ index }) => setActiveTab(index === 0 ? 'monthly' : 'custom')}
218
+ size="sm"
219
+ className={styles.switcher}
115
220
  >
116
- Custom
117
- </Button>
221
+ <Switch name="monthly" text="Monthly" />
222
+ <Switch name="custom" text="Custom range" />
223
+ </ContentSwitcher>
118
224
  </div>
225
+ )}
119
226
 
120
- {/* Monthly tab */}
121
- {activeTab === 'monthly' && (
122
- <div>
123
- <label htmlFor="month">Month:</label>
124
-
125
- <input
126
- id="month"
127
- type="month"
128
- className={styles.input}
129
- value={monthString}
130
- onChange={(e) => setMonthString(e.target.value)}
131
- />
132
- </div>
133
- )}
134
-
135
- {/* Custom tab */}
136
- {activeTab === 'custom' && (
137
- <div className={styles.dateContainer}>
138
- <label htmlFor="customStartDate">Start Date:</label>
139
- <label htmlFor="customEndDate">End Date:</label>
140
-
141
- <input
142
- id="customStartDate"
143
- type="date"
144
- className={styles.input}
145
- value={startDateString}
146
- onChange={(e) => setStartDateString(e.target.value)}
147
- />
148
-
149
- <input
150
- id="customEndDate"
151
- type="date"
152
- className={styles.input}
153
- value={endDateString}
154
- onChange={(e) => setEndDateString(e.target.value)}
155
- />
156
- </div>
157
- )}
158
- </div>
159
- )}
227
+ {usesMonth && (
228
+ <>
229
+ <Select
230
+ id="report-year"
231
+ labelText="Year"
232
+ size="sm"
233
+ className={classNames(styles.field, styles.yearField)}
234
+ invalid={Boolean(errors.year)}
235
+ invalidText={errors.year}
236
+ value={selectedYear}
237
+ onChange={(event) => setMonthString(`${event.target.value}-${selectedMonth}`)}
238
+ >
239
+ {years.map((year) => (
240
+ <SelectItem key={year} value={year} text={year} />
241
+ ))}
242
+ </Select>
243
+ <Select
244
+ id="report-month"
245
+ labelText="Month"
246
+ size="sm"
247
+ className={classNames(styles.field, styles.monthField)}
248
+ invalid={Boolean(errors.month)}
249
+ invalidText={errors.month}
250
+ value={selectedMonth}
251
+ onChange={(event) => setMonthString(`${selectedYear}-${event.target.value}`)}
252
+ >
253
+ {months.map((month) => (
254
+ <SelectItem key={month.value} value={month.value} text={month.label} />
255
+ ))}
256
+ </Select>
257
+ </>
258
+ )}
160
259
 
161
- {isFacility === 'true' && (
162
- <div>
163
- <label>Facility</label>
260
+ {usesDateRange && <div className={classNames(styles.field, styles.rangeField)}>{dateRangePicker}</div>}
164
261
 
165
- <select
166
- className={styles.input}
167
- name="selectedFacility"
262
+ {facilities && (
263
+ <Select
264
+ id="report-facility"
265
+ labelText="Facility"
266
+ size="sm"
267
+ className={styles.field}
168
268
  value={selectedFacility}
169
- onChange={(e) => setSelectedFacility(e.target.value)}
269
+ onChange={(event) => setSelectedFacility(event.target.value)}
170
270
  >
171
- <option value="">{facility}</option>
172
- </select>
173
- </div>
174
- )}
175
- </div>
271
+ <SelectItem value="" text="All facilities" />
272
+ {facilities.map((facility) => (
273
+ <SelectItem key={facility.value} value={facility.value} text={facility.label} />
274
+ ))}
275
+ </Select>
276
+ )}
277
+ </div>
176
278
 
177
- {!isLoding && (
178
- <div className={styles.buttonContainer}>
179
- <Button onClick={generateReport}>Generate Report</Button>
279
+ <div className={styles.actions}>
280
+ {/* Restating a period that cannot be generated reads as if it will be,
281
+ so the blocking reason takes its place. */}
282
+ {isValid && isStale ? (
283
+ <p className={classNames(styles.summary, styles.summaryStale)}>
284
+ <WarningAlt size={16} />
285
+ Filters changed. The report below is for <strong>{shownPeriod?.label}</strong> &mdash; select Generate
286
+ report to refresh it.
287
+ </p>
288
+ ) : isValid ? (
289
+ <p className={styles.summary}>
290
+ <Calendar size={16} />
291
+ Reporting period: <strong>{period.label}</strong>
292
+ </p>
293
+ ) : (
294
+ <p className={classNames(styles.summary, styles.summaryInvalid)}>
295
+ <WarningAlt size={16} />
296
+ {firstError(errors)}
297
+ </p>
298
+ )}
299
+ <div className={styles.actionButtons}>
300
+ {/* Only offered once there is something on screen to put on paper */}
301
+ {isReportGenerated && !isLoading && (
302
+ <Button kind="tertiary" size="sm" renderIcon={Download} onClick={() => window.print()}>
303
+ Download PDF
304
+ </Button>
305
+ )}
306
+ <Button size="sm" renderIcon={ChartColumn} onClick={handleGenerate} disabled={isLoading || !isValid}>
307
+ {isLoading ? 'Generating…' : 'Generate report'}
308
+ </Button>
309
+ </div>
180
310
  </div>
181
- )}
182
- </>
311
+ </Layer>
312
+ </div>
183
313
  );
184
314
  };
185
315