@ampath/esm-reports-app 1.0.0-next.8 → 1.0.0-next.80

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