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

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 +96 -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 +249 -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,71 @@
1
+ @use '@openmrs/esm-styleguide/src/vars' as *;
2
+
3
+ /* The one stylesheet every register table uses.
4
+ *
5
+ * Registers previously borrowed their parent report's sheet, so a report sized
6
+ * for an A4 page dragged its paper typography into its line lists, and the same
7
+ * table rules were restated in half a dozen files that drifted apart. These are
8
+ * screen tables: dense, scrollable, and independent of how their report prints.
9
+ */
10
+
11
+ .tableContainer {
12
+ width: 100%;
13
+ }
14
+
15
+ /* Sized to content rather than to the frame, so a long value -- a full name,
16
+ say -- widens its own column instead of wrapping. `.tableScroll` in the
17
+ layout scrolls when the table outgrows the page. */
18
+
19
+ .table {
20
+ width: auto;
21
+ min-width: 100%;
22
+ margin: 0;
23
+ font-size: 0.75rem;
24
+ border-collapse: collapse;
25
+ }
26
+
27
+ .tableBordered td,
28
+ .tableBordered th {
29
+ padding: 0.125rem 0.5rem;
30
+ font-size: 0.75rem;
31
+ line-height: 1.3;
32
+ border: 1px solid $ui-04;
33
+ }
34
+
35
+ .tableBordered th {
36
+ font-weight: 600;
37
+ color: $color-gray-100;
38
+ background-color: $ui-01;
39
+ /* headings carry deliberate line breaks, so they wrap where the markup says */
40
+ white-space: normal;
41
+ }
42
+
43
+ .tableBordered td {
44
+ white-space: nowrap;
45
+ }
46
+
47
+ /* Long prose columns (diagnoses, referrals, comments) wrap inside a bounded
48
+ width instead of stretching the row past the fold. */
49
+ .tableBordered td.wrapCell {
50
+ white-space: normal;
51
+ min-width: 8rem;
52
+ max-width: 16rem;
53
+ overflow-wrap: anywhere;
54
+ }
55
+
56
+ .tableStriped tbody tr:nth-of-type(odd) {
57
+ background-color: $ui-01;
58
+ }
59
+
60
+ .tableStriped tbody tr:hover {
61
+ background-color: $brand-primary-10;
62
+ }
63
+
64
+ .textCentre {
65
+ text-align: center;
66
+ }
67
+
68
+ .verticalText {
69
+ writing-mode: vertical-rl;
70
+ transform: rotate(180deg);
71
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The report each register can be opened from.
3
+ *
4
+ * Several registers serve more than one report -- MOH 333, 405 and 406 are
5
+ * reached from both MOH-711 and MOH-717, and MOH 412 from both MOH-711 and
6
+ * MOH-745. A register cannot name its parent as a constant, then: whichever
7
+ * report it named, the trail would be wrong for everyone arriving from the
8
+ * other one. The link carries `from`, and the trail is resolved from it.
9
+ */
10
+ export interface ReportParent {
11
+ label: string;
12
+ path: string;
13
+ }
14
+
15
+ export const REPORT_PARENTS: Record<string, ReportParent> = {
16
+ 'moh-240': { label: 'MOH-240 Register', path: '/moh-240' },
17
+ 'moh-505': { label: 'MOH-505 Report', path: '/moh-505' },
18
+ 'moh-705a': { label: 'MOH-705A Report', path: '/moh-705a' },
19
+ 'moh-705b': { label: 'MOH-705B Report', path: '/moh-705b' },
20
+ 'moh-706': { label: 'MOH-706 Report', path: '/moh-706' },
21
+ 'moh-710': { label: 'MOH-710 Report', path: '/moh-710' },
22
+ 'moh-711': { label: 'MOH-711 Report', path: '/moh-711' },
23
+ 'moh-717': { label: 'MOH-717 Report', path: '/moh-717' },
24
+ 'moh-731': { label: 'MOH-731 Report', path: '/moh-731' },
25
+ 'moh-740': { label: 'MOH-740 Report', path: '/moh-740' },
26
+ 'moh-745': { label: 'MOH-745 Report', path: '/moh-745' },
27
+ };
@@ -0,0 +1,89 @@
1
+ import { type RegisterColumn } from './register-columns';
2
+
3
+ /**
4
+ * Serialises line-list rows to CSV.
5
+ *
6
+ * Built from the data rather than from the rendered table: with pagination the
7
+ * DOM only ever holds one page, so scraping it would quietly export 50 rows
8
+ * while looking like it exported all 2,000 -- the worst way for an export to
9
+ * fail. The trade-off is that the file carries the API's own fields rather
10
+ * than the register's printed column headings, which is what a spreadsheet
11
+ * wants anyway.
12
+ */
13
+
14
+ /**
15
+ * Forces text Excel cannot mangle. A cell starting with `=`, `+`, `-` or `@`
16
+ * is parsed as a formula (`+2547...` becomes `#NAME?`), and a long digit-only
17
+ * cell is reinterpreted as a number (leading zeros dropped, switched to
18
+ * scientific notation), so both are emitted as an explicit text formula
19
+ * (`"..."` quoted inside `=`, which Excel renders as literal text).
20
+ */
21
+ const FORMULA_PREFIX = /^[=+\-@]/;
22
+ const LONG_DIGITS = /^\d{9,}$/;
23
+
24
+ function escapeCell(value: unknown, forceText = false): string {
25
+ const text = value === null || value === undefined ? '' : String(value).replace(/\s+/g, ' ').trim();
26
+
27
+ if (forceText && text !== '') {
28
+ return `="${text.replace(/"/g, '""')}"`;
29
+ }
30
+
31
+ if (text !== '' && (FORMULA_PREFIX.test(text) || LONG_DIGITS.test(text))) {
32
+ return `="${text.replace(/"/g, '""')}"`;
33
+ }
34
+
35
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
36
+ }
37
+
38
+ /** Serialises using the register's own columns, so the file matches the form */
39
+ export function columnsToCsv(
40
+ rows: Array<Record<string, any>>,
41
+ columns: RegisterColumn[],
42
+ options?: {
43
+ /** A second header row (e.g. the printed form's column letters) */
44
+ subHeaderRow?: string[];
45
+ },
46
+ ): string {
47
+ const headerRows = [
48
+ columns.map((column) => escapeCell(column.header)).join(','),
49
+ ...(options?.subHeaderRow ? [options.subHeaderRow.map((cell) => escapeCell(cell)).join(',')] : []),
50
+ ];
51
+
52
+ return [
53
+ ...headerRows,
54
+ ...rows.map((row, index) => columns.map((column) => escapeCell(column.value(row, index), column.text)).join(',')),
55
+ ].join('\r\n');
56
+ }
57
+
58
+ /** Fallback for registers with no column spec yet: the response's own fields */
59
+ export function rowsToCsv(rows: Array<Record<string, any>>): string {
60
+ if (!rows.length) {
61
+ return '';
62
+ }
63
+
64
+ /* Union of keys, not just the first row's: endpoints omit fields that are
65
+ null for a given client, so keying off row one drops columns. */
66
+ const keys = new Set<string>();
67
+ rows.forEach((row) => Object.keys(row).forEach((key) => keys.add(key)));
68
+ const columns = Array.from(keys);
69
+
70
+ return [
71
+ columns.map((column) => escapeCell(column)).join(','),
72
+ ...rows.map((row) => columns.map((column) => escapeCell(row[column])).join(',')),
73
+ ].join('\r\n');
74
+ }
75
+
76
+ /** Prompts the browser to save `csv` under `filename` */
77
+ export function downloadCsv(csv: string, filename: string): void {
78
+ // the BOM is what makes Excel read the file as UTF-8 rather than latin-1
79
+ const blob = new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' });
80
+ const url = URL.createObjectURL(blob);
81
+ const link = document.createElement('a');
82
+
83
+ link.href = url;
84
+ link.download = filename;
85
+ document.body.appendChild(link);
86
+ link.click();
87
+ document.body.removeChild(link);
88
+ URL.revokeObjectURL(url);
89
+ }
@@ -0,0 +1,54 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { openmrsFetch, useSession } from '@openmrs/esm-framework';
3
+
4
+ /**
5
+ * The display name of the location(s) a register was generated for.
6
+ *
7
+ * Read from the link's `locationUuids` rather than the session: the user may
8
+ * have switched location since generating the report, and the register must
9
+ * name the location its rows came from. The session location answers without
10
+ * a request when it is the one asked for; any other is looked up. Several
11
+ * uuids (comma-separated) are named in order.
12
+ */
13
+ export function useLocationName(locationUuids: string | null | undefined): string {
14
+ const session = useSession();
15
+ const uuids = (locationUuids ?? '')
16
+ .split(',')
17
+ .map((uuid) => uuid.trim())
18
+ .filter(Boolean);
19
+ const key = uuids.join(',');
20
+ const sessionUuid = session?.sessionLocation?.uuid;
21
+ const sessionName = session?.sessionLocation?.display;
22
+
23
+ const [names, setNames] = useState<Record<string, string>>({});
24
+
25
+ useEffect(() => {
26
+ const missing = uuids.filter((uuid) => uuid !== sessionUuid);
27
+ if (missing.length === 0) {
28
+ return;
29
+ }
30
+
31
+ const abortController = new AbortController();
32
+
33
+ Promise.all(
34
+ missing.map((uuid) =>
35
+ openmrsFetch(`/ws/rest/v1/location/${uuid}?v=custom:(display)`, { signal: abortController.signal })
36
+ .then((response) => response.json())
37
+ .then((location) => [uuid, location?.display ?? ''] as const)
38
+ .catch(() => [uuid, ''] as const),
39
+ ),
40
+ ).then((found) => {
41
+ if (!abortController.signal.aborted) {
42
+ setNames(Object.fromEntries(found.filter(([, name]) => name)));
43
+ }
44
+ });
45
+
46
+ return () => abortController.abort();
47
+ // eslint-disable-next-line react-hooks/exhaustive-deps
48
+ }, [key, sessionUuid]);
49
+
50
+ return uuids
51
+ .map((uuid) => (uuid === sessionUuid ? sessionName : names[uuid]))
52
+ .filter(Boolean)
53
+ .join(', ');
54
+ }
@@ -0,0 +1,92 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import { PAGE_SIZE, type PatientListPage } from '../../resources/patient-list-page';
4
+ import { showReportError } from '../report-error';
5
+
6
+ type Fetcher = (paging: { startIndex: number; limit: number }) => Promise<PatientListPage>;
7
+
8
+ /**
9
+ * Page state and fetching for a register's line list.
10
+ *
11
+ * Registers used to ask for a hardcoded 300 rows and render every one of them.
12
+ * At 41 columns that is 12,300 cells, and anything past 300 was silently
13
+ * dropped. This fetches a page at a time, so the DOM stays flat however large
14
+ * the list is, and reports the total so truncation is never invisible.
15
+ */
16
+ export function usePatientList(fetcher: Fetcher, deps: React.DependencyList) {
17
+ const [rows, setRows] = useState<Array<Record<string, any>>>([]);
18
+ const [total, setTotal] = useState(0);
19
+ const [isTotalExact, setIsTotalExact] = useState(true);
20
+ const [isLoading, setIsLoading] = useState(false);
21
+ const [page, setPage] = useState(1);
22
+ const [pageSize, setPageSize] = useState(PAGE_SIZE);
23
+
24
+ /* Guards against an earlier page landing after a later one and overwriting
25
+ it, which is easy to trigger by clicking through pages quickly. */
26
+ const request = useRef(0);
27
+
28
+ const load = useCallback(
29
+ async (nextPage: number, size: number) => {
30
+ const id = ++request.current;
31
+
32
+ setIsLoading(true);
33
+
34
+ try {
35
+ const result = await fetcher({ startIndex: (nextPage - 1) * size, limit: size });
36
+
37
+ if (id !== request.current) {
38
+ return;
39
+ }
40
+
41
+ setRows(result.results);
42
+ setTotal(result.total);
43
+ setIsTotalExact(result.isTotalExact);
44
+ } catch (error) {
45
+ if (id === request.current) {
46
+ setRows([]);
47
+ showReportError('the register', error);
48
+ }
49
+ } finally {
50
+ if (id === request.current) {
51
+ setIsLoading(false);
52
+ }
53
+ }
54
+ },
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ deps,
57
+ );
58
+
59
+ /** Back to page one whenever the query behind the list changes */
60
+ useEffect(() => {
61
+ setPage(1);
62
+ load(1, pageSize);
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, deps);
65
+
66
+ const onPageChange = useCallback(
67
+ ({ page: nextPage, pageSize: nextSize }: { page: number; pageSize: number }) => {
68
+ setPage(nextPage);
69
+ setPageSize(nextSize);
70
+ load(nextPage, nextSize);
71
+ },
72
+ [load],
73
+ );
74
+
75
+ /** Every row across every page, for the CSV export */
76
+ const fetchAll = useCallback(async () => {
77
+ const all: Array<Record<string, any>> = [];
78
+ const size = 500;
79
+
80
+ for (let index = 0; ; index += size) {
81
+ const result = await fetcher({ startIndex: index, limit: size });
82
+ all.push(...result.results);
83
+
84
+ if (result.results.length < size) {
85
+ return all;
86
+ }
87
+ }
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ }, deps);
90
+
91
+ return { rows, total, isTotalExact, isLoading, page, pageSize, onPageChange, fetchAll };
92
+ }
@@ -0,0 +1 @@
1
+ export * from './report-skeleton.component';
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { DataTableSkeleton, SkeletonText } from '@carbon/react';
3
+
4
+ import styles from './report-skeleton.scss';
5
+
6
+ interface ReportSkeletonProps {
7
+ /** How many indicator blocks to outline. Roughly the sections a form has. */
8
+ blocks?: number;
9
+ }
10
+
11
+ /**
12
+ * Placeholder in the shape of a generated report.
13
+ *
14
+ * Reports replaced their whole body while fetching, so a centred spinner left
15
+ * the page blank and shifted everything when the data landed. Outlining the
16
+ * masthead and the indicator tables keeps the layout stable and shows how much
17
+ * is coming.
18
+ */
19
+ export const ReportSkeleton: React.FC<ReportSkeletonProps> = ({ blocks = 2 }) => (
20
+ <div className={styles.skeleton} aria-busy="true" aria-label="Loading report">
21
+ <div className={styles.header}>
22
+ <SkeletonText heading width="45%" />
23
+ <SkeletonText width="70%" />
24
+ </div>
25
+
26
+ {Array.from({ length: blocks }, (_, index) => (
27
+ <div key={index} className={styles.block}>
28
+ <SkeletonText heading width="30%" />
29
+ <DataTableSkeleton showHeader={false} showToolbar={false} columnCount={4} rowCount={5} size="sm" />
30
+ </div>
31
+ ))}
32
+ </div>
33
+ );
34
+
35
+ /**
36
+ * Placeholder for the register and patient-list pages, which are a single
37
+ * table rather than a form.
38
+ */
39
+ export const RegisterSkeleton: React.FC<{ columnCount?: number; rowCount?: number }> = ({
40
+ columnCount = 6,
41
+ rowCount = 8,
42
+ }) => (
43
+ <div className={styles.skeleton} aria-busy="true" aria-label="Loading register">
44
+ <DataTableSkeleton showHeader={false} showToolbar={false} columnCount={columnCount} rowCount={rowCount} size="sm" />
45
+ </div>
46
+ );
@@ -0,0 +1,29 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .skeleton {
5
+ padding: layout.$spacing-05;
6
+ margin-bottom: layout.$spacing-05;
7
+ background-color: $ui-02;
8
+ border: 1px solid $ui-03;
9
+ }
10
+
11
+ /* When it stands in for a register table it sits under the caption bar */
12
+ .skeleton:not(:first-child) {
13
+ margin-bottom: 0;
14
+ border-top: none;
15
+ }
16
+
17
+ .header {
18
+ padding-bottom: layout.$spacing-05;
19
+ margin-bottom: layout.$spacing-05;
20
+ border-bottom: 1px solid $ui-03;
21
+ }
22
+
23
+ .block {
24
+ margin-bottom: layout.$spacing-06;
25
+
26
+ &:last-child {
27
+ margin-bottom: 0;
28
+ }
29
+ }
@@ -3,10 +3,18 @@ export const configSchema = {
3
3
  etlBaseUrl: {
4
4
  _type: Type.String,
5
5
  _default: '',
6
- _description: 'ETL endpoint',
6
+ _description: 'Base URL of the ETL service every report reads from, e.g. "/etl"',
7
+ },
8
+ /* Declared because the deployment sets it in this section. Nothing reads it
9
+ yet; without it the config validator reports it as an unknown key. */
10
+ subDomainUrl: {
11
+ _type: Type.String,
12
+ _default: '',
13
+ _description: 'Origin this instance is served from',
7
14
  },
8
15
  };
9
16
 
10
17
  export type Config = {
11
18
  etlBaseUrl: string;
19
+ subDomainUrl: string;
12
20
  };
@@ -0,0 +1,173 @@
1
+ import {
2
+ Activity,
3
+ ChartColumn,
4
+ Document,
5
+ DocumentTasks,
6
+ GenderFemale,
7
+ Growth,
8
+ HealthCross,
9
+ Microscope,
10
+ UserMultiple,
11
+ Warning,
12
+ type CarbonIconType,
13
+ } from '@carbon/react/icons';
14
+
15
+ export type ReportTone = 'violet' | 'magenta' | 'teal' | 'blue';
16
+
17
+ export interface ReportDefinition {
18
+ /** The MOH form number, e.g. `MOH 731` */
19
+ code: string;
20
+ /** Human readable name of the form */
21
+ name: string;
22
+ description: string;
23
+ /** Route registered in root.component.tsx */
24
+ path: string;
25
+ icon: CarbonIconType;
26
+ /** Extra terms matched by the search field */
27
+ keywords: string[];
28
+ }
29
+
30
+ export interface ReportCategory {
31
+ id: string;
32
+ title: string;
33
+ description: string;
34
+ tone: ReportTone;
35
+ reports: ReportDefinition[];
36
+ }
37
+
38
+ export const reportCategories: ReportCategory[] = [
39
+ {
40
+ id: 'hiv-and-chronic-care',
41
+ title: 'HIV & chronic care',
42
+ description: 'Comprehensive care programme summaries',
43
+ tone: 'violet',
44
+ reports: [
45
+ {
46
+ code: 'MOH 731',
47
+ name: 'Comprehensive HIV/AIDS',
48
+ description: 'NASCOP monthly summary for HIV testing, EMTCT, care and treatment.',
49
+ path: '/moh-731',
50
+ icon: HealthCross,
51
+ keywords: ['hiv', 'nascop', 'art', 'prep', 'emtct', 'pmtct', 'tb'],
52
+ },
53
+ {
54
+ code: 'MOH 740',
55
+ name: 'Diabetes & hypertension',
56
+ description: 'Monthly summary for the NCD comprehensive care clinic.',
57
+ path: '/moh-740',
58
+ icon: Activity,
59
+ keywords: ['ncd', 'diabetes', 'hypertension', 'chronic'],
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ id: 'reproductive-and-child-health',
65
+ title: 'Reproductive & child health',
66
+ description: 'RMNCAH service delivery summaries',
67
+ tone: 'magenta',
68
+ reports: [
69
+ {
70
+ code: 'MOH 710',
71
+ name: 'Immunization',
72
+ description: 'Child immunization and vitamin A supplementation summary.',
73
+ path: '/moh-710',
74
+ icon: Growth,
75
+ keywords: ['immunization', 'vaccine', 'child', 'khis', 'bcg', 'polio'],
76
+ },
77
+ {
78
+ code: 'MOH 711',
79
+ name: 'Integrated programme summary',
80
+ description: 'Reproductive and child health, medical and rehabilitation services.',
81
+ path: '/moh-711',
82
+ icon: UserMultiple,
83
+ keywords: ['rmncah', 'anc', 'pnc', 'maternity', 'family planning', 'gbv', 'physiotherapy'],
84
+ },
85
+ {
86
+ code: 'MOH 745',
87
+ name: 'Cancer screening',
88
+ description: 'Monthly cervical and breast cancer screening summary.',
89
+ path: '/moh-745',
90
+ icon: GenderFemale,
91
+ keywords: ['cancer', 'cervical', 'screening', 'via', 'hpv'],
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ id: 'outpatient-and-workload',
97
+ title: 'Outpatient & workload',
98
+ description: 'Morbidity and facility workload returns',
99
+ tone: 'teal',
100
+ reports: [
101
+ {
102
+ code: 'MOH 705A',
103
+ name: 'Outpatient morbidity, under 5',
104
+ description: 'Summary of diagnoses for children under five years.',
105
+ path: '/moh-705a',
106
+ icon: DocumentTasks,
107
+ keywords: ['morbidity', 'outpatient', 'under five', 'children', 'khis'],
108
+ },
109
+ {
110
+ code: 'MOH 705B',
111
+ name: 'Outpatient morbidity, over 5',
112
+ description: 'Summary of diagnoses for clients aged five years and above.',
113
+ path: '/moh-705b',
114
+ icon: Document,
115
+ keywords: ['morbidity', 'outpatient', 'above five', 'adults', 'khis'],
116
+ },
117
+ {
118
+ code: 'MOH 717',
119
+ name: 'Service workload',
120
+ description: 'Monthly service workload across facility departments.',
121
+ path: '/moh-717',
122
+ icon: ChartColumn,
123
+ keywords: ['workload', 'inpatient', 'pharmacy', 'mortuary', 'theatre', 'maternity'],
124
+ },
125
+ ],
126
+ },
127
+ {
128
+ id: 'diagnostics-and-surveillance',
129
+ title: 'Diagnostics & surveillance',
130
+ description: 'Laboratory and disease surveillance returns',
131
+ tone: 'blue',
132
+ reports: [
133
+ {
134
+ code: 'MOH 706',
135
+ name: 'Laboratory services',
136
+ description: 'Monthly summary of laboratory investigations and results.',
137
+ path: '/moh-706',
138
+ icon: Microscope,
139
+ keywords: ['laboratory', 'lab', 'urine', 'parasitology', 'blood chemistry'],
140
+ },
141
+ {
142
+ code: 'MOH 505',
143
+ name: 'IDSR epidemic monitoring',
144
+ description: 'Weekly monitoring form for priority notifiable diseases.',
145
+ path: '/moh-505',
146
+ icon: Warning,
147
+ keywords: ['idsr', 'surveillance', 'epidemic', 'notifiable', 'outbreak'],
148
+ },
149
+ ],
150
+ },
151
+ ];
152
+
153
+ export const totalReportCount = reportCategories.reduce((count, category) => count + category.reports.length, 0);
154
+
155
+ export function filterCategories(categories: ReportCategory[], searchTerm: string): ReportCategory[] {
156
+ const term = searchTerm.trim().toLowerCase();
157
+
158
+ if (!term) {
159
+ return categories;
160
+ }
161
+
162
+ return categories
163
+ .map((category) => ({
164
+ ...category,
165
+ reports: category.reports.filter((report) =>
166
+ [report.code, report.name, report.description, category.title, ...report.keywords]
167
+ .join(' ')
168
+ .toLowerCase()
169
+ .includes(term),
170
+ ),
171
+ }))
172
+ .filter((category) => category.reports.length > 0);
173
+ }