@ampath/esm-reports-app 1.0.0-next.6 → 1.0.0-next.60

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 (229) hide show
  1. package/dist/197.js +1 -1
  2. package/dist/282.js +1 -1
  3. package/dist/282.js.map +1 -1
  4. package/dist/295.css +1 -0
  5. package/dist/295.css.map +1 -0
  6. package/dist/295.js +1 -0
  7. package/dist/295.js.map +1 -0
  8. package/dist/300.js +1 -1
  9. package/dist/311.js +1 -1
  10. package/dist/311.js.map +1 -1
  11. package/dist/335.js +1 -1
  12. package/dist/353.js +1 -1
  13. package/dist/353.js.map +1 -1
  14. package/dist/410.js +1 -0
  15. package/dist/410.js.map +1 -0
  16. package/dist/478.js +1 -1
  17. package/dist/478.js.map +1 -1
  18. package/dist/540.js +1 -1
  19. package/dist/540.js.map +1 -1
  20. package/dist/55.js +1 -1
  21. package/dist/580.js +1 -0
  22. package/dist/580.js.map +1 -0
  23. package/dist/652.js +1 -1
  24. package/dist/876.css +1 -0
  25. package/dist/876.css.map +1 -0
  26. package/dist/876.js +1 -0
  27. package/dist/876.js.map +1 -0
  28. package/dist/907.js +2 -0
  29. package/dist/907.js.LICENSE.txt +35 -0
  30. package/dist/907.js.map +1 -0
  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/daily-summary-table.tsx +55 -0
  39. package/src/common/linelist-table/index.ts +3 -0
  40. package/src/common/linelist-table/linelist-columns.ts +48 -0
  41. package/src/common/linelist-table/linelist-table.component.tsx +442 -0
  42. package/src/common/linelist-table/linelist-table.scss +306 -0
  43. package/src/common/linelist-table/linelist-table.test.tsx +120 -0
  44. package/src/common/linelist-table/use-all-rows.ts +93 -0
  45. package/src/common/linelist-table/use-column-layout.ts +123 -0
  46. package/src/common/report-error/index.ts +1 -0
  47. package/src/common/report-error/show-report-error.ts +25 -0
  48. package/src/common/report-filters/date-range.ts +137 -0
  49. package/src/common/report-filters/index.ts +4 -0
  50. package/src/common/report-filters/report-filters.component.tsx +297 -100
  51. package/src/common/report-filters/report-filters.scss +255 -32
  52. package/src/common/report-filters/types.ts +66 -0
  53. package/src/common/report-filters/use-restore-period.ts +43 -0
  54. package/src/common/report-header/index.ts +1 -0
  55. package/src/common/report-header/use-location-details.ts +58 -0
  56. package/src/common/report-page/index.ts +2 -0
  57. package/src/common/report-page/report-page.component.tsx +32 -0
  58. package/src/common/report-page/report-page.scss +45 -0
  59. package/src/common/report-page/use-page-scale.ts +60 -0
  60. package/src/common/report-placeholder/has-report-data.ts +15 -0
  61. package/src/common/report-placeholder/index.ts +3 -0
  62. package/src/common/report-placeholder/report-placeholder.component.tsx +81 -0
  63. package/src/common/report-placeholder/report-placeholder.scss +48 -0
  64. package/src/common/report-placeholder/use-generated-location.ts +62 -0
  65. package/src/common/report-register/index.ts +6 -0
  66. package/src/common/report-register/register-columns.ts +46 -0
  67. package/src/common/report-register/register-layout.component.tsx +265 -0
  68. package/src/common/report-register/register-layout.scss +150 -0
  69. package/src/common/report-register/register-table.scss +71 -0
  70. package/src/common/report-register/report-parents.ts +27 -0
  71. package/src/common/report-register/rows-to-csv.ts +89 -0
  72. package/src/common/report-register/use-location-name.ts +54 -0
  73. package/src/common/report-register/use-patient-list.ts +92 -0
  74. package/src/common/report-skeleton/index.ts +1 -0
  75. package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
  76. package/src/common/report-skeleton/report-skeleton.scss +29 -0
  77. package/src/config-schema.ts +9 -1
  78. package/src/dashboard/report-catalog.ts +173 -0
  79. package/src/dashboard/reports-dasboard.tsx +101 -17
  80. package/src/dashboard/reports-dashboard.module.scss +269 -11
  81. package/src/index.ts +13 -1
  82. package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +69 -0
  83. package/src/reports/moh-240/moh-240-header.component.tsx +62 -0
  84. package/src/reports/moh-240/moh-240.columns.ts +113 -0
  85. package/src/reports/moh-240/moh-240.component.tsx +75 -0
  86. package/src/reports/moh-240/moh-240.scss +40 -0
  87. package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +36 -0
  88. package/src/reports/moh-240/sub-reports/page-summary.component.tsx +148 -0
  89. package/src/reports/moh-505/moh-505-header.component.tsx +67 -0
  90. package/src/reports/moh-505/moh-505.component.tsx +441 -0
  91. package/src/reports/moh-505/moh-505.scss +117 -0
  92. package/src/reports/moh-705B/moh-204b-register.component.tsx +244 -0
  93. package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
  94. package/src/reports/moh-705B/moh-705b.component.tsx +658 -0
  95. package/src/reports/moh-705B/moh-705b.scss +172 -0
  96. package/src/reports/moh-705a/moh-705a.component.tsx +660 -0
  97. package/src/reports/moh-705a/moh-705a.scss +172 -0
  98. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +329 -0
  99. package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
  100. package/src/reports/moh-705a/type.ts +6 -0
  101. package/src/reports/moh-706/moh-706-header.component.tsx +66 -0
  102. package/src/reports/moh-706/moh-706.component.tsx +150 -24
  103. package/src/reports/moh-706/moh-706.scss +119 -11
  104. package/src/reports/moh-706/sub-reports/bacteriology/bacteriology.component.tsx +36 -44
  105. package/src/reports/moh-706/sub-reports/blood-chemistry/blood-chemistry.component.tsx +232 -318
  106. package/src/reports/moh-706/sub-reports/drug-susceptibility-testing/drug-susceptibility-testing.component.tsx +32 -47
  107. package/src/reports/moh-706/sub-reports/haematology/haematology.component.tsx +44 -52
  108. package/src/reports/moh-706/sub-reports/histology-and-cytology/histology-and-cytology.component.tsx +25 -40
  109. package/src/reports/moh-706/sub-reports/parasitology/parasitology.component.tsx +31 -39
  110. package/src/reports/moh-706/sub-reports/serology/serology.component.tsx +26 -34
  111. package/src/reports/moh-706/sub-reports/specimen-referral-to-higher-levels/specimen-referral-to-higher-levels.component.tsx +10 -25
  112. package/src/reports/moh-706/sub-reports/urine-analysis/urine-analysis.component.tsx +42 -50
  113. package/src/reports/moh-710/moh-710.component.tsx +799 -632
  114. package/src/reports/moh-710/moh-710.scss +142 -21
  115. package/src/reports/moh-710/registers/moh-510/moh-510.columns.ts +40 -0
  116. package/src/reports/moh-710/registers/moh-510/moh-510.component.tsx +144 -0
  117. package/src/reports/moh-710/registers/moh-510/moh-510.scss +130 -0
  118. package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +268 -0
  119. package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
  120. package/src/reports/moh-710/registers/type.ts +31 -0
  121. package/src/reports/moh-711/moh-711.component.tsx +138 -1269
  122. package/src/reports/moh-711/moh711.scss +135 -20
  123. package/src/reports/moh-711/registers/moh-333-register.component.tsx +637 -0
  124. package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
  125. package/src/reports/moh-711/registers/moh-405-register.component.tsx +520 -0
  126. package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
  127. package/src/reports/moh-711/registers/moh-406-register.component.tsx +530 -0
  128. package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
  129. package/src/reports/moh-711/registers/moh-510-register.component.tsx +55 -0
  130. package/src/reports/moh-711/registers/moh-511-register.component.tsx +270 -0
  131. package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
  132. package/src/reports/moh-711/registers/type.ts +192 -0
  133. package/src/reports/moh-711/sections/anc.component.tsx +159 -0
  134. package/src/reports/moh-711/sections/cervical-cancer.component.tsx +148 -0
  135. package/src/reports/moh-711/sections/chanis.component.tsx +366 -0
  136. package/src/reports/moh-711/sections/family-planning.component.tsx +221 -0
  137. package/src/reports/moh-711/sections/gbv.component.tsx +115 -0
  138. package/src/reports/moh-711/sections/maternity.component.tsx +321 -0
  139. package/src/reports/moh-711/sections/medical-social-work.component.tsx +73 -0
  140. package/src/reports/moh-711/sections/other.component.tsx +47 -0
  141. package/src/reports/moh-711/sections/physiotherapy.component.tsx +61 -0
  142. package/src/reports/moh-711/sections/pnc.component.tsx +127 -0
  143. package/src/reports/moh-711/sections/post-abortion.component.tsx +42 -0
  144. package/src/reports/moh-711/sections/rehabilitation.component.tsx +57 -0
  145. package/src/reports/moh-711/sections/report-compiled-by.component.tsx +50 -0
  146. package/src/reports/moh-711/sections/tb-screening.component.tsx +57 -0
  147. package/src/reports/moh-717/moh-717.component.tsx +149 -985
  148. package/src/reports/moh-717/moh717.scss +217 -21
  149. package/src/reports/moh-717/registers/nutrition-register.component.tsx +48 -0
  150. package/src/reports/moh-717/sections/finance.component.tsx +42 -0
  151. package/src/reports/moh-717/sections/inpatient.component.tsx +374 -0
  152. package/src/reports/moh-717/sections/maternity.component.tsx +126 -0
  153. package/src/reports/moh-717/sections/medical-records.component.tsx +35 -0
  154. package/src/reports/moh-717/sections/mortuary.component.tsx +43 -0
  155. package/src/reports/moh-717/sections/operations.component.tsx +53 -0
  156. package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +98 -0
  157. package/src/reports/moh-717/sections/outpatient.component.tsx +597 -0
  158. package/src/reports/moh-717/sections/pharmacy.component.tsx +43 -0
  159. package/src/reports/moh-717/sections/preparedby.component.tsx +52 -0
  160. package/src/reports/moh-717/sections/special-services.component.tsx +114 -0
  161. package/src/reports/moh-731/linelist/moh-731-indicators.ts +1040 -0
  162. package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +423 -0
  163. package/src/reports/moh-731/linelist/moh-731-linelist.component.tsx +102 -0
  164. package/src/reports/moh-731/linelist/use-open-moh-731-linelist.ts +50 -0
  165. package/src/reports/moh-731/moh-731.component.tsx +251 -0
  166. package/src/reports/moh-731/moh-731.scss +493 -0
  167. package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
  168. package/src/reports/moh-731/registers/moh-366.component.tsx +471 -0
  169. package/src/reports/moh-731/registers/prep-register.component.tsx +284 -0
  170. package/src/reports/moh-731/registers/prep.columns.ts +34 -0
  171. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1209 -0
  172. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +727 -0
  173. package/src/reports/moh-731/sections/pmtct.component.tsx +572 -0
  174. package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
  175. package/src/reports/moh-731/summary/moh-731-summary.config.ts +520 -0
  176. package/src/reports/moh-731/summary/moh-731-summary.scss +242 -0
  177. package/src/reports/moh-740/moh-740.component.scss +202 -0
  178. package/src/reports/moh-740/moh-740.component.tsx +1312 -0
  179. package/src/reports/moh-740/moh-740.resource.ts +49 -0
  180. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +89 -0
  181. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +61 -0
  182. package/src/reports/moh-740/registers/moh-740-patient-list.scss +0 -0
  183. package/src/reports/moh-740/registers/moh-740-patient-list.tsx +95 -0
  184. package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
  185. package/src/reports/moh-740/shared/data-cell/data-cell.scss +6 -0
  186. package/src/reports/moh-740/shared/data-cell/data-cell.tsx +23 -0
  187. package/src/reports/moh-740/shared/utils/indicator-register-map.ts +157 -0
  188. package/src/reports/moh-740/types/index.ts +220 -0
  189. package/src/reports/moh-745/moh-745.component.tsx +1021 -0
  190. package/src/reports/moh-745/moh-745.scss +132 -0
  191. package/src/reports/moh-745/registers/moh-412-register.component.tsx +199 -0
  192. package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
  193. package/src/reports/moh-745/registers/type.ts +181 -0
  194. package/src/reports/table-wrapper/table-row-mapper.component.tsx +53 -0
  195. package/src/reports/table-wrapper/table-wrapper.component.tsx +30 -0
  196. package/src/reports/{moh-706/table-wrapper → table-wrapper}/table-wrapper.scss +1 -16
  197. package/src/resources/moh-505.resource.ts +69 -0
  198. package/src/resources/moh-705.resource.ts +106 -0
  199. package/src/resources/moh-706.resource.ts +80 -0
  200. package/src/resources/moh-710.resource.ts +32 -9
  201. package/src/resources/moh-711.resource.ts +137 -9
  202. package/src/resources/moh-717.resource.ts +26 -9
  203. package/src/resources/moh-731.resource.ts +90 -0
  204. package/src/resources/moh-745.resource.ts +76 -0
  205. package/src/resources/patient-list-page.ts +65 -0
  206. package/src/root.component.tsx +50 -7
  207. package/src/root.scss +36 -0
  208. package/src/routes.json +3 -1
  209. package/src/utils/format-indicator.ts +22 -0
  210. package/src/utils/get-base-url.ts +39 -7
  211. package/src/utils/utils.ts +36 -0
  212. package/tsconfig.json +1 -1
  213. package/webpack.config.js +22 -1
  214. package/dist/184.js +0 -2
  215. package/dist/184.js.LICENSE.txt +0 -14
  216. package/dist/184.js.map +0 -1
  217. package/dist/260.js +0 -1
  218. package/dist/260.js.map +0 -1
  219. package/dist/402.js +0 -1
  220. package/dist/402.js.map +0 -1
  221. package/dist/812.js +0 -1
  222. package/dist/812.js.map +0 -1
  223. package/dist/959.js +0 -2
  224. package/dist/959.js.LICENSE.txt +0 -20
  225. package/dist/959.js.map +0 -1
  226. package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -539
  227. package/dist/main.js +0 -1
  228. package/dist/main.js.map +0 -1
  229. package/src/reports/moh-706/table-wrapper/table-wrapper.component.tsx +0 -21
