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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/dist/197.js +1 -1
  2. package/dist/282.js +1 -1
  3. package/dist/282.js.map +1 -1
  4. package/dist/295.css +1 -0
  5. package/dist/295.css.map +1 -0
  6. package/dist/295.js +1 -0
  7. package/dist/295.js.map +1 -0
  8. package/dist/300.js +1 -1
  9. package/dist/311.js +1 -1
  10. package/dist/311.js.map +1 -1
  11. package/dist/335.js +1 -1
  12. package/dist/353.js +1 -1
  13. package/dist/353.js.map +1 -1
  14. package/dist/410.js +1 -0
  15. package/dist/410.js.map +1 -0
  16. package/dist/478.js +1 -1
  17. package/dist/478.js.map +1 -1
  18. package/dist/540.js +1 -1
  19. package/dist/540.js.map +1 -1
  20. package/dist/55.js +1 -1
  21. package/dist/580.js +1 -0
  22. package/dist/580.js.map +1 -0
  23. package/dist/652.js +1 -1
  24. package/dist/876.css +1 -0
  25. package/dist/876.css.map +1 -0
  26. package/dist/876.js +1 -0
  27. package/dist/876.js.map +1 -0
  28. package/dist/907.js +2 -0
  29. package/dist/907.js.LICENSE.txt +35 -0
  30. package/dist/907.js.map +1 -0
  31. package/dist/961.js +1 -1
  32. package/dist/961.js.map +1 -1
  33. package/dist/99.js +1 -1
  34. package/dist/ampath-esm-reports-app.js +1 -1
  35. package/dist/ampath-esm-reports-app.js.map +1 -1
  36. package/dist/routes.json +1 -1
  37. package/package.json +2 -2
  38. package/src/common/daily-summary-table.tsx +55 -0
  39. package/src/common/linelist-table/index.ts +3 -0
  40. package/src/common/linelist-table/linelist-columns.ts +48 -0
  41. package/src/common/linelist-table/linelist-table.component.tsx +442 -0
  42. package/src/common/linelist-table/linelist-table.scss +306 -0
  43. package/src/common/linelist-table/linelist-table.test.tsx +120 -0
  44. package/src/common/linelist-table/use-all-rows.ts +93 -0
  45. package/src/common/linelist-table/use-column-layout.ts +123 -0
  46. package/src/common/report-error/index.ts +1 -0
  47. package/src/common/report-error/show-report-error.ts +25 -0
  48. package/src/common/report-filters/date-range.ts +137 -0
  49. package/src/common/report-filters/index.ts +4 -0
  50. package/src/common/report-filters/report-filters.component.tsx +297 -100
  51. package/src/common/report-filters/report-filters.scss +255 -32
  52. package/src/common/report-filters/types.ts +66 -0
  53. package/src/common/report-filters/use-restore-period.ts +43 -0
  54. package/src/common/report-header/index.ts +1 -0
  55. package/src/common/report-header/use-location-details.ts +58 -0
  56. package/src/common/report-page/index.ts +2 -0
  57. package/src/common/report-page/report-page.component.tsx +32 -0
  58. package/src/common/report-page/report-page.scss +45 -0
  59. package/src/common/report-page/use-page-scale.ts +60 -0
  60. package/src/common/report-placeholder/has-report-data.ts +15 -0
  61. package/src/common/report-placeholder/index.ts +3 -0
  62. package/src/common/report-placeholder/report-placeholder.component.tsx +81 -0
  63. package/src/common/report-placeholder/report-placeholder.scss +48 -0
  64. package/src/common/report-placeholder/use-generated-location.ts +62 -0
  65. package/src/common/report-register/index.ts +6 -0
  66. package/src/common/report-register/register-columns.ts +46 -0
  67. package/src/common/report-register/register-layout.component.tsx +265 -0
  68. package/src/common/report-register/register-layout.scss +150 -0
  69. package/src/common/report-register/register-table.scss +71 -0
  70. package/src/common/report-register/report-parents.ts +27 -0
  71. package/src/common/report-register/rows-to-csv.ts +89 -0
  72. package/src/common/report-register/use-location-name.ts +54 -0
  73. package/src/common/report-register/use-patient-list.ts +92 -0
  74. package/src/common/report-skeleton/index.ts +1 -0
  75. package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
  76. package/src/common/report-skeleton/report-skeleton.scss +29 -0
  77. package/src/config-schema.ts +9 -1
  78. package/src/dashboard/report-catalog.ts +173 -0
  79. package/src/dashboard/reports-dasboard.tsx +101 -17
  80. package/src/dashboard/reports-dashboard.module.scss +269 -11
  81. package/src/index.ts +13 -1
  82. package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +69 -0
  83. package/src/reports/moh-240/moh-240-header.component.tsx +62 -0
  84. package/src/reports/moh-240/moh-240.columns.ts +113 -0
  85. package/src/reports/moh-240/moh-240.component.tsx +75 -0
  86. package/src/reports/moh-240/moh-240.scss +40 -0
  87. package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +36 -0
  88. package/src/reports/moh-240/sub-reports/page-summary.component.tsx +148 -0
  89. package/src/reports/moh-505/moh-505-header.component.tsx +67 -0
  90. package/src/reports/moh-505/moh-505.component.tsx +441 -0
  91. package/src/reports/moh-505/moh-505.scss +117 -0
  92. package/src/reports/moh-705B/moh-204b-register.component.tsx +244 -0
  93. package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
  94. package/src/reports/moh-705B/moh-705b.component.tsx +658 -0
  95. package/src/reports/moh-705B/moh-705b.scss +172 -0
  96. package/src/reports/moh-705a/moh-705a.component.tsx +660 -0
  97. package/src/reports/moh-705a/moh-705a.scss +172 -0
  98. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +329 -0
  99. package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
  100. package/src/reports/moh-705a/type.ts +6 -0
  101. package/src/reports/moh-706/moh-706-header.component.tsx +66 -0
  102. package/src/reports/moh-706/moh-706.component.tsx +150 -24
  103. package/src/reports/moh-706/moh-706.scss +119 -11
  104. package/src/reports/moh-706/sub-reports/bacteriology/bacteriology.component.tsx +36 -44
  105. package/src/reports/moh-706/sub-reports/blood-chemistry/blood-chemistry.component.tsx +232 -318
  106. package/src/reports/moh-706/sub-reports/drug-susceptibility-testing/drug-susceptibility-testing.component.tsx +32 -47
  107. package/src/reports/moh-706/sub-reports/haematology/haematology.component.tsx +44 -52
  108. package/src/reports/moh-706/sub-reports/histology-and-cytology/histology-and-cytology.component.tsx +25 -40
  109. package/src/reports/moh-706/sub-reports/parasitology/parasitology.component.tsx +31 -39
  110. package/src/reports/moh-706/sub-reports/serology/serology.component.tsx +26 -34
  111. package/src/reports/moh-706/sub-reports/specimen-referral-to-higher-levels/specimen-referral-to-higher-levels.component.tsx +10 -25
  112. package/src/reports/moh-706/sub-reports/urine-analysis/urine-analysis.component.tsx +42 -50
  113. package/src/reports/moh-710/moh-710.component.tsx +799 -632
  114. package/src/reports/moh-710/moh-710.scss +142 -21
  115. package/src/reports/moh-710/registers/moh-510/moh-510.columns.ts +40 -0
  116. package/src/reports/moh-710/registers/moh-510/moh-510.component.tsx +144 -0
  117. package/src/reports/moh-710/registers/moh-510/moh-510.scss +130 -0
  118. package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +268 -0
  119. package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
  120. package/src/reports/moh-710/registers/type.ts +31 -0
  121. package/src/reports/moh-711/moh-711.component.tsx +138 -1269
  122. package/src/reports/moh-711/moh711.scss +135 -20
  123. package/src/reports/moh-711/registers/moh-333-register.component.tsx +637 -0
  124. package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
  125. package/src/reports/moh-711/registers/moh-405-register.component.tsx +520 -0
  126. package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
  127. package/src/reports/moh-711/registers/moh-406-register.component.tsx +530 -0
  128. package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
  129. package/src/reports/moh-711/registers/moh-510-register.component.tsx +55 -0
  130. package/src/reports/moh-711/registers/moh-511-register.component.tsx +270 -0
  131. package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
  132. package/src/reports/moh-711/registers/type.ts +192 -0
  133. package/src/reports/moh-711/sections/anc.component.tsx +159 -0
  134. package/src/reports/moh-711/sections/cervical-cancer.component.tsx +148 -0
  135. package/src/reports/moh-711/sections/chanis.component.tsx +366 -0
  136. package/src/reports/moh-711/sections/family-planning.component.tsx +221 -0
  137. package/src/reports/moh-711/sections/gbv.component.tsx +115 -0
  138. package/src/reports/moh-711/sections/maternity.component.tsx +321 -0
  139. package/src/reports/moh-711/sections/medical-social-work.component.tsx +73 -0
  140. package/src/reports/moh-711/sections/other.component.tsx +47 -0
  141. package/src/reports/moh-711/sections/physiotherapy.component.tsx +61 -0
  142. package/src/reports/moh-711/sections/pnc.component.tsx +127 -0
  143. package/src/reports/moh-711/sections/post-abortion.component.tsx +42 -0
  144. package/src/reports/moh-711/sections/rehabilitation.component.tsx +57 -0
  145. package/src/reports/moh-711/sections/report-compiled-by.component.tsx +50 -0
  146. package/src/reports/moh-711/sections/tb-screening.component.tsx +57 -0
  147. package/src/reports/moh-717/moh-717.component.tsx +149 -985
  148. package/src/reports/moh-717/moh717.scss +217 -21
  149. package/src/reports/moh-717/registers/nutrition-register.component.tsx +48 -0
  150. package/src/reports/moh-717/sections/finance.component.tsx +42 -0
  151. package/src/reports/moh-717/sections/inpatient.component.tsx +374 -0
  152. package/src/reports/moh-717/sections/maternity.component.tsx +126 -0
  153. package/src/reports/moh-717/sections/medical-records.component.tsx +35 -0
  154. package/src/reports/moh-717/sections/mortuary.component.tsx +43 -0
  155. package/src/reports/moh-717/sections/operations.component.tsx +53 -0
  156. package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +98 -0
  157. package/src/reports/moh-717/sections/outpatient.component.tsx +597 -0
  158. package/src/reports/moh-717/sections/pharmacy.component.tsx +43 -0
  159. package/src/reports/moh-717/sections/preparedby.component.tsx +52 -0
  160. package/src/reports/moh-717/sections/special-services.component.tsx +114 -0
  161. package/src/reports/moh-731/linelist/moh-731-indicators.ts +1040 -0
  162. package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +423 -0
  163. package/src/reports/moh-731/linelist/moh-731-linelist.component.tsx +102 -0
  164. package/src/reports/moh-731/linelist/use-open-moh-731-linelist.ts +50 -0
  165. package/src/reports/moh-731/moh-731.component.tsx +251 -0
  166. package/src/reports/moh-731/moh-731.scss +493 -0
  167. package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
  168. package/src/reports/moh-731/registers/moh-366.component.tsx +471 -0
  169. package/src/reports/moh-731/registers/prep-register.component.tsx +284 -0
  170. package/src/reports/moh-731/registers/prep.columns.ts +34 -0
  171. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1209 -0
  172. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +727 -0
  173. package/src/reports/moh-731/sections/pmtct.component.tsx +572 -0
  174. package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
  175. package/src/reports/moh-731/summary/moh-731-summary.config.ts +520 -0
  176. package/src/reports/moh-731/summary/moh-731-summary.scss +242 -0
  177. package/src/reports/moh-740/moh-740.component.scss +202 -0
  178. package/src/reports/moh-740/moh-740.component.tsx +1312 -0
  179. package/src/reports/moh-740/moh-740.resource.ts +49 -0
  180. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +89 -0
  181. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +61 -0
  182. package/src/reports/moh-740/registers/moh-740-patient-list.scss +0 -0
  183. package/src/reports/moh-740/registers/moh-740-patient-list.tsx +95 -0
  184. package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
  185. package/src/reports/moh-740/shared/data-cell/data-cell.scss +6 -0
  186. package/src/reports/moh-740/shared/data-cell/data-cell.tsx +23 -0
  187. package/src/reports/moh-740/shared/utils/indicator-register-map.ts +157 -0
  188. package/src/reports/moh-740/types/index.ts +220 -0
  189. package/src/reports/moh-745/moh-745.component.tsx +1021 -0
  190. package/src/reports/moh-745/moh-745.scss +132 -0
  191. package/src/reports/moh-745/registers/moh-412-register.component.tsx +199 -0
  192. package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
  193. package/src/reports/moh-745/registers/type.ts +181 -0
  194. package/src/reports/table-wrapper/table-row-mapper.component.tsx +53 -0
  195. package/src/reports/table-wrapper/table-wrapper.component.tsx +30 -0
  196. package/src/reports/{moh-706/table-wrapper → table-wrapper}/table-wrapper.scss +1 -16
  197. package/src/resources/moh-505.resource.ts +69 -0
  198. package/src/resources/moh-705.resource.ts +106 -0
  199. package/src/resources/moh-706.resource.ts +80 -0
  200. package/src/resources/moh-710.resource.ts +32 -9
  201. package/src/resources/moh-711.resource.ts +137 -9
  202. package/src/resources/moh-717.resource.ts +26 -9
  203. package/src/resources/moh-731.resource.ts +90 -0
  204. package/src/resources/moh-745.resource.ts +76 -0
  205. package/src/resources/patient-list-page.ts +65 -0
  206. package/src/root.component.tsx +50 -7
  207. package/src/root.scss +36 -0
  208. package/src/routes.json +3 -1
  209. package/src/utils/format-indicator.ts +22 -0
  210. package/src/utils/get-base-url.ts +39 -7
  211. package/src/utils/utils.ts +36 -0
  212. package/tsconfig.json +1 -1
  213. package/webpack.config.js +22 -1
  214. package/dist/184.js +0 -2
  215. package/dist/184.js.LICENSE.txt +0 -14
  216. package/dist/184.js.map +0 -1
  217. package/dist/260.js +0 -1
  218. package/dist/260.js.map +0 -1
  219. package/dist/402.js +0 -1
  220. package/dist/402.js.map +0 -1
  221. package/dist/812.js +0 -1
  222. package/dist/812.js.map +0 -1
  223. package/dist/959.js +0 -2
  224. package/dist/959.js.LICENSE.txt +0 -20
  225. package/dist/959.js.map +0 -1
  226. package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -539
  227. package/dist/main.js +0 -1
  228. package/dist/main.js.map +0 -1
  229. package/src/reports/moh-706/table-wrapper/table-wrapper.component.tsx +0 -21
