@ampath/esm-reports-app 1.0.0-next.40 → 1.0.0-next.42

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 (147) hide show
  1. package/dist/110.js +2 -1
  2. package/dist/110.js.LICENSE.txt +35 -0
  3. package/dist/110.js.map +1 -1
  4. package/dist/205.js +1 -1
  5. package/dist/441.js +1 -0
  6. package/dist/441.js.map +1 -0
  7. package/dist/738.js +1 -0
  8. package/dist/738.js.map +1 -0
  9. package/dist/812.js +1 -1
  10. package/dist/812.js.map +1 -1
  11. package/dist/ampath-esm-reports-app.js +1 -1
  12. package/dist/ampath-esm-reports-app.js.buildmanifest.json +66 -39
  13. package/dist/main.js +1 -1
  14. package/dist/main.js.map +1 -1
  15. package/dist/routes.json +1 -1
  16. package/package.json +1 -1
  17. package/src/common/report-error/index.ts +1 -0
  18. package/src/common/report-error/show-report-error.ts +25 -0
  19. package/src/common/report-filters/date-range.ts +137 -0
  20. package/src/common/report-filters/index.ts +4 -0
  21. package/src/common/report-filters/report-filters.component.tsx +291 -104
  22. package/src/common/report-filters/report-filters.scss +199 -32
  23. package/src/common/report-filters/types.ts +66 -0
  24. package/src/common/report-filters/use-restore-period.ts +43 -0
  25. package/src/common/report-page/index.ts +2 -0
  26. package/src/common/report-page/report-page.component.tsx +32 -0
  27. package/src/common/report-page/report-page.scss +45 -0
  28. package/src/common/report-page/use-page-scale.ts +50 -0
  29. package/src/common/report-placeholder/has-report-data.ts +15 -0
  30. package/src/common/report-placeholder/index.ts +2 -0
  31. package/src/common/report-placeholder/report-placeholder.component.tsx +56 -0
  32. package/src/common/report-placeholder/report-placeholder.scss +46 -0
  33. package/src/common/report-register/index.ts +5 -0
  34. package/src/common/report-register/register-columns.ts +34 -0
  35. package/src/common/report-register/register-layout.component.tsx +237 -0
  36. package/src/common/report-register/register-layout.scss +140 -0
  37. package/src/common/report-register/register-table.scss +62 -0
  38. package/src/common/report-register/report-parents.ts +27 -0
  39. package/src/common/report-register/rows-to-csv.ts +59 -0
  40. package/src/common/report-register/use-patient-list.ts +92 -0
  41. package/src/common/report-skeleton/index.ts +1 -0
  42. package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
  43. package/src/common/report-skeleton/report-skeleton.scss +29 -0
  44. package/src/config-schema.ts +9 -1
  45. package/src/dashboard/report-catalog.ts +173 -0
  46. package/src/dashboard/reports-dasboard.tsx +101 -35
  47. package/src/dashboard/reports-dashboard.module.scss +269 -11
  48. package/src/index.ts +13 -1
  49. package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +50 -48
  50. package/src/reports/moh-240/moh-240-header.component.tsx +54 -56
  51. package/src/reports/moh-240/moh-240.columns.ts +64 -0
  52. package/src/reports/moh-240/moh-240.component.tsx +65 -84
  53. package/src/reports/moh-240/moh-240.scss +36 -7
  54. package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +27 -65
  55. package/src/reports/moh-240/sub-reports/page-summary.component.tsx +2 -2
  56. package/src/reports/moh-505/moh-505-header.component.tsx +59 -57
  57. package/src/reports/moh-505/moh-505.component.tsx +51 -56
  58. package/src/reports/moh-505/moh-505.scss +116 -2
  59. package/src/reports/moh-705B/moh-204b-register.component.tsx +41 -42
  60. package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
  61. package/src/reports/moh-705B/moh-705b.component.tsx +492 -473
  62. package/src/reports/moh-705B/moh-705b.scss +110 -24
  63. package/src/reports/moh-705a/moh-705a.component.tsx +496 -476
  64. package/src/reports/moh-705a/moh-705a.scss +109 -24
  65. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +41 -42
  66. package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
  67. package/src/reports/moh-706/moh-706-header.component.tsx +58 -56
  68. package/src/reports/moh-706/moh-706.component.tsx +84 -55
  69. package/src/reports/moh-706/moh-706.scss +115 -19
  70. package/src/reports/moh-710/moh-710.component.tsx +743 -727
  71. package/src/reports/moh-710/moh-710.scss +140 -23
  72. package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +48 -43
  73. package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
  74. package/src/reports/moh-711/moh-711.component.tsx +94 -106
  75. package/src/reports/moh-711/moh711.scss +95 -64
  76. package/src/reports/moh-711/registers/moh-333-register.component.tsx +92 -94
  77. package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
  78. package/src/reports/moh-711/registers/moh-405-register.component.tsx +88 -89
  79. package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
  80. package/src/reports/moh-711/registers/moh-406-register.component.tsx +85 -88
  81. package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
  82. package/src/reports/moh-711/registers/moh-510-register.component.tsx +38 -44
  83. package/src/reports/moh-711/registers/moh-511-register.component.tsx +42 -43
  84. package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
  85. package/src/reports/moh-711/sections/anc.component.tsx +3 -3
  86. package/src/reports/moh-711/sections/cervical-cancer.component.tsx +3 -3
  87. package/src/reports/moh-711/sections/chanis.component.tsx +2 -3
  88. package/src/reports/moh-711/sections/family-planning.component.tsx +2 -2
  89. package/src/reports/moh-711/sections/gbv.component.tsx +2 -2
  90. package/src/reports/moh-711/sections/maternity.component.tsx +3 -8
  91. package/src/reports/moh-711/sections/medical-social-work.component.tsx +2 -12
  92. package/src/reports/moh-711/sections/other.component.tsx +2 -2
  93. package/src/reports/moh-711/sections/physiotherapy.component.tsx +2 -2
  94. package/src/reports/moh-711/sections/pnc.component.tsx +7 -5
  95. package/src/reports/moh-711/sections/post-abortion.component.tsx +2 -2
  96. package/src/reports/moh-711/sections/rehabilitation.component.tsx +2 -2
  97. package/src/reports/moh-711/sections/report-compiled-by.component.tsx +12 -4
  98. package/src/reports/moh-711/sections/tb-screening.component.tsx +2 -2
  99. package/src/reports/moh-717/moh-717.component.tsx +100 -121
  100. package/src/reports/moh-717/moh717.scss +163 -102
  101. package/src/reports/moh-717/sections/maternity.component.tsx +1 -1
  102. package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +1 -1
  103. package/src/reports/moh-717/sections/outpatient.component.tsx +23 -23
  104. package/src/reports/moh-717/sections/preparedby.component.tsx +7 -2
  105. package/src/reports/moh-717/sections/special-services.component.tsx +1 -1
  106. package/src/reports/moh-731/moh-731.component.tsx +127 -110
  107. package/src/reports/moh-731/moh-731.scss +396 -68
  108. package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
  109. package/src/reports/moh-731/registers/moh-366.component.tsx +76 -83
  110. package/src/reports/moh-731/registers/prep-register.component.tsx +70 -78
  111. package/src/reports/moh-731/registers/prep.columns.ts +34 -0
  112. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1031 -751
  113. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +230 -86
  114. package/src/reports/moh-731/sections/pmtct.component.tsx +185 -64
  115. package/src/reports/moh-740/moh-740.component.scss +159 -79
  116. package/src/reports/moh-740/moh-740.component.tsx +258 -227
  117. package/src/reports/moh-740/moh-740.resource.ts +17 -10
  118. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +77 -96
  119. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +50 -70
  120. package/src/reports/moh-740/registers/moh-740-patient-list.tsx +61 -32
  121. package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
  122. package/src/reports/moh-745/moh-745.component.tsx +897 -877
  123. package/src/reports/moh-745/moh-745.scss +104 -46
  124. package/src/reports/moh-745/registers/moh-412-register.component.tsx +42 -43
  125. package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
  126. package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
  127. package/src/reports/table-wrapper/table-wrapper.scss +1 -16
  128. package/src/resources/moh-505.resource.ts +18 -35
  129. package/src/resources/moh-705.resource.ts +28 -39
  130. package/src/resources/moh-706.resource.ts +17 -30
  131. package/src/resources/moh-710.resource.ts +7 -17
  132. package/src/resources/moh-711.resource.ts +46 -64
  133. package/src/resources/moh-717.resource.ts +7 -17
  134. package/src/resources/moh-731.resource.ts +26 -32
  135. package/src/resources/moh-745.resource.ts +18 -20
  136. package/src/resources/patient-list-page.ts +66 -0
  137. package/src/root.component.tsx +28 -26
  138. package/src/root.scss +39 -0
  139. package/src/utils/get-base-url.ts +39 -7
  140. package/dist/822.js +0 -2
  141. package/dist/822.js.LICENSE.txt +0 -20
  142. package/dist/822.js.map +0 -1
  143. package/src/reports/moh-731/registers/moh-366.scss +0 -45
  144. package/src/reports/moh-731/registers/prep-registers.scss +0 -45
  145. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.scss +0 -18
  146. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.scss +0 -18
  147. /package/src/{reports/moh-740/shared/utils → utils}/format-indicator.ts +0 -0