@@ -0,0 +1,137 @@
1
+ import dayjs from 'dayjs';
2
+
3
+ /** The wire format every report resource expects for dates */
4
+ export const DATE_FORMAT = 'YYYY-MM-DD';
5
+
6
+ /** The format the month dropdown round-trips through */
7
+ export const MONTH_FORMAT = 'YYYY-MM';
8
+
9
+ export function formatDate(date: dayjs.ConfigType): string {
10
+ return dayjs(date).format(DATE_FORMAT);
11
+ }
12
+
13
+ export function formatMonth(date: dayjs.ConfigType): string {
14
+ return dayjs(date).format(MONTH_FORMAT);
15
+ }
16
+
17
+ /**
18
+ * Expands `YYYY-MM` into the first and last day of that month.
19
+ *
20
+ * Parsed in local time on purpose: reporting periods are calendar months at the
21
+ * facility, so a UTC parse would shift the boundaries for facilities east of
22
+ * Greenwich and pull a day of data in from the neighbouring month.
23
+ */
24
+ export function monthToDateRange(month: string): { startDate: string; endDate: string } {
25
+ const start = dayjs(`${month}-01`).startOf('month');
26
+
27
+ return {
28
+ startDate: start.format(DATE_FORMAT),
29
+ endDate: start.endOf('month').format(DATE_FORMAT),
30
+ };
31
+ }
32
+
33
+ export function describePeriod(startDate: string, endDate: string, month?: string): string {
34
+ if (month) {
35
+ return dayjs(`${month}-01`).format('MMMM YYYY');
36
+ }
37
+
38
+ return `${dayjs(startDate).format('DD MMM YYYY')} to ${dayjs(endDate).format('DD MMM YYYY')}`;
39
+ }
40
+
41
+ /** The raw field values the user has entered, before they resolve to a period */
42
+ export interface PeriodDraft {
43
+ /** `true` for the Custom range tab and for daily mode, `false` for Monthly */
44
+ usesDateRange: boolean;
45
+ /** `YYYY`, Monthly only */
46
+ year?: string;
47
+ /** `MM`, Monthly only */
48
+ month?: string;
49
+ /** `YYYY-MM-DD`, Custom range only */
50
+ startDate?: string;
51
+ /** `YYYY-MM-DD`, Custom range only */
52
+ endDate?: string;
53
+ }
54
+
55
+ /** Messages keyed by the field they belong against, so each shows its own */
56
+ export interface PeriodErrors {
57
+ year?: string;
58
+ month?: string;
59
+ startDate?: string;
60
+ endDate?: string;
61
+ }
62
+
63
+ /** Order the fields appear in, so the summary quotes the first thing to fix */
64
+ const FIELD_ORDER: Array<keyof PeriodErrors> = ['year', 'month', 'startDate', 'endDate'];
65
+
66
+ export function firstError(errors: PeriodErrors): string | undefined {
67
+ return FIELD_ORDER.map((field) => errors[field]).find(Boolean);
68
+ }
69
+
70
+ /**
71
+ * Validates whichever period the active tab is collecting.
72
+ *
73
+ * Each tab has its own failure modes and each message is attached to the field
74
+ * that caused it, so nothing is reported against a control the user cannot see
75
+ * on the tab they are on.
76
+ *
77
+ * The Monthly tab needs the future check in particular: the date picker
78
+ * enforces `maxDate` itself, but Year and Month are independent dropdowns, so a
79
+ * later month in the current year yields a period that has not happened yet.
80
+ */
81
+ export function validatePeriod({ usesDateRange, year, month, startDate, endDate }: PeriodDraft): PeriodErrors {
82
+ const errors: PeriodErrors = {};
83
+ const today = dayjs();
84
+
85
+ if (usesDateRange) {
86
+ const start = dayjs(startDate);
87
+ const end = dayjs(endDate);
88
+ const hasStart = Boolean(startDate) && start.isValid();
89
+ const hasEnd = Boolean(endDate) && end.isValid();
90
+
91
+ if (!hasStart) {
92
+ errors.startDate = 'Enter a start date as dd/mm/yyyy.';
93
+ }
94
+
95
+ if (!hasEnd) {
96
+ errors.endDate = 'Enter an end date as dd/mm/yyyy.';
97
+ }
98
+
99
+ if (hasStart && hasEnd && start.isAfter(end, 'day')) {
100
+ errors.startDate = 'The start date must be on or before the end date.';
101
+ }
102
+
103
+ if (hasStart && !errors.startDate && start.isAfter(today, 'day')) {
104
+ errors.startDate = 'The start date cannot be in the future.';
105
+ }
106
+
107
+ if (hasEnd && end.isAfter(today, 'day')) {
108
+ errors.endDate = 'The end date cannot be in the future.';
109
+ }
110
+
111
+ return errors;
112
+ }
113
+
114
+ if (!year) {
115
+ errors.year = 'Choose a year.';
116
+ }
117
+
118
+ if (!month) {
119
+ errors.month = 'Choose a month.';
120
+ }
121
+
122
+ if (year && month) {
123
+ // dayjs rolls an out-of-range month over into the next year rather than
124
+ // reporting it invalid, so `2026-13` would read as a future date instead of
125
+ // a malformed one. Check the parts before trusting the parse.
126
+ const monthNumber = Number(month);
127
+ const isWellFormed = /^\d{4}$/.test(year) && Number.isInteger(monthNumber) && monthNumber >= 1 && monthNumber <= 12;
128
+
129
+ if (!isWellFormed) {
130
+ errors.month = 'That is not a valid month.';
131
+ } else if (dayjs(`${year}-${month}-01`).isAfter(today, 'month')) {
132
+ errors.month = 'That month has not started yet.';
133
+ }
134
+ }
135
+
136
+ return errors;
137
+ }
@@ -0,0 +1,4 @@
1
+ export { default as ReportFiltersComponent } from './report-filters.component';
2
+ export * from './date-range';
3
+ export * from './types';
4
+ export * from './use-restore-period';
@@ -1,124 +1,321 @@
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
-
44
- const generateReport = () => {
45
- if (onGenerate) {
46
- const filters: { startDate?: string; endDate?: string; month?: string } = {};
47
- if (mode === 'daily') {
48
- filters.startDate = startDateString;
49
- filters.endDate = endDateString;
50
- } else if (mode === 'monthly') {
51
- filters.month = monthString;
52
- }
53
- 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));
54
166
  }