@@ -1,28 +1,112 @@
1
- import React from 'react';
1
+ import React, { useMemo, useState } from 'react';
2
2
  import { useNavigate } from 'react-router-dom';
3
- import { ClickableTile } from '@carbon/react';
3
+ import { Button, ClickableTile, Layer, Search, Tag } from '@carbon/react';
4
+ import { ArrowRight, Calendar, Location, Report } from '@carbon/react/icons';
5
+ import { formatDate, useSession } from '@openmrs/esm-framework';
4
6
 
7
+ import { filterCategories, reportCategories, totalReportCount, type ReportDefinition } from './report-catalog';
5
8
  import styles from './reports-dashboard.module.scss';
9
+
6
10
  interface ReportsDashboardProps {}
11
+
7
12
  const ReportsDashboard: React.FC<ReportsDashboardProps> = () => {
8
13
  const navigate = useNavigate();
14
+ const session = useSession();
15
+ const [searchTerm, setSearchTerm] = useState<string>('');
16
+
17
+ const facilityName = session?.sessionLocation?.display;
18
+ const today = formatDate(new Date(), { mode: 'standard', time: false, noToday: true });
19
+
20
+ const visibleCategories = useMemo(() => filterCategories(reportCategories, searchTerm), [searchTerm]);
21
+ const matchCount = visibleCategories.reduce((count, category) => count + category.reports.length, 0);
22
+ const isSearching = searchTerm.trim().length > 0;
23
+
24
+ const renderReportTile = (report: ReportDefinition, tone: string) => {
25
+ const Icon = report.icon;
26
+
27
+ return (
28
+ <ClickableTile
29
+ key={report.path}
30
+ className={styles.tile}
31
+ data-tone={tone}
32
+ onClick={() => navigate(report.path)}
33
+ aria-label={`${report.code} ${report.name}`}
34
+ >
35
+ <div className={styles.tileIcon}>
36
+ <Icon size={20} />
37
+ </div>
38
+ <div className={styles.tileCopy}>
39
+ <p className={styles.tileCode}>{report.code}</p>
40
+ <h4 className={styles.tileName}>{report.name}</h4>
41
+ </div>
42
+ <ArrowRight size={16} className={styles.tileArrow} />
43
+ </ClickableTile>
44
+ );
45
+ };
46
+
9
47
  return (
10
- <>
11
- <div className={styles.container}>
12
- <ClickableTile onClick={() => navigate('/moh-710')} className={styles.tile}>
13
- MOH-710 REPORT
14
- </ClickableTile>
15
- <ClickableTile onClick={() => navigate('/moh-711')} className={styles.tile}>
16
- MOH-711 REPORT
17
- </ClickableTile>
18
- <ClickableTile onClick={() => navigate('/moh-717')} className={styles.tile}>
19
- MOH-717 REPORT
20
- </ClickableTile>
21
- <ClickableTile onClick={() => navigate('/moh-706')} className={styles.tile}>
22
- MOH-706 REPORT
23
- </ClickableTile>
48
+ <div className={styles.dashboard}>
49
+ <header className={styles.header}>
50
+ <div className={styles.headerIcon}>
51
+ <Report size={20} />
52
+ </div>
53
+ <h3 className={styles.headerTitle}>Reports</h3>
54
+ <div className={styles.headerMeta}>
55
+ {facilityName && (
56
+ <span className={styles.metaItem}>
57
+ <Location size={16} />
58
+ {facilityName}
59
+ </span>
60
+ )}
61
+ <span className={styles.metaItem}>
62
+ <Calendar size={16} />
63
+ {today}
64
+ </span>
65
+ </div>
66
+ </header>
67
+
68
+ <div className={styles.toolbar}>
69
+ <Layer className={styles.searchLayer}>
70
+ <Search
71
+ id="reports-search"
72
+ labelText="Search reports"
73
+ placeholder="Search reports"
74
+ size="sm"
75
+ value={searchTerm}
76
+ onChange={(event) => setSearchTerm(event.target.value)}
77
+ onClear={() => setSearchTerm('')}
78
+ />
79
+ </Layer>
80
+ <p className={styles.resultCount}>
81
+ {isSearching ? `${matchCount} of ${totalReportCount} reports` : `${totalReportCount} reports available`}
82
+ </p>
24
83
  </div>
25
- </>
84
+
85
+ {visibleCategories.length > 0 ? (
86
+ visibleCategories.map((category) => (
87
+ <section key={category.id} className={styles.category}>
88
+ <div className={styles.categoryHeader} data-tone={category.tone}>
89
+ <h5 className={styles.categoryTitle}>{category.title}</h5>
90
+ <Tag size="sm" type="outline">
91
+ {category.reports.length}
92
+ </Tag>
93
+ </div>
94
+ <div className={styles.grid}>
95
+ {category.reports.map((report) => renderReportTile(report, category.tone))}
96
+ </div>
97
+ </section>
98
+ ))
99
+ ) : (
100
+ <Layer className={styles.emptyState}>
101
+ <Report size={32} className={styles.emptyStateIcon} />
102
+ <h5>No reports match &ldquo;{searchTerm}&rdquo;</h5>
103
+ <p>Try a form number such as 731, or a service such as immunization.</p>
104
+ <Button kind="ghost" size="sm" onClick={() => setSearchTerm('')}>
105
+ Clear search
106
+ </Button>
107
+ </Layer>
108
+ )}
109
+ </div>
26
110
  );
27
111
  };
