@ampath/esm-reports-app 1.0.0-next.7 → 1.0.0-next.70

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 (258) 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/299.js +2 -0
  9. package/dist/299.js.LICENSE.txt +3 -0
  10. package/dist/299.js.map +1 -0
  11. package/dist/300.js +1 -1
  12. package/dist/311.js +1 -1
  13. package/dist/311.js.map +1 -1
  14. package/dist/335.js +1 -1
  15. package/dist/353.js +1 -1
  16. package/dist/353.js.map +1 -1
  17. package/dist/354.js +2 -0
  18. package/dist/354.js.LICENSE.txt +20 -0
  19. package/dist/354.js.map +1 -0
  20. package/dist/410.js +1 -0
  21. package/dist/410.js.map +1 -0
  22. package/dist/457.js +2 -0
  23. package/dist/457.js.LICENSE.txt +261 -0
  24. package/dist/457.js.map +1 -0
  25. package/dist/478.js +1 -1
  26. package/dist/478.js.map +1 -1
  27. package/dist/480.js +1 -0
  28. package/dist/540.js +1 -1
  29. package/dist/540.js.map +1 -1
  30. package/dist/55.js +1 -1
  31. package/dist/580.js +1 -0
  32. package/dist/580.js.map +1 -0
  33. package/dist/600.js +2 -0
  34. package/dist/600.js.LICENSE.txt +35 -0
  35. package/dist/600.js.map +1 -0
  36. package/dist/652.js +1 -1
  37. package/dist/781.js +1 -0
  38. package/dist/781.js.map +1 -0
  39. package/dist/919.css +1 -0
  40. package/dist/919.css.map +1 -0
  41. package/dist/919.js +1 -0
  42. package/dist/919.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 +255 -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 +59 -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 +27 -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 +173 -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-362/moh-362-lab-register.component.tsx +392 -0
  100. package/src/registers/moh-362/moh-362-linkage-register.component.tsx +120 -0
  101. package/src/registers/moh-362/moh-362-register.scss +222 -0
  102. package/src/registers/moh-362/moh-362.component.tsx +99 -0
  103. package/src/registers/moh-362/moh-362.scss +11 -0
  104. package/src/registers/moh-362/moh-362.types.ts +97 -0
  105. package/src/registers/register-catalog.ts +80 -0
  106. package/src/registers/tb-lab-register/tb-lab-register-table.component.tsx +116 -0
  107. package/src/registers/tb-lab-register/tb-lab-register-table.scss +123 -0
  108. package/src/registers/tb-lab-register/tb-lab-register.columns.ts +164 -0
  109. package/src/registers/tb-lab-register/tb-lab-register.component.test.tsx +232 -0
  110. package/src/registers/tb-lab-register/tb-lab-register.component.tsx +153 -0
  111. package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +63 -48
  112. package/src/reports/moh-240/moh-240-header.component.tsx +62 -0
  113. package/src/reports/moh-240/moh-240.columns.ts +113 -0
  114. package/src/reports/moh-240/moh-240.component.tsx +72 -9
  115. package/src/reports/moh-240/moh-240.scss +40 -0
  116. package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +31 -37
  117. package/src/reports/moh-240/sub-reports/page-summary.component.tsx +143 -106
  118. package/src/reports/moh-505/moh-505-header.component.tsx +67 -0
  119. package/src/reports/moh-505/moh-505.component.tsx +441 -0
  120. package/src/reports/moh-505/moh-505.scss +117 -0
  121. package/src/reports/moh-705B/moh-204b-register.component.tsx +244 -0
  122. package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
  123. package/src/reports/moh-705B/moh-705b.component.tsx +659 -0
  124. package/src/reports/moh-705B/moh-705b.scss +172 -0
  125. package/src/reports/moh-705a/moh-705a.component.tsx +661 -0
  126. package/src/reports/moh-705a/moh-705a.scss +172 -0
  127. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +329 -0
  128. package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
  129. package/src/reports/moh-705a/type.ts +6 -0
  130. package/src/reports/moh-706/moh-706-header.component.tsx +66 -0
  131. package/src/reports/moh-706/moh-706.component.tsx +150 -24
  132. package/src/reports/moh-706/moh-706.scss +119 -11
  133. package/src/reports/moh-706/sub-reports/bacteriology/bacteriology.component.tsx +54 -62
  134. package/src/reports/moh-706/sub-reports/blood-chemistry/blood-chemistry.component.tsx +228 -317
  135. package/src/reports/moh-706/sub-reports/drug-susceptibility-testing/drug-susceptibility-testing.component.tsx +31 -46
  136. package/src/reports/moh-706/sub-reports/haematology/haematology.component.tsx +47 -55
  137. package/src/reports/moh-706/sub-reports/histology-and-cytology/histology-and-cytology.component.tsx +43 -58
  138. package/src/reports/moh-706/sub-reports/parasitology/parasitology.component.tsx +31 -39
  139. package/src/reports/moh-706/sub-reports/serology/serology.component.tsx +26 -34
  140. package/src/reports/moh-706/sub-reports/specimen-referral-to-higher-levels/specimen-referral-to-higher-levels.component.tsx +9 -24
  141. package/src/reports/moh-706/sub-reports/urine-analysis/urine-analysis.component.tsx +43 -51
  142. package/src/reports/moh-710/moh-710.component.tsx +799 -632
  143. package/src/reports/moh-710/moh-710.scss +142 -21
  144. package/src/reports/moh-710/registers/moh-510/moh-510.columns.ts +40 -0
  145. package/src/reports/moh-710/registers/moh-510/moh-510.component.tsx +144 -0
  146. package/src/reports/moh-710/registers/moh-510/moh-510.scss +130 -0
  147. package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +268 -0
  148. package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
  149. package/src/reports/moh-710/registers/type.ts +31 -0
  150. package/src/reports/moh-711/moh-711.component.tsx +138 -1269
  151. package/src/reports/moh-711/moh711.scss +135 -20
  152. package/src/reports/moh-711/registers/moh-333-register.component.tsx +637 -0
  153. package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
  154. package/src/reports/moh-711/registers/moh-405-register.component.tsx +520 -0
  155. package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
  156. package/src/reports/moh-711/registers/moh-406-register.component.tsx +530 -0
  157. package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
  158. package/src/reports/moh-711/registers/moh-510-register.component.tsx +55 -0
  159. package/src/reports/moh-711/registers/moh-511-register.component.tsx +270 -0
  160. package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
  161. package/src/reports/moh-711/registers/type.ts +192 -0
  162. package/src/reports/moh-711/sections/anc.component.tsx +159 -0
  163. package/src/reports/moh-711/sections/cervical-cancer.component.tsx +148 -0
  164. package/src/reports/moh-711/sections/chanis.component.tsx +366 -0
  165. package/src/reports/moh-711/sections/family-planning.component.tsx +221 -0
  166. package/src/reports/moh-711/sections/gbv.component.tsx +115 -0
  167. package/src/reports/moh-711/sections/maternity.component.tsx +321 -0
  168. package/src/reports/moh-711/sections/medical-social-work.component.tsx +73 -0
  169. package/src/reports/moh-711/sections/other.component.tsx +47 -0
  170. package/src/reports/moh-711/sections/physiotherapy.component.tsx +61 -0
  171. package/src/reports/moh-711/sections/pnc.component.tsx +127 -0
  172. package/src/reports/moh-711/sections/post-abortion.component.tsx +42 -0
  173. package/src/reports/moh-711/sections/rehabilitation.component.tsx +57 -0
  174. package/src/reports/moh-711/sections/report-compiled-by.component.tsx +50 -0
  175. package/src/reports/moh-711/sections/tb-screening.component.tsx +57 -0
  176. package/src/reports/moh-717/moh-717.component.tsx +149 -985
  177. package/src/reports/moh-717/moh717.scss +217 -21
  178. package/src/reports/moh-717/registers/nutrition-register.component.tsx +48 -0
  179. package/src/reports/moh-717/sections/finance.component.tsx +42 -0
  180. package/src/reports/moh-717/sections/inpatient.component.tsx +374 -0
  181. package/src/reports/moh-717/sections/maternity.component.tsx +126 -0
  182. package/src/reports/moh-717/sections/medical-records.component.tsx +35 -0
  183. package/src/reports/moh-717/sections/mortuary.component.tsx +43 -0
  184. package/src/reports/moh-717/sections/operations.component.tsx +53 -0
  185. package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +98 -0
  186. package/src/reports/moh-717/sections/outpatient.component.tsx +597 -0
  187. package/src/reports/moh-717/sections/pharmacy.component.tsx +43 -0
  188. package/src/reports/moh-717/sections/preparedby.component.tsx +52 -0
  189. package/src/reports/moh-717/sections/special-services.component.tsx +114 -0
  190. package/src/reports/moh-731/linelist/moh-731-indicators.ts +1122 -0
  191. package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +472 -0
  192. package/src/reports/moh-731/linelist/moh-731-linelist.component.tsx +102 -0
  193. package/src/reports/moh-731/linelist/use-open-moh-731-linelist.ts +50 -0
  194. package/src/reports/moh-731/moh-731.component.tsx +266 -0
  195. package/src/reports/moh-731/moh-731.scss +499 -0
  196. package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
  197. package/src/reports/moh-731/registers/moh-366.component.tsx +471 -0
  198. package/src/reports/moh-731/registers/prep-register.component.tsx +284 -0
  199. package/src/reports/moh-731/registers/prep.columns.ts +34 -0
  200. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1206 -0
  201. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +727 -0
  202. package/src/reports/moh-731/sections/pmtct.component.tsx +572 -0
  203. package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
  204. package/src/reports/moh-731/summary/moh-731-summary.config.ts +529 -0
  205. package/src/reports/moh-731/summary/moh-731-summary.scss +242 -0
  206. package/src/reports/moh-740/moh-740.component.scss +202 -0
  207. package/src/reports/moh-740/moh-740.component.tsx +1332 -0
  208. package/src/reports/moh-740/moh-740.resource.ts +54 -0
  209. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +149 -0
  210. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +123 -0
  211. package/src/reports/moh-740/registers/moh-740-patient-list.scss +0 -0
  212. package/src/reports/moh-740/registers/moh-740-patient-list.tsx +60 -0
  213. package/src/reports/moh-740/registers/moh-740.columns.ts +54 -0
  214. package/src/reports/moh-740/shared/data-cell/data-cell.scss +6 -0
  215. package/src/reports/moh-740/shared/data-cell/data-cell.tsx +23 -0
  216. package/src/reports/moh-740/shared/utils/indicator-register-map.ts +159 -0
  217. package/src/reports/moh-740/types/index.ts +223 -0
  218. package/src/reports/moh-745/moh-745.component.tsx +1021 -0
  219. package/src/reports/moh-745/moh-745.scss +132 -0
  220. package/src/reports/moh-745/registers/moh-412-register.component.tsx +199 -0
  221. package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
  222. package/src/reports/moh-745/registers/type.ts +181 -0
  223. package/src/reports/table-wrapper/table-row-mapper.component.tsx +57 -0
  224. package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
  225. package/src/reports/table-wrapper/table-wrapper.scss +1 -16
  226. package/src/resources/moh-362.resource.ts +27 -0
  227. package/src/resources/moh-505.resource.ts +69 -0
  228. package/src/resources/moh-705.resource.ts +106 -0
  229. package/src/resources/moh-706.resource.ts +80 -0
  230. package/src/resources/moh-710.resource.ts +32 -9
  231. package/src/resources/moh-711.resource.ts +137 -9
  232. package/src/resources/moh-717.resource.ts +26 -9
  233. package/src/resources/moh-731.resource.ts +90 -0
  234. package/src/resources/moh-745.resource.ts +76 -0
  235. package/src/resources/patient-list-page.ts +65 -0
  236. package/src/root.component.tsx +59 -8
  237. package/src/root.scss +36 -0
  238. package/src/routes.json +3 -1
  239. package/src/utils/format-indicator.ts +39 -0
  240. package/src/utils/get-base-url.ts +39 -7
  241. package/src/utils/utils.ts +36 -0
  242. package/tsconfig.json +1 -1
  243. package/webpack.config.js +22 -1
  244. package/dist/138.js +0 -2
  245. package/dist/138.js.LICENSE.txt +0 -20
  246. package/dist/138.js.map +0 -1
  247. package/dist/184.js +0 -2
  248. package/dist/184.js.LICENSE.txt +0 -14
  249. package/dist/184.js.map +0 -1
  250. package/dist/205.js +0 -1
  251. package/dist/205.js.map +0 -1
  252. package/dist/711.js +0 -1
  253. package/dist/711.js.map +0 -1
  254. package/dist/812.js +0 -1
  255. package/dist/812.js.map +0 -1
  256. package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -539
  257. package/dist/main.js +0 -1
  258. package/dist/main.js.map +0 -1
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Whether a report has been generated and came back with something to show.
3
+ *
4
+ * Reports flatten their response with `Object.assign({}, ...data.result)`, but
5
+ * they initialise that state variously as `{}`, `[]` or a value restored from
6
+ * sessionStorage, so "has it run yet" is really "does it have any keys". An
7
+ * empty array has none, which is what makes this work across all of them.
8
+ */
9
+ export function hasReportData(data: unknown): boolean {
10
+ if (!data || typeof data !== 'object') {
11
+ return false;
12
+ }
13
+
14
+ return Object.keys(data).length > 0;
15
+ }
@@ -0,0 +1,3 @@
1
+ export { default as ReportPlaceholder } from './report-placeholder.component';
2
+ export * from './has-report-data';
3
+ export * from './use-generated-location';
@@ -0,0 +1,81 @@
1
+ import React from 'react';
2
+ import { Layer } from '@carbon/react';
3
+ import { Location, Report } from '@carbon/react/icons';
4
+ import { useSession } from '@openmrs/esm-framework';
5
+
6
+ import styles from './report-placeholder.scss';
7
+
8
+ interface ReportPlaceholderProps {
9
+ /** Named so the guidance says which return is about to be produced */
10
+ reportName?: string;
11
+ /** Shown when a report was generated but came back with nothing in it */
12
+ isEmptyResult?: boolean;
13
+ /** A report was generated, but for another location than the current one */
14
+ isLocationChanged?: boolean;
15
+ }
16
+
17
+ /**
18
+ * Stands in for the report template until a report has actually been generated,
19
+ * and explains how to produce one.
20
+ *
21
+ * Report templates are large pre-printed MOH forms; rendering one with every
22
+ * value blank reads as "the facility reported zeroes" rather than "you have not
23
+ * run this yet", so the template is held back until there is data to put in it.
24
+ * That leaves the page empty, hence the guidance here.
25
+ */
26
+ const ReportPlaceholder: React.FC<ReportPlaceholderProps> = ({
27
+ reportName,
28
+ isEmptyResult = false,
29
+ isLocationChanged = false,
30
+ }) => {
31
+ const session = useSession();
32
+ const location = session?.sessionLocation?.display;
33
+
34
+ return (
35
+ <Layer className={styles.placeholder}>
36
+ {isLocationChanged ? (
37
+ <Location size={32} className={styles.icon} />
38
+ ) : (
39
+ <Report size={32} className={styles.icon} />
40
+ )}
41
+
42
+ {isLocationChanged ? (
43
+ <>
44
+ <h5>Location changed</h5>
45
+ <p>
46
+ The last {reportName ?? 'report'} was generated for a different location, so it is not shown here. Select{' '}
47
+ <strong>Generate report</strong> to produce it for{' '}
48
+ {location ? <strong>{location}</strong> : 'this location'}.
49
+ </p>
50
+ </>
51
+ ) : isEmptyResult ? (
52
+ <>
53
+ <h5>No records for this period</h5>
54
+ <p>
55
+ {reportName ?? 'The report'} ran successfully but returned no data for the period you selected. Try a
56
+ different reporting period, or confirm that data has been entered for this facility.
57
+ </p>
58
+ </>
59
+ ) : (
60
+ <>
61
+ <h5>No report generated yet</h5>
62
+ <p>
63
+ {reportName ? `The ${reportName} form` : 'The report'} will appear here, filled in with your
64
+ facility&rsquo;s figures, once you generate it.
65
+ </p>
66
+ <ol className={styles.steps}>
67
+ <li>Choose the reporting period in the filters above.</li>
68
+ <li>
69
+ Select <strong>Generate report</strong>.
70
+ </li>
71
+ <li>
72
+ Use <strong>Download PDF</strong> to save or print the completed form.
73
+ </li>
74
+ </ol>
75
+ </>
76
+ )}
77
+ </Layer>
78
+ );
79
+ };
80
+
81
+ export default ReportPlaceholder;
@@ -0,0 +1,48 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .placeholder {
6
+ display: flex;
7
+ flex-direction: column;
8
+ align-items: center;
9
+ gap: layout.$spacing-03;
10
+ padding: layout.$spacing-07 layout.$spacing-05;
11
+ /* the same gap below the filter card as a generated report's panel has */
12
+ margin-top: layout.$spacing-05;
13
+ margin-bottom: layout.$spacing-05;
14
+ text-align: center;
15
+ background-color: $ui-02;
16
+ border: 1px solid $ui-03;
17
+
18
+ h5 {
19
+ @include type.type-style('productive-heading-02');
20
+ color: $color-gray-100;
21
+ }
22
+
23
+ p {
24
+ @include type.type-style('body-long-01');
25
+ color: $text-02;
26
+ }
27
+ }
28
+
29
+ .icon {
30
+ fill: $ui-04;
31
+ }
32
+
33
+ /* The steps that produce a report. Left aligned inside the centred card: a
34
+ numbered sequence is read down its left edge, not from a ragged centre. */
35
+
36
+ .steps {
37
+ max-width: 28rem;
38
+ margin-top: layout.$spacing-03;
39
+ padding-left: layout.$spacing-06;
40
+ text-align: left;
41
+ list-style: decimal;
42
+
43
+ li {
44
+ @include type.type-style('body-long-01');
45
+ color: $text-02;
46
+ margin-bottom: layout.$spacing-02;
47
+ }
48
+ }
@@ -0,0 +1,62 @@
1
+ import { useCallback, useState } from 'react';
2
+ import { useSession } from '@openmrs/esm-framework';
3
+
4
+ function readLocation(storageKey?: string): string {
5
+ if (!storageKey) {
6
+ return '';
7
+ }
8
+ try {
9
+ return sessionStorage.getItem(storageKey) ?? '';
10
+ } catch {
11
+ return '';
12
+ }
13
+ }
14
+
15
+ function writeLocation(storageKey: string | undefined, locationUuid: string) {
16
+ if (!storageKey) {
17
+ return;
18
+ }
19
+ try {
20
+ sessionStorage.setItem(storageKey, locationUuid);
21
+ } catch {
22
+ /* storage unavailable: the report just won't survive a reload */
23
+ }
24
+ }
25
+
26
+ /**
27
+ * Which location the report on screen was generated for, compared with the
28
+ * one the user is at now.
29
+ *
30
+ * A report's figures belong to the location they were fetched for, but its
31
+ * masthead names the session location. After switching location, the old
32
+ * figures would be shown -- and labelled -- as the new location's. So a report
33
+ * only counts as generated while it is for the current location; otherwise the
34
+ * page falls back to the placeholder and asks for it to be generated again.
35
+ *
36
+ * Reports that keep their figures in session storage pass `storageKey`, so the
37
+ * location is restored alongside them. Figures restored without a location
38
+ * (saved before this was recorded) are treated as not generated, rather than
39
+ * assumed to be for wherever the user happens to be.
40
+ */
41
+ export function useGeneratedLocation(storageKey?: string) {
42
+ const session = useSession();
43
+ const currentLocation = session?.sessionLocation?.uuid ?? '';
44
+ const [generatedLocation, setGeneratedLocation] = useState(() => readLocation(storageKey));
45
+
46
+ /** Call once a report has been fetched, with the location it was fetched for */
47
+ const markGenerated = useCallback(
48
+ (locationUuid: string) => {
49
+ setGeneratedLocation(locationUuid);
50
+ writeLocation(storageKey, locationUuid);
51
+ },
52
+ [storageKey],
53
+ );
54
+
55
+ return {
56
+ /** The report held is for the location the user is at now */
57
+ isForCurrentLocation: Boolean(generatedLocation) && generatedLocation === currentLocation,
58
+ /** A report is held, but for a different location than the current one */
59
+ hasLocationChanged: Boolean(generatedLocation) && generatedLocation !== currentLocation,
60
+ markGenerated,
61
+ };
62
+ }
@@ -0,0 +1,6 @@
1
+ export { default as RegisterLayout } from './register-layout.component';
2
+ export * from './register-columns';
3
+ export * from './report-parents';
4
+ export * from './rows-to-csv';
5
+ export * from './use-patient-list';
6
+ export * from './use-location-name';
@@ -0,0 +1,46 @@
1
+ /** One exported column: the heading it carries on the register, and its value */
2
+ export interface RegisterColumn {
3
+ header: string;
4
+ value: (row: Record<string, any>, index: number) => unknown;
5
+ /**
6
+ * Keep the cell as text in the CSV so Excel never reinterprets it:
7
+ * identifiers lose leading zeros and phone numbers become `#NAME?`
8
+ * formulas when the spreadsheet parses them as numbers.
9
+ */
10
+ text?: boolean;
11
+ /**
12
+ * Allow the cell to wrap on screen. Registers keep their columns
13
+ * `nowrap` so short cells stay compact; prose columns (diagnoses,
14
+ * referrals, comments) would otherwise stretch the whole row wide.
15
+ */
16
+ wrap?: boolean;
17
+ }
18
+
19
+ /** The age bands the registers tabulate against, in the order they appear */
20
+ export const AGE_BANDS = [
21
+ { label: '< 1 yrs', min: 0, max: 0 },
22
+ { label: '1-4 yrs', min: 1, max: 4 },
23
+ { label: '5-9 yrs', min: 5, max: 9 },
24
+ { label: '10-14 yrs', min: 10, max: 14 },
25
+ { label: '15-19 yrs', min: 15, max: 19 },
26
+ { label: '20-24 yrs', min: 20, max: 24 },
27
+ { label: '25+ yrs', min: 25, max: Number.POSITIVE_INFINITY },
28
+ ] as const;
29
+
30
+ /**
31
+ * Expands one indicator into the fourteen age-band by gender columns the
32
+ * register prints for it.
33
+ *
34
+ * The registers spell these out as fourteen near-identical cells; describing
35
+ * the rule once keeps the export in step with the table without restating it
36
+ * fourteen times per block.
37
+ */
38
+ export function ageBandColumns(heading: string, value: (row: Record<string, any>) => unknown): RegisterColumn[] {
39
+ return AGE_BANDS.flatMap((band) =>
40
+ (['M', 'F'] as const).map((gender) => ({
41
+ header: `${heading} ${band.label} (${gender})`,
42
+ value: (row: Record<string, any>) =>
43
+ row.age >= band.min && row.age <= band.max && row.gender === gender ? value(row) : '',
44
+ })),
45
+ );
46
+ }
@@ -0,0 +1,265 @@
1
+ import React from 'react';
2
+ import { Breadcrumb, BreadcrumbItem, Button, Layer, Pagination } from '@carbon/react';
3
+ import { Link, useSearchParams } from 'react-router-dom';
4
+ import { ArrowLeft, Calendar, Document, Download, Location } from '@carbon/react/icons';
5
+ import { formatDate } from '@openmrs/esm-framework';
6
+
7
+ import { formatIndicatorName } from '../../utils/format-indicator';
8
+ import { showReportError } from '../report-error';
9
+ import { columnsToCsv, downloadCsv, rowsToCsv } from './rows-to-csv';
10
+ import { type RegisterColumn } from './register-columns';
11
+ import { REPORT_PARENTS } from './report-parents';
12
+
13
+ import { RegisterSkeleton } from '../report-skeleton';
14
+ import styles from './register-layout.scss';
15
+
16
+ interface RegisterLayoutProps {
17
+ /** The report this register was opened from, e.g. `MOH-731 Report` */
18
+ parentLabel: string;
19
+ /** Route back to that report, e.g. `/moh-731` */
20
+ parentPath?: string;
21
+ /**
22
+ * Returns to the report in place, for a register that is a view on the
23
+ * report's own route rather than a route of its own.
24
+ *
25
+ * MOH-740 toggles its registers in place, so there is nowhere to navigate:
26
+ * a breadcrumb would link the page to itself. Given this, the trail is
27
+ * replaced by a button that puts the report back.
28
+ */
29
+ onBack?: () => void;
30
+ /** Register name, e.g. `HIV Care Treatment Daily Activity Register MOH 366` */
31
+ title: string;
32
+ /**
33
+ * The page heading, when the register can name the figure itself. Without
34
+ * it the heading is built from the link's `label` and `gender`.
35
+ */
36
+ heading?: string;
37
+ /** The location the register was generated for, shown beside the period */
38
+ location?: string;
39
+ isLoading: boolean;
40
+ /** `true` once loaded with nothing to show */
41
+ isEmpty: boolean;
42
+ /** Paging state from `usePatientList` */
43
+ page?: number;
44
+ pageSize?: number;
45
+ total?: number;
46
+ /** `false` when the total is a lower bound the endpoint did not confirm */
47
+ isTotalExact?: boolean;
48
+ onPageChange?: (change: { page: number; pageSize: number }) => void;
49
+ /** Every row across every page, used to build the export */
50
+ fetchAll?: () => Promise<Array<Record<string, any>>>;
51
+ /** The register's own columns. Without them the export falls back to the
52
+ response's raw fields, which will not match the printed form. */
53
+ columns?: RegisterColumn[];
54
+ /**
55
+ * The printed form's own sub-headings (e.g. its column letters), written as
56
+ * a second CSV row so exports stay line-numbered against the paper form.
57
+ */
58
+ subHeaders?: string[];
59
+ /** The register table. Optional: MOH 510 has no table implemented yet. */
60
+ children?: React.ReactNode;
61
+ }
62
+
63
+ /**
64
+ * The frame every drill-down register renders inside.
65
+ *
66
+ * Registers are opened from a value box on a report, so the trail back matters
67
+ * more than a bare Back button: the breadcrumb shows where the figure came
68
+ * from and lets the user step to either level. The table itself is wide -- the
69
+ * MOH 366 register runs to 41 columns -- so it scrolls within the page rather
70
+ * than forcing the whole layout sideways.
71
+ */
72
+ const GENDERS: Record<string, string> = { M: 'Male', F: 'Female' };
73
+
74
+ const RegisterLayout: React.FC<RegisterLayoutProps> = ({
75
+ parentLabel,
76
+ parentPath,
77
+ onBack,
78
+ title,
79
+ heading: headingProp,
80
+ location,
81
+ isLoading,
82
+ isEmpty,
83
+ page,
84
+ pageSize,
85
+ total,
86
+ isTotalExact = true,
87
+ onPageChange,
88
+ fetchAll,
89
+ columns,
90
+ subHeaders,
91
+ children,
92
+ }) => {
93
+ /* The same register serves every indicator that links to it, so without this
94
+ each drill-down looks identical no matter which box was clicked. */
95
+ const [params] = useSearchParams();
96
+ const [isExporting, setIsExporting] = React.useState(false);
97
+ const indicator = params.get('indicator') ?? params.get('indicators');
98
+ /* The report passes the row label it displayed, so the crumb reads the way
99
+ the figure was labelled on the form. `formatIndicatorName` is the fallback
100
+ for links made before the label was carried through. */
101
+ const label = params.get('label');
102
+ const gender = params.get('gender');
103
+ const startDate = params.get('startDate');
104
+ const endDate = params.get('endDate');
105
+ const locationUuids = params.get('locationUuids');
106
+
107
+ /* Where the drill-down came from. Registers shared by two reports cannot
108
+ name their parent as a constant, so the link says which one it was; the
109
+ props remain the fallback for links made before it was carried. */
110
+ const from = params.get('from');
111
+ const parent = (from && REPORT_PARENTS[from]) || { label: parentLabel, path: parentPath ?? '' };
112
+
113
+ /* The crumb has to carry the period, not just the path: a report that does
114
+ not keep its figures in session storage regenerates from its URL, and
115
+ linking to the bare path would land the user on the placeholder instead of
116
+ the figures the register was opened from. */
117
+ const parentQuery = new URLSearchParams();
118
+ if (locationUuids) parentQuery.append('locationUuids', locationUuids);
119
+ if (startDate) parentQuery.append('startDate', startDate);
120
+ if (endDate) parentQuery.append('endDate', endDate);
121
+ /* back to the report tab the register was opened from, e.g. a summary view */
122
+ const view = params.get('view');
123
+ if (view) parentQuery.append('view', view);
124
+ const section = params.get('section');
125
+ if (section) parentQuery.append('section', section);
126
+ const parentLink = parentQuery.toString() ? `${parent.path}?${parentQuery}` : parent.path;
127
+
128
+ const heading =
129
+ headingProp ??
130
+ (label
131
+ ? `${label}${gender && GENDERS[gender] ? ` (${GENDERS[gender]})` : ''}`
132
+ : formatIndicatorName(indicator ?? ''));
133
+
134
+ /* Exports every page, not the one on screen: the DOM only holds the current
135
+ page once paginated, so a scrape would silently export 50 of 2,000. */
136
+ const exportCsv = async () => {
137
+ if (!fetchAll) {
138
+ return;
139
+ }
140
+
141
+ setIsExporting(true);
142
+
143
+ try {
144
+ const all = await fetchAll();
145
+ const slug = [title, label ?? indicator, startDate, endDate]
146
+ .filter(Boolean)
147
+ .join('-')
148
+ .replace(/[^a-z0-9]+/gi, '-')
149
+ .replace(/^-|-$/g, '')
150
+ .toLowerCase();
151
+
152
+ downloadCsv(columns ? columnsToCsv(all, columns, { subHeaderRow: subHeaders }) : rowsToCsv(all), `${slug}.csv`);
153
+ } catch (error) {
154
+ showReportError('the register export', error);
155
+ } finally {
156
+ setIsExporting(false);
157
+ }
158
+ };
159
+
160
+ const canExport = !isLoading && !isEmpty;
161
+ const pageSizeOrDefault = pageSize ?? 50;
162
+
163
+ const period =
164
+ startDate && endDate
165
+ ? `${formatDate(new Date(startDate), { mode: 'standard', time: false, noToday: true })} to ${formatDate(
166
+ new Date(endDate),
167
+ { mode: 'standard', time: false, noToday: true },
168
+ )}`
169
+ : null;
170
+
171
+ return (
172
+ <div className={styles.register}>
173
+ {onBack ? (
174
+ <div className={styles.breadcrumb}>
175
+ <Button kind="ghost" size="sm" renderIcon={ArrowLeft} onClick={onBack} className={styles.back}>
176
+ {parent.label}
177
+ </Button>
178
+ <h3 className={styles.title}>{heading || title}</h3>
179
+ </div>
180
+ ) : (
181
+ <Breadcrumb noTrailingSlash className={styles.breadcrumb}>
182
+ <BreadcrumbItem>
183
+ <Link to="/">All reports</Link>
184
+ </BreadcrumbItem>
185
+ <BreadcrumbItem>
186
+ <Link to={parentLink}>{parent.label}</Link>
187
+ </BreadcrumbItem>
188
+ <BreadcrumbItem isCurrentPage>
189
+ <h3 className={styles.title}>{heading || title}</h3>
190
+ </BreadcrumbItem>
191
+ </Breadcrumb>
192
+ )}
193
+
194
+ {/* Register, period and the export sit on one line: they all describe the
195
+ table below, and splitting them stacked two sparse rows above it. */}
196
+ {(indicator || period || canExport) && (
197
+ <div className={styles.context}>
198
+ {/* Register and period read as one caption for the table, so they
199
+ share a panel rather than sitting as two loose fragments. */}
200
+ <span className={styles.summary}>
201
+ <span className={styles.summaryTitle}>{title}</span>
202
+ <span className={styles.period}>
203
+ {period && (
204
+ <>
205
+ <Calendar size={14} />
206
+ {period}
207
+ </>
208
+ )}
209
+ {location && (
210
+ <span className={styles.location}>
211
+ <Location size={14} />
212
+ {location}
213
+ </span>
214
+ )}
215
+ {typeof total === 'number' && total > 0 && (
216
+ <span>{isTotalExact ? `${total.toLocaleString()} patients` : `${total.toLocaleString()}+ patients`}</span>
217
+ )}
218
+ </span>
219
+ </span>
220
+ {canExport && fetchAll && (
221
+ <Button
222
+ kind="tertiary"
223
+ size="sm"
224
+ renderIcon={Download}
225
+ onClick={exportCsv}
226
+ disabled={isExporting}
227
+ className={styles.export}
228
+ >
229
+ {isExporting ? 'Preparing…' : 'Export CSV'}
230
+ </Button>
231
+ )}
232
+ </div>
233
+ )}
234
+
235
+ {isLoading && <RegisterSkeleton />}
236
+
237
+ {!isLoading && isEmpty && (
238
+ <Layer className={styles.empty}>
239
+ <Document size={32} className={styles.emptyIcon} />
240
+ <h4>No records to list</h4>
241
+ <p>No clients matched this indicator for the selected period and facility.</p>
242
+ </Layer>
243
+ )}
244
+
245
+ {canExport && (
246
+ <>
247
+ <div className={styles.tableScroll}>{children}</div>
248
+ {onPageChange && typeof total === 'number' && total > pageSizeOrDefault && (
249
+ <Pagination
250
+ className={styles.pagination}
251
+ page={page}
252
+ pageSize={pageSizeOrDefault}
253
+ pageSizes={[25, 50, 100, 200]}
254
+ totalItems={total}
255
+ size="sm"
256
+ onChange={onPageChange}
257
+ />
258
+ )}
259
+ </>
260
+ )}
261
+ </div>
262
+ );
263
+ };
264
+
265
+ export default RegisterLayout;
@@ -0,0 +1,150 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .register {
6
+ padding: layout.$spacing-05 0 layout.$spacing-07;
7
+ }
8
+
9
+ .breadcrumb {
10
+ margin-bottom: layout.$spacing-05;
11
+
12
+ :global(.cds--breadcrumb-item) {
13
+ display: inline-flex;
14
+ align-items: baseline;
15
+ }
16
+ }
17
+
18
+ /* The register name doubles as the page heading, so it stays an `h3` rather
19
+ than being repeated in a title row of its own. */
20
+
21
+ .title {
22
+ @include type.type-style('productive-heading-03');
23
+ display: inline;
24
+ color: $color-gray-100;
25
+ }
26
+
27
+ /* Registers run to dozens of columns, so the table scrolls inside the page
28
+ instead of pushing the whole layout sideways. */
29
+
30
+ .tableScroll {
31
+ overflow-x: auto;
32
+ padding: layout.$spacing-05;
33
+ background-color: $ui-02;
34
+ border: 1px solid $ui-03;
35
+ border-top: none;
36
+
37
+ /* Each register carries its own table margins -- 1rem here, 0.5rem there,
38
+ 0.75rem elsewhere -- which land inside this frame and give every register a
39
+ different gap to the border. The padding above is the spacing now, so those
40
+ are levelled out. */
41
+
42
+ table,
43
+ > div {
44
+ margin: 0;
45
+ }
46
+ }
47
+
48
+
49
+ .empty {
50
+ display: flex;
51
+ flex-direction: column;
52
+ align-items: center;
53
+ gap: layout.$spacing-03;
54
+ padding: layout.$spacing-07 layout.$spacing-05;
55
+ text-align: center;
56
+ background-color: $ui-02;
57
+ border: 1px solid $ui-03;
58
+ border-top: none;
59
+
60
+ h4 {
61
+ @include type.type-style('productive-heading-02');
62
+ color: $color-gray-100;
63
+ }
64
+
65
+ p {
66
+ @include type.type-style('body-long-01');
67
+ color: $text-02;
68
+ }
69
+ }
70
+
71
+ .emptyIcon {
72
+ fill: $ui-04;
73
+ }
74
+
75
+ /* The caption and the table below it are one card: the bar carries the register
76
+ name, the period it covers and the export, and sits flush on whatever follows
77
+ -- skeleton, empty state or table -- so the page reads as a single object
78
+ rather than three stacked fragments. */
79
+
80
+ .context {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ gap: layout.$spacing-03 layout.$spacing-05;
86
+ padding: layout.$spacing-04 layout.$spacing-05;
87
+ background-color: $ui-02;
88
+ border: 1px solid $ui-03;
89
+ border-bottom: none;
90
+ }
91
+
92
+ /* Register name over the period it covers: side by side they made one long
93
+ line that crowded the action; stacked, the name leads and the period reads
94
+ as its caption. */
95
+
96
+ .summary {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: layout.$spacing-02;
100
+ min-width: 0;
101
+ }
102
+
103
+ .summaryTitle {
104
+ @include type.type-style('body-short-01');
105
+ font-weight: 600;
106
+ color: $color-gray-100;
107
+ }
108
+
109
+ .period {
110
+ @include type.type-style('label-01');
111
+ display: inline-flex;
112
+ flex-wrap: wrap;
113
+ align-items: center;
114
+ gap: layout.$spacing-02;
115
+ color: $text-02;
116
+
117
+ svg {
118
+ fill: var(--brand-01);
119
+ transform: translateY(0.125rem);
120
+ }
121
+ }
122
+
123
+ /* Location sits after the period as one unit, so on a narrow screen it wraps
124
+ to its own line whole rather than leaving its pin behind */
125
+ .location {
126
+ display: inline-flex;
127
+ align-items: center;
128
+ gap: layout.$spacing-02;
129
+ margin-left: layout.$spacing-03;
130
+ }
131
+
132
+ .export {
133
+ flex-shrink: 0;
134
+ margin-left: auto;
135
+ }
136
+
137
+ /* Sits directly under the table, completing the card */
138
+
139
+ .pagination {
140
+ border: 1px solid $ui-03;
141
+ border-top: none;
142
+ }
143
+
144
+ /* A register rendered as a view on its report's own route has nowhere to
145
+ navigate, so the trail is a button and the register name beside it. */
146
+
147
+ .back {
148
+ padding-inline: 0.25rem;
149
+ min-block-size: auto;
150
+ }