55
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
+
56
197
  return (
57
- <>
58
- <div className={styles.titleContainer}>
59
- <ArrowLeft className={styles.backIcon} onClick={() => navigate(-1)} />
60
- <h2 className={styles.title}>{reportName}</h2>
61
- </div>
62
- <div className={styles.filtersContainer}>
63
- {mode === 'daily' && (
64
- <div className={styles.dateContainer}>
65
- <label htmlFor="startingMonth">Start Date:</label>
66
- <label htmlFor="endingMonth">End Date:</label>
67
-
68
- <input
69
- id="startingMonth"
70
- type="date"
71
- className={styles.input}
72
- value={startDateString}
73
- onChange={(e) => setStartDateString(e.target.value)}
74
- />
75
-
76
- <input
77
- id="endingMonth"
78
- type="date"
79
- className={styles.input}
80
- value={endDateString}
81
- onChange={(e) => setEndDateString(e.target.value)}
82
- />
83
- </div>
84
- )}
85
- {mode === 'monthly' && (
86
- <div>
87
- <div>
88
- <div>
89
- <label htmlFor="month">Month:</label>
90
- <input
91
- id="month"
92
- type="month"
93
- className={styles.input}
94
- value={monthString}
95
- onChange={(e) => setMonthString(e.target.value)}
96
- />
97
- </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>
98
224
  </div>
99
- <div className={styles.clearFix}></div>
100
- </div>
101
- )}
102
- {isFacility === 'true' && (
103
- <div>
104
- <label>Facility</label>
105
- <select
106
- className={styles.input}
107
- 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}
108
268
  value={selectedFacility}
109
- onChange={(e) => setSelectedFacility(e.target.value)}
269
+ onChange={(event) => setSelectedFacility(event.target.value)}
110
270
  >
111
- <option value="">{facility}</option>
112
- </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. Each message is the icon
282
+ plus one span: left as loose text and <strong> in the flex row,
283
+ every fragment became a column of its own and wrapped apart. */}
284
+ {isValid && isStale ? (
285
+ <p className={classNames(styles.summary, styles.summaryStale)}>
286
+ <WarningAlt size={16} />
287
+ <span>
288
+ Filters changed. The report below is for <strong>{shownPeriod?.label}</strong> &mdash; select Generate
289
+ report to refresh it.
290
+ </span>
291
+ </p>
292
+ ) : isValid ? (
293
+ <p className={styles.summary}>
294
+ <Calendar size={16} />
295
+ <span>
296
+ Reporting period: <strong>{period.label}</strong>
297
+ </span>
298
+ </p>
299
+ ) : (
300
+ <p className={classNames(styles.summary, styles.summaryInvalid)}>
301
+ <WarningAlt size={16} />
302
+ <span>{firstError(errors)}</span>
303
+ </p>
304
+ )}
305
+ <div className={styles.actionButtons}>
306
+ {/* Only offered once there is something on screen to put on paper */}
307
+ {isReportGenerated && !isLoading && (
308
+ <Button kind="tertiary" size="sm" renderIcon={Download} onClick={() => window.print()}>
309
+ Download PDF
310
+ </Button>
311
+ )}
312
+ <Button size="sm" renderIcon={ChartColumn} onClick={handleGenerate} disabled={isLoading || !isValid}>
313
+ {isLoading ? 'Generating…' : 'Generate report'}
314
+ </Button>
113
315
  </div>
114
- )}
115
- </div>
116
- {!isLoding && (
117
- <div className={styles.buttonContainer}>
118
- <Button onClick={generateReport}>Generate Report</Button>
119
316
  </div>
120
- )}
121
- </>
317
+ </Layer>
318
+ </div>
122
319
  );
123
320
  };
124
321