28
112
 
@@ -1,14 +1,272 @@
1
- .container {
2
- display: flex;
1
+ @use 'sass:list';
2
+ @use '@carbon/colors';
3
+ @use '@carbon/layout';
4
+ @use '@carbon/motion';
5
+ @use '@carbon/type';
6
+ @use '@openmrs/esm-styleguide/src/vars' as *;
7
+
8
+ $tones: (
9
+ 'violet': (
10
+ colors.$purple-60,
11
+ colors.$purple-10,
12
+ ),
13
+ 'magenta': (
14
+ colors.$magenta-60,
15
+ colors.$magenta-10,
16
+ ),
17
+ 'teal': (
18
+ colors.$teal-60,
19
+ colors.$teal-10,
20
+ ),
21
+ 'blue': (
22
+ colors.$blue-60,
23
+ colors.$blue-10,
24
+ ),
25
+ );
26
+
27
+ .dashboard {
28
+ /* Horizontal gutter comes from `.page` in root.scss */
29
+ padding: layout.$spacing-05 0 layout.$spacing-07;
30
+ background-color: $openmrs-background-grey;
31
+ min-height: 100%;
32
+ }
33
+
34
+ /* Header */
35
+
36
+ .header {
37
+ display: flex;
38
+ align-items: center;
39
+ flex-wrap: wrap;
40
+ gap: layout.$spacing-03 layout.$spacing-04;
41
+ padding: layout.$spacing-03 layout.$spacing-05;
42
+ margin-bottom: layout.$spacing-04;
43
+ background-color: $ui-02;
44
+ border: 1px solid $ui-03;
45
+ border-left: layout.$spacing-02 solid var(--brand-01);
46
+ }
47
+
48
+ .headerIcon {
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ flex-shrink: 0;
53
+ width: layout.$spacing-06;
54
+ height: layout.$spacing-06;
55
+ color: var(--brand-01);
56
+ background-color: $brand-primary-10;
57
+ }
58
+
59
+ .headerTitle {
60
+ @include type.type-style('productive-heading-03');
61
+ color: $color-gray-100;
62
+ }
63
+
64
+ .headerMeta {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: layout.$spacing-05;
68
+ margin-left: auto;
69
+ }
70
+
71
+ .metaItem {
72
+ @include type.type-style('label-01');
73
+ display: inline-flex;
74
+ align-items: center;
75
+ gap: layout.$spacing-02;
76
+ color: $text-02;
77
+ white-space: nowrap;
78
+
79
+ svg {
80
+ fill: var(--brand-01);
81
+ }
82
+ }
83
+
84
+ /* Toolbar */
85
+
86
+ .toolbar {
87
+ display: flex;
88
+ align-items: center;
89
+ gap: layout.$spacing-05;
90
+ margin-bottom: layout.$spacing-05;
91
+ }
92
+
93
+ .searchLayer {
94
+ flex: 1 1 auto;
95
+ max-width: 24rem;
96
+ }
97
+
98
+ .resultCount {
99
+ @include type.type-style('label-01');
100
+ color: $text-02;
101
+ white-space: nowrap;
102
+ }
103
+
104
+ /* Categories */
105
+
106
+ .category {
107
+ margin-bottom: layout.$spacing-06;
108
+ }
109
+
110
+ .categoryHeader {
111
+ display: flex;
112
+ align-items: center;
113
+ gap: layout.$spacing-03;
114
+ padding-left: layout.$spacing-03;
115
+ margin-bottom: layout.$spacing-04;
116
+ border-left: layout.$spacing-02 solid $ui-04;
117
+ }
118
+
119
+ .categoryTitle {
120
+ @include type.type-style('productive-heading-01');
121
+ color: $color-gray-100;
122
+ }
123
+
124
+ .grid {
125
+ display: grid;
126
+ grid-template-columns: repeat(3, 1fr);
127
+ gap: layout.$spacing-04;
128
+ }
129
+
130
+ /* Report tiles. Selectors are nested under `.grid` so they win over the
131
+ Carbon tile styles the app shell loads globally. */
132
+
133
+ .grid .tile {
134
+ position: relative;
135
+ display: flex;
136
+ align-items: center;
137
+ gap: layout.$spacing-04;
138
+ min-height: 0;
139
+ padding: layout.$spacing-04;
140
+ background-color: $ui-02;
141
+ border: 1px solid $ui-03;
142
+ border-left: layout.$spacing-02 solid $ui-04;
143
+ text-decoration: none;
144
+ transition: all motion.$duration-fast-02 motion.motion(standard, productive);
145
+
146
+ &:hover {
147
+ background-color: $ui-02;
148
+ border-color: $ui-04;
149
+ box-shadow: 0 layout.$spacing-02 layout.$spacing-05 rgba(0, 0, 0, 0.1);
150
+
151
+ .tileArrow {
152
+ transform: translateX(layout.$spacing-02);
153
+ }
154
+ }
155
+ }
156
+
157
+ .tileIcon {
158
+ display: flex;
159
+ align-items: center;
160
+ justify-content: center;
161
+ flex-shrink: 0;
162
+ width: layout.$spacing-07;
163
+ height: layout.$spacing-07;
164
+ background-color: $ui-01;
165
+ }
166
+
167
+ .tileCopy {
168
+ min-width: 0;
169
+ }
170
+
171
+ .tileCode {
172
+ @include type.type-style('label-01');
173
+ color: $text-02;
174
+ letter-spacing: 0.04em;
175
+ }
176
+
177
+ .tileName {
178
+ @include type.type-style('productive-heading-01');
179
+ color: $color-gray-100;
180
+ }
181
+
182
+ .tileArrow {
183
+ flex-shrink: 0;
184
+ margin-left: auto;
185
+ fill: $text-02;
186
+ transition: transform motion.$duration-fast-02 motion.motion(standard, productive);
187
+ }
188
+
189
+ /* Tone accents, applied per category */
190
+
191
+ @each $name, $tone in $tones {
192
+ $accent: list.nth($tone, 1);
193
+ $tint: list.nth($tone, 2);
194
+
195
+ .categoryHeader[data-tone='#{$name}'] {
196
+ border-left-color: $accent;
197
+ }
198
+
199
+ .grid .tile[data-tone='#{$name}'] {
200
+ border-left-color: $accent;
201
+
202
+ .tileIcon {
203
+ color: $accent;
204
+ background-color: $tint;
205
+ }
206
+
207
+ &:hover {
208
+ border-color: $accent;
209
+
210
+ .tileArrow {
211
+ fill: $accent;
212
+ }
213
+ }
214
+ }
215
+ }
216
+
217
+ /* Empty state */
218
+
219
+ .emptyState {
220
+ display: flex;
221
+ flex-direction: column;
222
+ align-items: center;
223
+ gap: layout.$spacing-03;
224
+ padding: layout.$spacing-07 layout.$spacing-05;
225
+ text-align: center;
226
+ background-color: $ui-02;
227
+ border: 1px solid $ui-03;
228
+
229
+ h5 {
230
+ @include type.type-style('productive-heading-02');
231
+ color: $color-gray-100;
232
+ }
233
+
234
+ p {
235
+ @include type.type-style('body-long-01');
236
+ color: $text-02;
237
+ }
238
+ }
239
+
240
+ .emptyStateIcon {
241
+ fill: $ui-04;
242
+ }
243
+
244
+ @media (max-width: $breakpoint-tablet-max) {
245
+ .dashboard {
246
+ padding: layout.$spacing-05 0;
247
+ }
248
+
249
+ .headerMeta {
250
+ align-items: flex-start;
251
+ flex-wrap: wrap;
252
+ margin-left: 0;
253
+ }
254
+
255
+ .toolbar {
3
256
  flex-wrap: wrap;
4
- gap: 20px;
5
- margin: 2rem;
257
+ }
258
+
259
+ .searchLayer {
260
+ max-width: none;
261
+ }
262
+
263
+ .grid {
264
+ grid-template-columns: repeat(2, 1fr);
265
+ }
6
266
  }
7
267
 
8
- .tile {
9
- flex: 1 1 calc(33.333% - 40px);
10
- min-width: 5rem;
11
- max-width: 15rem;
12
- padding: 1rem;
13
- text-align: center;
14
- }
268
+ @media (max-width: $breakpoint-phone-max) {
269
+ .grid {
270
+ grid-template-columns: 1fr;
271
+ }
272
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,18 @@ import { reportsDashboardMeta } from './dashboard-meta/reports-dashboard.meta';
5
5
 
6
6
  export const moduleName = '@ampath/esm-reports-app';
7
7
 
8
+ /**
9
+ * The section this app's configuration is declared under.
10
+ *
11
+ * Not the package name: the AMPATH deployment's `o3-config.json` declares
12
+ * `@ampath/esm-reporting`, and there is no `@ampath/esm-reports-app` section in
13
+ * it. Registering the schema under the package name left `etlBaseUrl` on its
14
+ * empty default no matter what the deployment set, so every report failed with
15
+ * "the ETL base URL is not configured". Keep this in step with the config, not
16
+ * with `package.json`.
17
+ */
18
+ export const configSectionName = '@ampath/esm-reporting';
19
+
8
20
  const options = {
9
21
  featureName: 'ampath-esm-reports',
10
22
  moduleName,
@@ -13,7 +25,7 @@ const options = {
13
25
  export const importTranslation = require.context('../translations', false, /.json$/, 'lazy');
14
26
 
15
27
  export function startupApp() {
16
- defineConfigSchema(moduleName, configSchema);
28
+ defineConfigSchema(configSectionName, configSchema);
17
29
  }
18
30
  export const root = getAsyncLifecycle(() => import('./root.component'), options);
19
31
 
@@ -0,0 +1,69 @@
1
+ import {
2
+ DataTable,
3
+ type DataTableHeader,
4
+ type DataTableRow,
5
+ Table,
6
+ TableBody,
7
+ TableCell,
8
+ TableHead,
9
+ TableHeader,
10
+ TableRow,
11
+ } from '@carbon/react';
12
+ import React from 'react';
13
+
14
+ interface DatatableWrapperProps {
15
+ rows: Omit<DataTableRow<any[]>, 'cells'>[];
16
+ headers: DataTableHeader[];
17
+ subHeaders?: string[];
18
+ /** Header keys whose cells wrap instead of stretching the row (`wrap: true` columns) */
19
+ wrapKeys?: readonly string[];
20
+ /** Applied to the table, so a register can use its own stylesheet */
21
+ className?: string;
22
+ }
23
+
24
+ const DatatableWrapper: React.FC<DatatableWrapperProps> = ({
25
+ rows,
26
+ headers,
27
+ subHeaders = [],
28
+ wrapKeys = [],
29
+ className,
30
+ }) => {
31
+ return (
32
+ <DataTable rows={rows} headers={headers}>
33
+ {({ rows, headers, getTableProps, getHeaderProps, getRowProps, getCellProps }) => (
34
+ <Table {...getTableProps()} className={className}>
35
+ <TableHead>
36
+ <TableRow>
37
+ {headers.map((header) => (
38
+ <TableHeader {...getHeaderProps({ header })}>{header.header}</TableHeader>
39
+ ))}
40
+ </TableRow>
41
+ {subHeaders && subHeaders.length && (
42
+ <TableRow>
43
+ {subHeaders.map((head, i) => (
44
+ <TableHeader key={i}>{head}</TableHeader>
45
+ ))}
46
+ </TableRow>
47
+ )}
48
+ </TableHead>
49
+ <TableBody>
50
+ {rows.map((row) => (
51
+ <TableRow {...getRowProps({ row })}>
52
+ {row.cells.map((cell, i) => (
53
+ <TableCell
54
+ {...getCellProps({ cell })}
55
+ className={wrapKeys.includes(headers[i]?.key) ? 'wrapCell' : undefined}
56
+ >
57
+ {cell.value}
58
+ </TableCell>
59
+ ))}
60
+ </TableRow>
61
+ ))}
62
+ </TableBody>
63
+ </Table>
64
+ )}
65
+ </DataTable>
66
+ );
67
+ };
68
+
69
+ export default DatatableWrapper;
@@ -0,0 +1,62 @@
1
+ import React, { useMemo } from 'react';
2
+ import dayjs from 'dayjs';
3
+ import TableWrapper from '../table-wrapper/table-wrapper.component';
4
+ import TableRowMapper from '../table-wrapper/table-row-mapper.component';
5
+ import { getCell } from '../../utils/utils';
6
+ import { useLocationDetails } from '../../common/report-header';
7
+ import styles from './moh-240.scss';
8
+
9
+ interface MOH240HeaderProps {
10
+ facility?: string;
11
+ startDate?: string;
12
+ endDate?: string;
13
+ }
14
+
15
+ const asDate = (value?: string) => (value ? dayjs(value).format('DD/MM/YYYY') : '');
16
+
17
+ const MOH240Header: React.FC<MOH240HeaderProps> = ({ facility = '', startDate, endDate }) => {
18
+ const { county, subCounty, kmhflCode } = useLocationDetails();
19
+
20
+ const tableRows = useMemo(() => {
21
+ return [
22
+ {
23
+ tableCells: [getCell('', 'County:', 1, 1, true), getCell('', county ?? '', 3)],
24
+ },
25
+ {
26
+ tableCells: [getCell('', 'Sub-County:', 1, 1, true), getCell('', subCounty ?? '', 3)],
27
+ },
28
+ {
29
+ tableCells: [getCell('', 'Health Facility:', 1, 1, true), getCell('', facility, 3)],
30
+ },
31
+ {
32
+ tableCells: [getCell('', 'KMHFL Code:', 1, 1, true), getCell('', kmhflCode ?? '', 3)],
33
+ },
34
+ {
35
+ tableCells: [
36
+ getCell('', 'Type:', 1, 1, true),
37
+ getCell('', ''),
38
+ getCell('', 'Man. Agency:', 1, 1, true),
39
+ getCell('', ''),
40
+ ],
41
+ },
42
+ {
43
+ tableCells: [
44
+ getCell('', 'Start date:', 1, 1, true),
45
+ getCell('', asDate(startDate)),
46
+ getCell('', 'End date:', 1, 1, true),
47
+ getCell('', asDate(endDate)),
48
+ ],
49
+ },
50
+ ];
51
+ }, [county, subCounty, kmhflCode, facility, startDate, endDate]);
52
+
53
+ return (
54
+ <div className={styles.headerSection}>
55
+ <TableWrapper className={styles.formTable}>
56
+ <TableRowMapper tableRows={tableRows} />
57
+ </TableWrapper>
58
+ </div>
59
+ );
60
+ };
61
+
62
+ export default MOH240Header;
@@ -0,0 +1,113 @@
1
+ import { type RegisterColumn } from '../../common/report-register';
2
+ import dayjs from 'dayjs';
3
+
4
+ /** The register's columns, in the order the printed MOH 240 prints them */
5
+ export const MOH_240_HEADERS = [
6
+ { key: 'date', header: 'Date (DD/MM/YYYY)' },
7
+ { key: 'opd_ip_ref_no', header: 'OPD / IPD Ref. No.' },
8
+ { key: 'lab_no', header: 'Lab. No. (new client)' },
9
+ { key: 'revisit_no', header: 'Revisit No.' },
10
+ { key: 'full_name', header: 'Full Names (Three names)' },
11
+ { key: 'age', header: 'Age' },
12
+ { key: 'sex', header: 'Sex' },
13
+ { key: 'county_sub_county', header: 'County / Sub County' },
14
+ { key: 'village_estate_landmark', header: 'Village / Estate / Landmark' },
15
+ { key: 'telephone_number', header: 'Telephone Number' },
16
+ { key: 'clinical_diagnosis', header: 'Clinical Diagnosis' },
17
+ { key: 'prior_treatment', header: 'Prior Treatment' },
18
+ { key: 'type_of_specimen', header: 'Type of Specimen' },
19
+ { key: 'condition_of_specimen', header: 'Condition of Specimen' },
20
+ { key: 'investigation_required', header: 'Investigation Required' },
21
+ { key: 'test_datetime', header: 'Date Sample Collected' },
22
+ { key: 'date_sample_received', header: 'Date Sample Received' },
23
+ { key: 'clinician_name', header: 'Clinician Name' },
24
+ { key: 'date_sample_analysed', header: 'Date Sample Analysed' },
25
+ { key: 'results', header: 'Results' },
26
+ { key: 'date_results_dispatched', header: 'Date Results Dispatched' },
27
+ { key: 'amount_charged', header: 'Amount Charged' },
28
+ { key: 'receipt_number', header: 'Receipt Number' },
29
+ { key: 'referrals', header: 'Referrals (From Other HF / To Other HF / 3rd Tier Reference Laboratories)' },
30
+ { key: 'comments', header: 'Comments' },
31
+ { key: 'testing_officer', header: 'Name of Analysing/Testing Officer' },
32
+ { key: 'signature', header: 'Signature' },
33
+ ] as const;
34
+
35
+ /** The column letters the printed register runs across the top */
36
+ export const MOH_240_SUB_HEADERS = MOH_240_HEADERS.map((_, i) =>
37
+ i < 26 ? String.fromCharCode(65 + i) : `A${String.fromCharCode(65 + i - 26)}`,
38
+ );
39
+
40
+ /**
41
+ * Columns whose values must stay text in the CSV: identifiers lose leading
42
+ * zeros and phone numbers open as `#NAME?` formulas when Excel parses them
43
+ * as numbers.
44
+ */
45
+ export const MOH_240_TEXT_KEYS = ['lab_no', 'opd_ip_ref_no', 'telephone_number', 'receipt_number'];
46
+
47
+ /**
48
+ * Prose columns allowed to wrap on screen; everything else stays `nowrap` so
49
+ * one long value cannot stretch the 27-column row past the fold.
50
+ * `results` wraps because full-panel rows (v1.2) compose every analyte,
51
+ * including 'Not done' placeholders, into one long string.
52
+ */
53
+ export const MOH_240_WRAP_KEYS = [
54
+ 'clinical_diagnosis',
55
+ 'type_of_specimen',
56
+ 'referrals',
57
+ 'comments',
58
+ 'results',
59
+ ];
60
+
61
+ /**
62
+ * The results column carries whichever indicator the drill-down was opened for,
63
+ * so it is read from a field named by that indicator rather than from `results`.
64
+ */
65
+ export function resultFieldFor(indicator: string): string {
66
+ if (indicator.startsWith('min_') || indicator.startsWith('max_')) {
67
+ return indicator.substring(4);
68
+ }
69
+ if (indicator.startsWith('total_')) {
70
+ return indicator.substring(6);
71
+ }
72
+ if (indicator.startsWith('positive_')) {
73
+ return indicator.substring(9);
74
+ }
75
+ return indicator;
76
+ }
77
+
78
+ /**
79
+ * Formats a datetime column the way the printed form reads it. Values the
80
+ * payload already sends as `DD/MM/YYYY` pass through untouched; only raw ISO
81
+ * datetimes (`2026-08-19T10:18:14.000Z`) are reformatted, so screen and CSV
82
+ * agree no matter which shape the endpoint returns.
83
+ */
84
+ export function formatRegisterDate(value: unknown): string {
85
+ if (value === null || value === undefined || value === '') {
86
+ return '';
87
+ }
88
+ const text = String(value).trim();
89
+ return /^\d{4}-\d{2}-\d{2}/.test(text) ? dayjs(text).format('DD/MM/YYYY') : text;
90
+ }
91
+
92
+ /** Exported with the register's own headings, so the CSV matches the form */
93
+ export function moh240Columns(indicator: string): RegisterColumn[] {
94
+ const resultField = resultFieldFor(indicator);
95
+
96
+ return MOH_240_HEADERS.map(({ key, header }) => ({
97
+ header,
98
+ value: (row: Record<string, any>) => {
99
+ if (key === 'results') {
100
+ // The lab-706 register sends the row's translated result directly;
101
+ // the per-indicator field remains the fallback for payloads that
102
+ // only carry the test slots.
103
+ return row.results ?? row[resultField] ?? '';
104
+ }
105
+ if (key === 'test_datetime') {
106
+ return formatRegisterDate(row[key]);
107
+ }
108
+ return row[key] ?? '';
109
+ },
110
+ text: (MOH_240_TEXT_KEYS as readonly string[]).includes(key) || undefined,
111
+ wrap: (MOH_240_WRAP_KEYS as readonly string[]).includes(key) || undefined,
112
+ }));
113
+ }