@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.
- package/dist/110.js +2 -1
- package/dist/110.js.LICENSE.txt +35 -0
- package/dist/110.js.map +1 -1
- package/dist/205.js +1 -1
- package/dist/441.js +1 -0
- package/dist/441.js.map +1 -0
- package/dist/738.js +1 -0
- package/dist/738.js.map +1 -0
- package/dist/812.js +1 -1
- package/dist/812.js.map +1 -1
- package/dist/ampath-esm-reports-app.js +1 -1
- package/dist/ampath-esm-reports-app.js.buildmanifest.json +66 -39
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/common/report-error/index.ts +1 -0
- package/src/common/report-error/show-report-error.ts +25 -0
- package/src/common/report-filters/date-range.ts +137 -0
- package/src/common/report-filters/index.ts +4 -0
- package/src/common/report-filters/report-filters.component.tsx +291 -104
- package/src/common/report-filters/report-filters.scss +199 -32
- package/src/common/report-filters/types.ts +66 -0
- package/src/common/report-filters/use-restore-period.ts +43 -0
- package/src/common/report-page/index.ts +2 -0
- package/src/common/report-page/report-page.component.tsx +32 -0
- package/src/common/report-page/report-page.scss +45 -0
- package/src/common/report-page/use-page-scale.ts +50 -0
- package/src/common/report-placeholder/has-report-data.ts +15 -0
- package/src/common/report-placeholder/index.ts +2 -0
- package/src/common/report-placeholder/report-placeholder.component.tsx +56 -0
- package/src/common/report-placeholder/report-placeholder.scss +46 -0
- package/src/common/report-register/index.ts +5 -0
- package/src/common/report-register/register-columns.ts +34 -0
- package/src/common/report-register/register-layout.component.tsx +237 -0
- package/src/common/report-register/register-layout.scss +140 -0
- package/src/common/report-register/register-table.scss +62 -0
- package/src/common/report-register/report-parents.ts +27 -0
- package/src/common/report-register/rows-to-csv.ts +59 -0
- package/src/common/report-register/use-patient-list.ts +92 -0
- package/src/common/report-skeleton/index.ts +1 -0
- package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
- package/src/common/report-skeleton/report-skeleton.scss +29 -0
- package/src/config-schema.ts +9 -1
- package/src/dashboard/report-catalog.ts +173 -0
- package/src/dashboard/reports-dasboard.tsx +101 -35
- package/src/dashboard/reports-dashboard.module.scss +269 -11
- package/src/index.ts +13 -1
- package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +50 -48
- package/src/reports/moh-240/moh-240-header.component.tsx +54 -56
- package/src/reports/moh-240/moh-240.columns.ts +64 -0
- package/src/reports/moh-240/moh-240.component.tsx +65 -84
- package/src/reports/moh-240/moh-240.scss +36 -7
- package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +27 -65
- package/src/reports/moh-240/sub-reports/page-summary.component.tsx +2 -2
- package/src/reports/moh-505/moh-505-header.component.tsx +59 -57
- package/src/reports/moh-505/moh-505.component.tsx +51 -56
- package/src/reports/moh-505/moh-505.scss +116 -2
- package/src/reports/moh-705B/moh-204b-register.component.tsx +41 -42
- package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
- package/src/reports/moh-705B/moh-705b.component.tsx +492 -473
- package/src/reports/moh-705B/moh-705b.scss +110 -24
- package/src/reports/moh-705a/moh-705a.component.tsx +496 -476
- package/src/reports/moh-705a/moh-705a.scss +109 -24
- package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +41 -42
- package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
- package/src/reports/moh-706/moh-706-header.component.tsx +58 -56
- package/src/reports/moh-706/moh-706.component.tsx +84 -55
- package/src/reports/moh-706/moh-706.scss +115 -19
- package/src/reports/moh-710/moh-710.component.tsx +743 -727
- package/src/reports/moh-710/moh-710.scss +140 -23
- package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +48 -43
- package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
- package/src/reports/moh-711/moh-711.component.tsx +94 -106
- package/src/reports/moh-711/moh711.scss +95 -64
- package/src/reports/moh-711/registers/moh-333-register.component.tsx +92 -94
- package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
- package/src/reports/moh-711/registers/moh-405-register.component.tsx +88 -89
- package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
- package/src/reports/moh-711/registers/moh-406-register.component.tsx +85 -88
- package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
- package/src/reports/moh-711/registers/moh-510-register.component.tsx +38 -44
- package/src/reports/moh-711/registers/moh-511-register.component.tsx +42 -43
- package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
- package/src/reports/moh-711/sections/anc.component.tsx +3 -3
- package/src/reports/moh-711/sections/cervical-cancer.component.tsx +3 -3
- package/src/reports/moh-711/sections/chanis.component.tsx +2 -3
- package/src/reports/moh-711/sections/family-planning.component.tsx +2 -2
- package/src/reports/moh-711/sections/gbv.component.tsx +2 -2
- package/src/reports/moh-711/sections/maternity.component.tsx +3 -8
- package/src/reports/moh-711/sections/medical-social-work.component.tsx +2 -12
- package/src/reports/moh-711/sections/other.component.tsx +2 -2
- package/src/reports/moh-711/sections/physiotherapy.component.tsx +2 -2
- package/src/reports/moh-711/sections/pnc.component.tsx +7 -5
- package/src/reports/moh-711/sections/post-abortion.component.tsx +2 -2
- package/src/reports/moh-711/sections/rehabilitation.component.tsx +2 -2
- package/src/reports/moh-711/sections/report-compiled-by.component.tsx +12 -4
- package/src/reports/moh-711/sections/tb-screening.component.tsx +2 -2
- package/src/reports/moh-717/moh-717.component.tsx +100 -121
- package/src/reports/moh-717/moh717.scss +163 -102
- package/src/reports/moh-717/sections/maternity.component.tsx +1 -1
- package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +1 -1
- package/src/reports/moh-717/sections/outpatient.component.tsx +23 -23
- package/src/reports/moh-717/sections/preparedby.component.tsx +7 -2
- package/src/reports/moh-717/sections/special-services.component.tsx +1 -1
- package/src/reports/moh-731/moh-731.component.tsx +127 -110
- package/src/reports/moh-731/moh-731.scss +396 -68
- package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
- package/src/reports/moh-731/registers/moh-366.component.tsx +76 -83
- package/src/reports/moh-731/registers/prep-register.component.tsx +70 -78
- package/src/reports/moh-731/registers/prep.columns.ts +34 -0
- package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1031 -751
- package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +230 -86
- package/src/reports/moh-731/sections/pmtct.component.tsx +185 -64
- package/src/reports/moh-740/moh-740.component.scss +159 -79
- package/src/reports/moh-740/moh-740.component.tsx +258 -227
- package/src/reports/moh-740/moh-740.resource.ts +17 -10
- package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +77 -96
- package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +50 -70
- package/src/reports/moh-740/registers/moh-740-patient-list.tsx +61 -32
- package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
- package/src/reports/moh-745/moh-745.component.tsx +897 -877
- package/src/reports/moh-745/moh-745.scss +104 -46
- package/src/reports/moh-745/registers/moh-412-register.component.tsx +42 -43
- package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
- package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
- package/src/reports/table-wrapper/table-wrapper.scss +1 -16
- package/src/resources/moh-505.resource.ts +18 -35
- package/src/resources/moh-705.resource.ts +28 -39
- package/src/resources/moh-706.resource.ts +17 -30
- package/src/resources/moh-710.resource.ts +7 -17
- package/src/resources/moh-711.resource.ts +46 -64
- package/src/resources/moh-717.resource.ts +7 -17
- package/src/resources/moh-731.resource.ts +26 -32
- package/src/resources/moh-745.resource.ts +18 -20
- package/src/resources/patient-list-page.ts +66 -0
- package/src/root.component.tsx +28 -26
- package/src/root.scss +39 -0
- package/src/utils/get-base-url.ts +39 -7
- package/dist/822.js +0 -2
- package/dist/822.js.LICENSE.txt +0 -20
- package/dist/822.js.map +0 -1
- package/src/reports/moh-731/registers/moh-366.scss +0 -45
- package/src/reports/moh-731/registers/prep-registers.scss +0 -45
- package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.scss +0 -18
- package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.scss +0 -18
- /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 {
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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 = '
|
|
18
|
-
|
|
38
|
+
mode = 'both',
|
|
39
|
+
facilities,
|
|
40
|
+
yearsBack = 6,
|
|
41
|
+
isLoading = false,
|
|
42
|
+
isReportGenerated = false,
|
|
43
|
+
generatedPeriod,
|
|
19
44
|
onGenerate,
|
|
20
|
-
|
|
45
|
+
onPeriodChange,
|
|
21
46
|
}) => {
|
|
22
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
33
|
-
|
|
148
|
+
const isStale = isReportGenerated && shownPeriod !== null && shownPeriod.key !== periodKey;
|
|
149
|
+
|
|
150
|
+
const handleGenerate = () => {
|
|
151
|
+
if (!isValid) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
34
154
|
|
|
35
|
-
|
|
155
|
+
setGeneratedFor({ key: periodKey, label: period.label });
|
|
156
|
+
onGenerate(period);
|
|
36
157
|
};
|
|
37
158
|
|
|
38
|
-
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
<
|
|
61
|
-
<
|
|
62
|
-
|
|
63
|
-
</
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
className={styles.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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={(
|
|
269
|
+
onChange={(event) => setSelectedFacility(event.target.value)}
|
|
114
270
|
>
|
|
115
|
-
<
|
|
116
|
-
|
|
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> — 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
|
-
.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
|
|
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
|
-
.
|
|
23
|
-
display:
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
30
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
41
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
+
}
|