@@ -1,128 +1,315 @@
1
- import { ArrowLeft } from '@carbon/react/icons';
2
- import { useNavigate } from 'react-router-dom';
3
1
  import React from 'react';
4
- 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';
5
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';
6
29
  import styles from './report-filters.scss';
7
30
 
8
- interface ReportFiltersComponentProps {
9
- reportName: string;
10
- mode?: 'monthly' | 'daily';
11
- isFacility?: false | 'true';
12
- onGenerate?: (filters: { startDate?: string; endDate?: string; month?: string }) => void;
13
- isLoding?: boolean;
14
- }
15
- const ReportFiltersComponent: React.FC<ReportFiltersComponentProps> = ({
31
+ const months = Array.from({ length: 12 }, (_, index) => ({
32
+ value: String(index + 1).padStart(2, '0'),
33
+ label: dayjs().month(index).format('MMMM'),
34
+ }));
35
+
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);
66
+
67
+ const [activeTab, setActiveTab] = React.useState<'monthly' | 'custom'>(
68
+ urlStart && urlEnd && !urlWholeMonth ? 'custom' : 'monthly',
69
+ );
70
+
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
+ );
76
+
77
+ const [selectedFacility, setSelectedFacility] = React.useState<string>('');
78
+
79
+ const [selectedYear, selectedMonth] = monthString.split('-');
80
+
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
+ );
86
+
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 ?? ''}`;
27
136
 
28
- const getPreviousMonth = () => {
29
- const date = new Date();
30
- date.setMonth(date.getMonth() - 1);
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),
145
+ }
146
+ : null);
31
147
 
32
- const year = date.getFullYear();
33
- const month = String(date.getMonth() + 1).padStart(2, '0');
148
+ const isStale = isReportGenerated && shownPeriod !== null && shownPeriod.key !== periodKey;
149
+
150
+ const handleGenerate = () => {
151
+ if (!isValid) {
152
+ return;
153
+ }
34
154
 
35
- return `${year}-${month}`;
155
+ setGeneratedFor({ key: periodKey, label: period.label });
156
+ onGenerate(period);
36
157
  };
37
158
 
38
- const [startDateString, setStartDateString] = React.useState<string>(getToday());
39
- const [endDateString, setEndDateString] = React.useState<string>(getToday());
40
- const [monthString, setMonthString] = React.useState<string>(getPreviousMonth());
41
- const [selectedFacility, setSelectedFacility] = React.useState<string>('');
42
- const [facility, setFacility] = React.useState<string>('Select Facility');
43
- const [startDateTime, setStartDateTime] = React.useState<string>('');
44
- const [endDateTime, setEndDateTime] = React.useState<string>('');
45
-
46
- const generateReport = () => {
47
- if (onGenerate) {
48
- const filters: { startDate?: string; endDate?: string; month?: string } = {};
49
- if (mode === 'daily') {
50
- filters.startDate = startDateString;
51
- filters.endDate = endDateString;
52
- } else if (mode === 'monthly') {
53
- filters.month = monthString;
54
- }
55
- onGenerate(filters);
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));
56
166
  }
57
167
  };
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
+
58
197
  return (
59
- <>
60
- <div className={styles.titleContainer}>
61
- <Button className={styles.backIcon} onClick={() => navigate('/')}>
62
- Back
63
- </Button>
64
- <h2 className={styles.title}>{reportName}</h2>
65
- </div>
66
- <div className={styles.filtersContainer}>
67
- {mode === 'daily' && (
68
- <div className={styles.dateContainer}>
69
- <label htmlFor="startingMonth">Start Date:</label>
70
- <label htmlFor="endingMonth">End Date:</label>
71
-
72
- <input
73
- id="startingMonth"
74
- type="date"
75
- className={styles.input}
76
- value={startDateString}
77
- onChange={(e) => setStartDateString(e.target.value)}
78
- />
79
-
80
- <input
81
- id="endingMonth"
82
- type="date"
83
- className={styles.input}
84
- value={endDateString}
85
- onChange={(e) => setEndDateString(e.target.value)}
86
- />
87
- </div>
88
- )}
89
- {mode === 'monthly' && (
90
- <div>
91
- <div>
92
- <div>
93
- <label htmlFor="month">Month:</label>
94
- <input
95
- id="month"
96
- type="month"
97
- className={styles.input}
98
- value={monthString}
99
- onChange={(e) => setMonthString(e.target.value)}
100
- />
101
- </div>
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>
209
+
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}
220
+ >
221
+ <Switch name="monthly" text="Monthly" />
222
+ <Switch name="custom" text="Custom range" />
223
+ </ContentSwitcher>
102
224
  </div>
103
- <div className={styles.clearFix}></div>
104
- </div>
105
- )}
106
- {isFacility === 'true' && (
107
- <div>
108
- <label>Facility</label>
109
- <select
110
- className={styles.input}
111
- name="selectedFacility"
225
+ )}
226
+
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
+ )}
259
+
260
+ {usesDateRange && <div className={classNames(styles.field, styles.rangeField)}>{dateRangePicker}</div>}
261
+
262
+ {facilities && (
263
+ <Select
264
+ id="report-facility"
265
+ labelText="Facility"
266
+ size="sm"
267
+ className={styles.field}
112
268
  value={selectedFacility}
113
- onChange={(e) => setSelectedFacility(e.target.value)}
269
+ onChange={(event) => setSelectedFacility(event.target.value)}
114
270
  >
115
- <option value="">{facility}</option>
116
- </select>
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>
278
+
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>
117
309
  </div>
118
- )}
119
- </div>
120
- {!isLoding && (
121
- <div className={styles.buttonContainer}>
122
- <Button onClick={generateReport}>Generate Report</Button>
123
310
  </div>
124
- )}
125
- </>
311
+ </Layer>
312
+ </div>
126
313
  );
127
314
  };
128
315
 
@@ -1,49 +1,216 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
1
4
 
5
+ .filters {
6
+ padding: layout.$spacing-05 0 0;
7
+ }
2
8
 
3
- .filtersContainer {
4
- margin: 0 2rem;
5
- margin-bottom: 0.5rem;
6
- border: 1px solid #ccc;
7
- padding: 1rem;
9
+ /* Breadcrumb trail. The trailing crumb is the page heading, so the items are
10
+ baseline-aligned rather than centred on differing font sizes. */
11
+
12
+ .breadcrumb {
13
+ margin-bottom: layout.$spacing-04;
14
+
15
+ :global(.cds--breadcrumb-item) {
16
+ display: inline-flex;
17
+ align-items: baseline;
18
+ }
8
19
  }
9
20
 
21
+ /* An `h3` inside the trailing crumb, so it keeps heading semantics without
22
+ repeating the report name in a separate title row. */
10
23
  .title {
11
- font-size: 1.5rem;
12
- font-weight: bold;
13
- margin-top: 1rem;
14
- color: green;
15
- text-decoration: underline;
24
+ @include type.type-style('productive-heading-03');
25
+ display: inline;
26
+ color: $color-gray-100;
16
27
  }
17
28
 
18
- .titleContainer {
19
- margin: 2rem;
29
+ /* Filter card */
30
+
31
+ .filtersContainer {
32
+ padding: layout.$spacing-04 layout.$spacing-05;
33
+ background-color: $ui-02;
34
+ border: 1px solid $ui-03;
35
+ border-left: layout.$spacing-02 solid var(--brand-01);
20
36
  }
21
37
 
22
- .dateContainer {
23
- display: grid;
24
- grid-template-columns: 1fr 1fr;
25
- gap: 8px 16px;
26
- align-items: center;
38
+ .fields {
39
+ display: flex;
40
+ flex-wrap: wrap;
41
+ align-items: flex-end;
42
+ gap: layout.$spacing-05;
43
+ margin-bottom: layout.$spacing-04;
27
44
  }
28
45
 
29
- .dateContainer label {
30
- font-weight: 500;
46
+ /* Nested so it beats the `flex: 0 1 auto` that Carbon puts on `.cds--form-item`.
47
+
48
+ Fields grow into the row but stop at a width suited to what they hold. Left
49
+ to share the row unbounded, a monthly report -- which shows only Year and
50
+ Month -- gave a four-digit year a 434px box and the month 650px. The cap
51
+ keeps every control a sensible size and leaves the slack at the end of the
52
+ row rather than inside the inputs. */
53
+
54
+ .fields .field {
55
+ flex: 1 1 auto;
56
+ min-width: 0;
57
+ }
58
+
59
+ .fields .periodField {
60
+ min-width: 14rem;
61
+ max-width: 16rem;
62
+ }
63
+
64
+ /* Wide enough for the validation message, not just the value. Carbon renders
65
+ `invalidText` under the input at the field's own width, so a field sized to
66
+ "2026" or "September" makes "That month has not started yet." wrap onto a
67
+ second line and jog the row below it. */
68
+
69
+ .fields .yearField {
70
+ min-width: 7rem;
71
+ max-width: 9rem;
31
72
  }
32
73
 
33
- .input {
74
+ .fields .monthField {
75
+ min-width: 9rem;
76
+ max-width: 13rem;
77
+ }
78
+
79
+ /* The range picker holds two inputs, so it is allowed roughly twice the width */
80
+ .fields .rangeField {
81
+ min-width: 16rem;
82
+ max-width: 24rem;
83
+ }
84
+
85
+ /* Matches Carbon's `.cds--label` so the switcher's label sits on the same
86
+ baseline as the labels Carbon renders for the selects. */
87
+ .fieldLabel {
88
+ @include type.type-style('label-01');
89
+ display: inline-block;
90
+ color: $text-02;
91
+ line-height: 1rem;
92
+ margin-bottom: layout.$spacing-03;
93
+ vertical-align: baseline;
94
+ }
95
+
96
+ /* Carbon sizes these to their content, so they need telling to fill the field */
97
+
98
+ .switcher,
99
+ .fields .field :global(.cds--select-input) {
34
100
  width: 100%;
35
- padding: 6px 8px;
36
- box-sizing: border-box;
37
- border: 1px solid #ccc;
38
- outline: none;
39
101
  }
40
- .input:focus {
41
- border-color: #999;
102
+
103
+ .datePicker {
104
+ display: flex;
105
+ gap: layout.$spacing-05;
106
+ width: 100%;
107
+ }
108
+
109
+ /* Carbon pins `inline-size: 143.5px` on both the container and the input in
110
+ range mode. Overridden through the range class so this wins on specificity
111
+ rather than on stylesheet order. */
112
+ .fields .datePicker:global(.cds--date-picker--range) {
113
+ :global(.cds--date-picker-container) {
114
+ flex: 1 1 0;
115
+ inline-size: auto;
116
+ min-inline-size: 0;
117
+ }
118
+
119
+ :global(.cds--date-picker__input) {
120
+ inline-size: 100%;
121
+ min-inline-size: 0;
122
+ }
42
123
  }
43
124
 
44
- .buttonContainer {
45
- display: flex;
46
- justify-content: flex-end;
47
- gap: 10px;
48
- margin: 0 2rem;
49
- }
125
+ /* Action row */
126
+
127
+ .actions {
128
+ display: flex;
129
+ align-items: center;
130
+ justify-content: space-between;
131
+ flex-wrap: wrap;
132
+ gap: layout.$spacing-04;
133
+ padding-top: layout.$spacing-04;
134
+ border-top: 1px solid $ui-03;
135
+ }
136
+
137
+ .summary {
138
+ @include type.type-style('body-short-01');
139
+ display: inline-flex;
140
+ align-items: center;
141
+ gap: layout.$spacing-03;
142
+ color: $text-02;
143
+
144
+ svg {
145
+ fill: var(--brand-01);
146
+ }
147
+
148
+ strong {
149
+ color: $color-gray-100;
150
+ font-weight: 600;
151
+ }
152
+ }
153
+
154
+ .actionButtons {
155
+ display: flex;
156
+ align-items: center;
157
+ gap: layout.$spacing-03;
158
+ margin-left: auto;
159
+ }
160
+
161
+ /* The filter bar is application chrome, not part of the report */
162
+
163
+ @media print {
164
+ .filters {
165
+ display: none;
166
+ }
167
+ }
168
+
169
+ @media (max-width: $breakpoint-tablet-max) {
170
+ .filters {
171
+ padding: layout.$spacing-05 0 0;
172
+ }
173
+
174
+ .fields,
175
+ .datePicker {
176
+ flex-direction: column;
177
+ align-items: stretch;
178
+ }
179
+
180
+ .fields .field,
181
+ .switcher {
182
+ min-width: 0;
183
+ width: 100%;
184
+ }
185
+
186
+ .actionButtons {
187
+ margin-left: 0;
188
+ width: 100%;
189
+
190
+ button {
191
+ flex: 1 1 0;
192
+ }
193
+ }
194
+ }
195
+
196
+ /* The summary doubles as the blocking reason when the period is not valid */
197
+ .summaryInvalid {
198
+ color: $danger;
199
+
200
+ svg {
201
+ fill: $danger;
202
+ }
203
+ }
204
+
205
+ /* The filters no longer match the report on screen. Warning-toned rather than
206
+ error-toned: nothing is wrong, the figures are simply for another period. */
207
+
208
+ .summaryStale {
209
+ padding: layout.$spacing-02 layout.$spacing-03;
210
+ color: $color-gray-100;
211
+ background-color: $warning-background;
212
+
213
+ svg {
214
+ fill: $color-gray-70;
215
+ }
216
+ }