@ampath/esm-reports-app 1.0.0-next.41 → 1.0.0-next.45

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 (183) hide show
  1. package/dist/143.css +1 -0
  2. package/dist/143.css.map +1 -0
  3. package/dist/143.js +1 -0
  4. package/dist/143.js.map +1 -0
  5. package/dist/197.js +1 -1
  6. package/dist/282.js +1 -1
  7. package/dist/282.js.map +1 -1
  8. package/dist/295.css +1 -0
  9. package/dist/295.css.map +1 -0
  10. package/dist/295.js +1 -0
  11. package/dist/295.js.map +1 -0
  12. package/dist/300.js +1 -1
  13. package/dist/311.js +1 -1
  14. package/dist/311.js.map +1 -1
  15. package/dist/335.js +1 -1
  16. package/dist/353.js +1 -1
  17. package/dist/353.js.map +1 -1
  18. package/dist/410.js +1 -0
  19. package/dist/410.js.map +1 -0
  20. package/dist/427.js +2 -0
  21. package/dist/427.js.LICENSE.txt +35 -0
  22. package/dist/427.js.map +1 -0
  23. package/dist/441.js +1 -0
  24. package/dist/441.js.map +1 -0
  25. package/dist/478.js +1 -1
  26. package/dist/478.js.map +1 -1
  27. package/dist/540.js +1 -1
  28. package/dist/540.js.map +1 -1
  29. package/dist/55.js +1 -1
  30. package/dist/652.js +1 -1
  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/report-error/index.ts +1 -0
  39. package/src/common/report-error/show-report-error.ts +25 -0
  40. package/src/common/report-filters/date-range.ts +137 -0
  41. package/src/common/report-filters/index.ts +4 -0
  42. package/src/common/report-filters/report-filters.component.tsx +281 -151
  43. package/src/common/report-filters/report-filters.scss +180 -55
  44. package/src/common/report-filters/types.ts +66 -0
  45. package/src/common/report-filters/use-restore-period.ts +43 -0
  46. package/src/common/report-page/index.ts +2 -0
  47. package/src/common/report-page/report-page.component.tsx +32 -0
  48. package/src/common/report-page/report-page.scss +45 -0
  49. package/src/common/report-page/use-page-scale.ts +50 -0
  50. package/src/common/report-placeholder/has-report-data.ts +15 -0
  51. package/src/common/report-placeholder/index.ts +2 -0
  52. package/src/common/report-placeholder/report-placeholder.component.tsx +56 -0
  53. package/src/common/report-placeholder/report-placeholder.scss +46 -0
  54. package/src/common/report-register/index.ts +5 -0
  55. package/src/common/report-register/register-columns.ts +34 -0
  56. package/src/common/report-register/register-layout.component.tsx +237 -0
  57. package/src/common/report-register/register-layout.scss +140 -0
  58. package/src/common/report-register/register-table.scss +62 -0
  59. package/src/common/report-register/report-parents.ts +27 -0
  60. package/src/common/report-register/rows-to-csv.ts +59 -0
  61. package/src/common/report-register/use-patient-list.ts +92 -0
  62. package/src/common/report-skeleton/index.ts +1 -0
  63. package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
  64. package/src/common/report-skeleton/report-skeleton.scss +29 -0
  65. package/src/config-schema.ts +9 -1
  66. package/src/dashboard/report-catalog.ts +173 -0
  67. package/src/dashboard/reports-dasboard.tsx +101 -35
  68. package/src/dashboard/reports-dashboard.module.scss +269 -11
  69. package/src/index.ts +13 -1
  70. package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +50 -48
  71. package/src/reports/moh-240/moh-240-header.component.tsx +54 -56
  72. package/src/reports/moh-240/moh-240.columns.ts +64 -0
  73. package/src/reports/moh-240/moh-240.component.tsx +65 -84
  74. package/src/reports/moh-240/moh-240.scss +36 -7
  75. package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +27 -65
  76. package/src/reports/moh-240/sub-reports/page-summary.component.tsx +2 -2
  77. package/src/reports/moh-505/moh-505-header.component.tsx +59 -57
  78. package/src/reports/moh-505/moh-505.component.tsx +51 -56
  79. package/src/reports/moh-505/moh-505.scss +116 -2
  80. package/src/reports/moh-705B/moh-204b-register.component.tsx +41 -42
  81. package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
  82. package/src/reports/moh-705B/moh-705b.component.tsx +492 -473
  83. package/src/reports/moh-705B/moh-705b.scss +110 -24
  84. package/src/reports/moh-705a/moh-705a.component.tsx +496 -476
  85. package/src/reports/moh-705a/moh-705a.scss +109 -24
  86. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +41 -42
  87. package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
  88. package/src/reports/moh-706/moh-706-header.component.tsx +58 -56
  89. package/src/reports/moh-706/moh-706.component.tsx +84 -55
  90. package/src/reports/moh-706/moh-706.scss +115 -19
  91. package/src/reports/moh-710/moh-710.component.tsx +743 -727
  92. package/src/reports/moh-710/moh-710.scss +140 -23
  93. package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +48 -43
  94. package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
  95. package/src/reports/moh-711/moh-711.component.tsx +94 -106
  96. package/src/reports/moh-711/moh711.scss +95 -64
  97. package/src/reports/moh-711/registers/moh-333-register.component.tsx +92 -94
  98. package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
  99. package/src/reports/moh-711/registers/moh-405-register.component.tsx +88 -89
  100. package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
  101. package/src/reports/moh-711/registers/moh-406-register.component.tsx +85 -88
  102. package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
  103. package/src/reports/moh-711/registers/moh-510-register.component.tsx +38 -44
  104. package/src/reports/moh-711/registers/moh-511-register.component.tsx +42 -43
  105. package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
  106. package/src/reports/moh-711/sections/anc.component.tsx +3 -3
  107. package/src/reports/moh-711/sections/cervical-cancer.component.tsx +3 -3
  108. package/src/reports/moh-711/sections/chanis.component.tsx +2 -3
  109. package/src/reports/moh-711/sections/family-planning.component.tsx +2 -2
  110. package/src/reports/moh-711/sections/gbv.component.tsx +2 -2
  111. package/src/reports/moh-711/sections/maternity.component.tsx +3 -8
  112. package/src/reports/moh-711/sections/medical-social-work.component.tsx +2 -12
  113. package/src/reports/moh-711/sections/other.component.tsx +2 -2
  114. package/src/reports/moh-711/sections/physiotherapy.component.tsx +2 -2
  115. package/src/reports/moh-711/sections/pnc.component.tsx +7 -5
  116. package/src/reports/moh-711/sections/post-abortion.component.tsx +2 -2
  117. package/src/reports/moh-711/sections/rehabilitation.component.tsx +2 -2
  118. package/src/reports/moh-711/sections/report-compiled-by.component.tsx +12 -4
  119. package/src/reports/moh-711/sections/tb-screening.component.tsx +2 -2
  120. package/src/reports/moh-717/moh-717.component.tsx +100 -121
  121. package/src/reports/moh-717/moh717.scss +163 -102
  122. package/src/reports/moh-717/sections/maternity.component.tsx +1 -1
  123. package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +1 -1
  124. package/src/reports/moh-717/sections/outpatient.component.tsx +23 -23
  125. package/src/reports/moh-717/sections/preparedby.component.tsx +7 -2
  126. package/src/reports/moh-717/sections/special-services.component.tsx +1 -1
  127. package/src/reports/moh-731/moh-731.component.tsx +127 -110
  128. package/src/reports/moh-731/moh-731.scss +396 -68
  129. package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
  130. package/src/reports/moh-731/registers/moh-366.component.tsx +76 -83
  131. package/src/reports/moh-731/registers/prep-register.component.tsx +70 -78
  132. package/src/reports/moh-731/registers/prep.columns.ts +34 -0
  133. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1031 -751
  134. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +230 -86
  135. package/src/reports/moh-731/sections/pmtct.component.tsx +185 -64
  136. package/src/reports/moh-740/moh-740.component.scss +159 -79
  137. package/src/reports/moh-740/moh-740.component.tsx +261 -230
  138. package/src/reports/moh-740/moh-740.resource.ts +17 -10
  139. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +77 -96
  140. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +50 -70
  141. package/src/reports/moh-740/registers/moh-740-patient-list.tsx +61 -32
  142. package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
  143. package/src/reports/moh-740/shared/utils/indicator-register-map.ts +3 -3
  144. package/src/reports/moh-740/types/index.ts +3 -3
  145. package/src/reports/moh-745/moh-745.component.tsx +897 -877
  146. package/src/reports/moh-745/moh-745.scss +104 -46
  147. package/src/reports/moh-745/registers/moh-412-register.component.tsx +42 -43
  148. package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
  149. package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
  150. package/src/reports/table-wrapper/table-wrapper.scss +1 -16
  151. package/src/resources/moh-505.resource.ts +18 -35
  152. package/src/resources/moh-705.resource.ts +28 -39
  153. package/src/resources/moh-706.resource.ts +17 -30
  154. package/src/resources/moh-710.resource.ts +7 -17
  155. package/src/resources/moh-711.resource.ts +46 -64
  156. package/src/resources/moh-717.resource.ts +7 -17
  157. package/src/resources/moh-731.resource.ts +26 -32
  158. package/src/resources/moh-745.resource.ts +18 -20
  159. package/src/resources/patient-list-page.ts +66 -0
  160. package/src/root.component.tsx +28 -26
  161. package/src/root.scss +36 -0
  162. package/src/utils/get-base-url.ts +39 -7
  163. package/tsconfig.json +1 -1
  164. package/dist/110.js +0 -1
  165. package/dist/110.js.map +0 -1
  166. package/dist/184.js +0 -2
  167. package/dist/184.js.LICENSE.txt +0 -14
  168. package/dist/184.js.map +0 -1
  169. package/dist/205.js +0 -1
  170. package/dist/205.js.map +0 -1
  171. package/dist/812.js +0 -1
  172. package/dist/812.js.map +0 -1
  173. package/dist/822.js +0 -2
  174. package/dist/822.js.LICENSE.txt +0 -20
  175. package/dist/822.js.map +0 -1
  176. package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -560
  177. package/dist/main.js +0 -1
  178. package/dist/main.js.map +0 -1
  179. package/src/reports/moh-731/registers/moh-366.scss +0 -45
  180. package/src/reports/moh-731/registers/prep-registers.scss +0 -45
  181. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.scss +0 -18
  182. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.scss +0 -18
  183. /package/src/{reports/moh-740/shared/utils → utils}/format-indicator.ts +0 -0
@@ -1,91 +1,216 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
1
4
 
5
+ .filters {
6
+ padding: layout.$spacing-05 0 0;
7
+ }
2
8
 
3
- .filtersContainer {
4
- margin: 0 2rem;
5
- margin-bottom: 0.5rem;
6
- border: 1px solid #ccc;
7
- padding: 1rem;
9
+ /* Breadcrumb trail. The trailing crumb is the page heading, so the items are
10
+ baseline-aligned rather than centred on differing font sizes. */
11
+
12
+ .breadcrumb {
13
+ margin-bottom: layout.$spacing-04;
14
+
15
+ :global(.cds--breadcrumb-item) {
16
+ display: inline-flex;
17
+ align-items: baseline;
18
+ }
8
19
  }
9
20
 
21
+ /* An `h3` inside the trailing crumb, so it keeps heading semantics without
22
+ repeating the report name in a separate title row. */
10
23
  .title {
11
- font-size: 1.5rem;
12
- font-weight: bold;
13
- margin-top: 1rem;
14
- color: green;
15
- text-decoration: underline;
24
+ @include type.type-style('productive-heading-03');
25
+ display: inline;
26
+ color: $color-gray-100;
16
27
  }
17
28
 
18
- .titleContainer {
19
- margin: 2rem;
29
+ /* Filter card */
30
+
31
+ .filtersContainer {
32
+ padding: layout.$spacing-04 layout.$spacing-05;
33
+ background-color: $ui-02;
34
+ border: 1px solid $ui-03;
35
+ border-left: layout.$spacing-02 solid var(--brand-01);
20
36
  }
21
37
 
22
- .dateContainer {
23
- display: grid;
24
- grid-template-columns: 1fr 1fr;
25
- gap: 8px 16px;
26
- align-items: center;
38
+ .fields {
39
+ display: flex;
40
+ flex-wrap: wrap;
41
+ align-items: flex-end;
42
+ gap: layout.$spacing-05;
43
+ margin-bottom: layout.$spacing-04;
44
+ }
45
+
46
+ /* Nested so it beats the `flex: 0 1 auto` that Carbon puts on `.cds--form-item`.
47
+
48
+ Fields grow into the row but stop at a width suited to what they hold. Left
49
+ to share the row unbounded, a monthly report -- which shows only Year and
50
+ Month -- gave a four-digit year a 434px box and the month 650px. The cap
51
+ keeps every control a sensible size and leaves the slack at the end of the
52
+ row rather than inside the inputs. */
53
+
54
+ .fields .field {
55
+ flex: 1 1 auto;
56
+ min-width: 0;
57
+ }
58
+
59
+ .fields .periodField {
60
+ min-width: 14rem;
61
+ max-width: 16rem;
62
+ }
63
+
64
+ /* Wide enough for the validation message, not just the value. Carbon renders
65
+ `invalidText` under the input at the field's own width, so a field sized to
66
+ "2026" or "September" makes "That month has not started yet." wrap onto a
67
+ second line and jog the row below it. */
68
+
69
+ .fields .yearField {
70
+ min-width: 7rem;
71
+ max-width: 9rem;
72
+ }
73
+
74
+ .fields .monthField {
75
+ min-width: 9rem;
76
+ max-width: 13rem;
77
+ }
78
+
79
+ /* The range picker holds two inputs, so it is allowed roughly twice the width */
80
+ .fields .rangeField {
81
+ min-width: 16rem;
82
+ max-width: 24rem;
27
83
  }
28
84
 
29
- .dateContainer label {
30
- font-weight: 500;
85
+ /* Matches Carbon's `.cds--label` so the switcher's label sits on the same
86
+ baseline as the labels Carbon renders for the selects. */
87
+ .fieldLabel {
88
+ @include type.type-style('label-01');
89
+ display: inline-block;
90
+ color: $text-02;
91
+ line-height: 1rem;
92
+ margin-bottom: layout.$spacing-03;
93
+ vertical-align: baseline;
31
94
  }
32
95
 
33
- .input {
96
+ /* Carbon sizes these to their content, so they need telling to fill the field */
97
+
98
+ .switcher,
99
+ .fields .field :global(.cds--select-input) {
34
100
  width: 100%;
35
- padding: 6px 8px;
36
- box-sizing: border-box;
37
- border: 1px solid #ccc;
38
- outline: none;
39
101
  }
40
- .input:focus {
41
- border-color: #999;
102
+
103
+ .datePicker {
104
+ display: flex;
105
+ gap: layout.$spacing-05;
106
+ width: 100%;
42
107
  }
43
108
 
44
- .buttonContainer {
45
- display: flex;
46
- justify-content: flex-end;
47
- gap: 10px;
48
- margin: 0 2rem;
109
+ /* Carbon pins `inline-size: 143.5px` on both the container and the input in
110
+ range mode. Overridden through the range class so this wins on specificity
111
+ rather than on stylesheet order. */
112
+ .fields .datePicker:global(.cds--date-picker--range) {
113
+ :global(.cds--date-picker-container) {
114
+ flex: 1 1 0;
115
+ inline-size: auto;
116
+ min-inline-size: 0;
117
+ }
118
+
119
+ :global(.cds--date-picker__input) {
120
+ inline-size: 100%;
121
+ min-inline-size: 0;
122
+ }
49
123
  }
50
124
 
51
- .tabsContainer {
125
+ /* Action row */
126
+
127
+ .actions {
52
128
  display: flex;
53
- gap: 1rem;
54
- margin-bottom: 1rem;
55
129
  align-items: center;
130
+ justify-content: space-between;
131
+ flex-wrap: wrap;
132
+ gap: layout.$spacing-04;
133
+ padding-top: layout.$spacing-04;
134
+ border-top: 1px solid $ui-03;
56
135
  }
57
136
 
58
- .tab,
59
- .activeTab {
60
- display: flex;
137
+ .summary {
138
+ @include type.type-style('body-short-01');
139
+ display: inline-flex;
61
140
  align-items: center;
62
- justify-content: center;
63
- min-height: 2.5rem;
141
+ gap: layout.$spacing-03;
142
+ color: $text-02;
143
+
144
+ svg {
145
+ fill: var(--brand-01);
146
+ }
147
+
148
+ strong {
149
+ color: $color-gray-100;
150
+ font-weight: 600;
151
+ }
64
152
  }
65
153
 
66
- .tab {
154
+ .actionButtons {
67
155
  display: flex;
68
156
  align-items: center;
69
- justify-content: center;
157
+ gap: layout.$spacing-03;
158
+ margin-left: auto;
159
+ }
70
160
 
71
- min-height: 2.5rem;
161
+ /* The filter bar is application chrome, not part of the report */
72
162
 
73
- background: transparent;
74
- color: green;
163
+ @media print {
164
+ .filters {
165
+ display: none;
166
+ }
167
+ }
75
168
 
76
- border: 1px solid black;
77
- cursor: pointer;
169
+ @media (max-width: $breakpoint-tablet-max) {
170
+ .filters {
171
+ padding: layout.$spacing-05 0 0;
172
+ }
173
+
174
+ .fields,
175
+ .datePicker {
176
+ flex-direction: column;
177
+ align-items: stretch;
178
+ }
179
+
180
+ .fields .field,
181
+ .switcher {
182
+ min-width: 0;
183
+ width: 100%;
184
+ }
185
+
186
+ .actionButtons {
187
+ margin-left: 0;
188
+ width: 100%;
189
+
190
+ button {
191
+ flex: 1 1 0;
192
+ }
193
+ }
78
194
  }
79
195
 
80
- .activeTab {
81
- display: flex;
82
- align-items: center;
83
- justify-content: center;
196
+ /* The summary doubles as the blocking reason when the period is not valid */
197
+ .summaryInvalid {
198
+ color: $danger;
199
+
200
+ svg {
201
+ fill: $danger;
202
+ }
203
+ }
84
204
 
85
- min-height: 2.5rem;
205
+ /* The filters no longer match the report on screen. Warning-toned rather than
206
+ error-toned: nothing is wrong, the figures are simply for another period. */
86
207
 
87
- background: green;
88
- color: white;
208
+ .summaryStale {
209
+ padding: layout.$spacing-02 layout.$spacing-03;
210
+ color: $color-gray-100;
211
+ background-color: $warning-background;
89
212
 
90
- border: 1px solid black;
91
- }
213
+ svg {
214
+ fill: $color-gray-70;
215
+ }
216
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Which period controls a report offers.
3
+ *
4
+ * `monthly` suits returns that are only ever compiled for a calendar month --
5
+ * offering a date range there invites a period the return is not defined for.
6
+ * `range` suits reports read over arbitrary dates. `both` lets the user choose.
7
+ */
8
+ export type ReportFilterMode =
9
+ | 'monthly' // year and month only
10
+ | 'range' // start and end date only
11
+ | 'both'; // a switcher between the two
12
+
13
+ export interface FacilityOption {
14
+ value: string;
15
+ label: string;
16
+ }
17
+
18
+ /**
19
+ * What the filter bar hands back when the user generates a report.
20
+ *
21
+ * `startDate` and `endDate` are always resolved to `YYYY-MM-DD`, whichever way
22
+ * the period was picked, so a report never has to expand a month into a range
23
+ * itself.
24
+ */
25
+ export interface ReportPeriod {
26
+ startDate: string;
27
+ endDate: string;
28
+ /** Set only when the user picked a whole month, as `YYYY-MM` */
29
+ month?: string;
30
+ /** Set only when the caller supplied `facilities` */
31
+ facility?: string;
32
+ /** Human readable period, e.g. `June 2026` or `01 Jun 2026 to 14 Jun 2026` */
33
+ label: string;
34
+ }
35
+
36
+ export interface ReportFiltersProps {
37
+ /** Shown as the page title, e.g. `MOH-731 Report` */
38
+ reportName: string;
39
+ /** Defaults to `both`. See {@link ReportFilterMode}. */
40
+ mode?: ReportFilterMode;
41
+ /** Renders a facility picker when supplied. Omit to hide it. */
42
+ facilities?: FacilityOption[];
43
+ /** How many years the year dropdown offers, counting back from now */
44
+ yearsBack?: number;
45
+ /** Disables the generate button and shows a pending label */
46
+ isLoading?: boolean;
47
+ /** Reveals the Download PDF button beside Generate once a report is on screen */
48
+ isReportGenerated?: boolean;
49
+ /**
50
+ * The period the report currently on screen was generated for.
51
+ *
52
+ * Reports that restore their data from session storage come back with a
53
+ * report showing but no record here of which period produced it. Passing it
54
+ * lets the filter bar flag stale figures on the first change after a reload,
55
+ * rather than only after the user has generated once in this session.
56
+ */
57
+ generatedPeriod?: { startDate?: string; endDate?: string };
58
+ onGenerate: (period: ReportPeriod) => void;
59
+ /**
60
+ * Fires whenever the selected period changes, before Generate is pressed.
61
+ *
62
+ * Lets a report show the period on its form as soon as it is chosen, rather
63
+ * than only once figures have been fetched for it.
64
+ */
65
+ onPeriodChange?: (period: ReportPeriod) => void;
66
+ }
@@ -0,0 +1,43 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import { useSearchParams } from 'react-router-dom';
3
+
4
+ import { describePeriod } from './date-range';
5
+ import { type ReportPeriod } from './types';
6
+
7
+ /**
8
+ * Regenerates a report when its URL carries a period.
9
+ *
10
+ * A register's breadcrumb links back to the report the figure came from, and
11
+ * carries the period it was generated for. Without this the user lands on the
12
+ * report's "not generated yet" placeholder and has to pick the same month
13
+ * again -- the trail leads back to the page but not to what was on it.
14
+ *
15
+ * Reports that keep their figures in session storage restore themselves
16
+ * already; this is for the ones that do not.
17
+ */
18
+ export function useRestorePeriod(onRestore: (period: ReportPeriod) => void) {
19
+ const [searchParams] = useSearchParams();
20
+
21
+ const startDate = searchParams.get('startDate');
22
+ const endDate = searchParams.get('endDate');
23
+ const month = searchParams.get('month') ?? undefined;
24
+
25
+ /* Keyed on the period rather than a bare "has run" flag, so following a
26
+ second register back to a different month still regenerates. */
27
+ const restored = useRef<string | null>(null);
28
+
29
+ useEffect(() => {
30
+ if (!startDate || !endDate) {
31
+ return;
32
+ }
33
+
34
+ const key = `${startDate}|${endDate}`;
35
+
36
+ if (restored.current === key) {
37
+ return;
38
+ }
39
+
40
+ restored.current = key;
41
+ onRestore({ startDate, endDate, month, label: describePeriod(startDate, endDate, month) });
42
+ }, [startDate, endDate, month, onRestore]);
43
+ }
@@ -0,0 +1,2 @@
1
+ export { default as ReportPage } from './report-page.component';
2
+ export * from './use-page-scale';
@@ -0,0 +1,32 @@
1
+ import React from 'react';
2
+
3
+ import { usePageScale } from './use-page-scale';
4
+ import styles from './report-page.scss';
5
+
6
+ interface ReportPageProps {
7
+ children: React.ReactNode;
8
+ }
9
+
10
+ /**
11
+ * An A4 sheet that fills the width it is given.
12
+ *
13
+ * Reports that mirror a paper return are drawn at true A4 so they print 1:1,
14
+ * but a fixed page wastes most of a wide monitor. The sheet is scaled up to
15
+ * fill its container and floored at A4, below which the viewport scrolls
16
+ * instead -- shrinking a dense return past its paper size makes it unreadable.
17
+ */
18
+ const ReportPage: React.FC<ReportPageProps> = ({ children }) => {
19
+ const { ref, scale } = usePageScale<HTMLDivElement>();
20
+
21
+ return (
22
+ <div className={styles.viewport} ref={ref}>
23
+ {/* a custom property rather than an inline `zoom`, so the print
24
+ stylesheet can still reset it to 1 */}
25
+ <div className={styles.sheet} style={{ '--page-zoom': scale } as React.CSSProperties}>
26
+ {children}
27
+ </div>
28
+ </div>
29
+ );
30
+ };
31
+
32
+ export default ReportPage;
@@ -0,0 +1,45 @@
1
+ @use '@openmrs/esm-styleguide/src/vars' as *;
2
+
3
+ /* A4 portrait, in the units the paper is measured in, so the sheet occupies
4
+ exactly one page on screen and prints at 1:1. */
5
+
6
+ $page-w: 210mm;
7
+ $page-h: 297mm;
8
+ $page-margin: 10mm;
9
+
10
+ .viewport {
11
+ overflow-x: auto;
12
+ }
13
+
14
+ .sheet {
15
+ box-sizing: border-box;
16
+ width: $page-w;
17
+ min-height: $page-h;
18
+ padding: $page-margin;
19
+ margin: 0 auto;
20
+ background-color: #fff;
21
+ box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
22
+ /* set by `usePageScale` from the measured container width, floored at 1 */
23
+ zoom: var(--page-zoom, 1);
24
+ }
25
+
26
+ @media print {
27
+ @page {
28
+ size: A4 portrait;
29
+ margin: $page-margin;
30
+ }
31
+
32
+ .viewport {
33
+ overflow: visible;
34
+ }
35
+
36
+ .sheet {
37
+ width: auto;
38
+ min-height: 0;
39
+ padding: 0;
40
+ margin: 0;
41
+ box-shadow: none;
42
+ /* paper geometry stands on its own; magnification is for screens */
43
+ zoom: 1;
44
+ }
45
+ }
@@ -0,0 +1,50 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ /** A4 portrait width, 210mm, in CSS pixels at the reference 96dpi */
4
+ const A4_WIDTH_PX = (210 * 96) / 25.4;
5
+
6
+ /**
7
+ * Scales the sheet to fill whatever width it is given, never going below A4.
8
+ *
9
+ * The report is drawn at true A4 so it prints at 1:1, but a fixed page wastes
10
+ * most of a wide monitor and is hard to read. This measures the containing
11
+ * element and returns the factor that makes the page fill it, floored at 1 so
12
+ * the sheet is never rendered smaller than the paper it represents -- below
13
+ * that width the container scrolls instead, which keeps the form legible rather
14
+ * than shrinking it into illegibility.
15
+ *
16
+ * A callback ref rather than `useRef` + `useEffect`, because the page it
17
+ * measures only mounts once a report has been generated: a mount effect would
18
+ * run while the placeholder is still showing, find nothing to measure, and
19
+ * never run again -- leaving the generated sheet stuck at A4.
20
+ *
21
+ * Returned as a factor for `zoom`, which scales the layout box as well as the
22
+ * paint, so the scroll container stays the right size around the page.
23
+ */
24
+ export function usePageScale<T extends HTMLElement>() {
25
+ const [scale, setScale] = useState(1);
26
+ const observer = useRef<ResizeObserver | null>(null);
27
+
28
+ const ref = useCallback((node: T | null) => {
29
+ observer.current?.disconnect();
30
+
31
+ if (!node) {
32
+ return;
33
+ }
34
+
35
+ const measure = () => {
36
+ // clientWidth excludes the scrollbar, so the page cannot chase a
37
+ // scrollbar it has just caused
38
+ setScale(Math.max(1, node.clientWidth / A4_WIDTH_PX));
39
+ };
40
+
41
+ measure();
42
+
43
+ observer.current = new ResizeObserver(measure);
44
+ observer.current.observe(node);
45
+ }, []);
46
+
47
+ useEffect(() => () => observer.current?.disconnect(), []);
48
+
49
+ return { ref, scale };
50
+ }
@@ -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,2 @@
1
+ export { default as ReportPlaceholder } from './report-placeholder.component';
2
+ export * from './has-report-data';
@@ -0,0 +1,56 @@
1
+ import React from 'react';
2
+ import { Layer } from '@carbon/react';
3
+ import { Report } from '@carbon/react/icons';
4
+
5
+ import styles from './report-placeholder.scss';
6
+
7
+ interface ReportPlaceholderProps {
8
+ /** Named so the guidance says which return is about to be produced */
9
+ reportName?: string;
10
+ /** Shown when a report was generated but came back with nothing in it */
11
+ isEmptyResult?: boolean;
12
+ }
13
+
14
+ /**
15
+ * Stands in for the report template until a report has actually been generated,
16
+ * and explains how to produce one.
17
+ *
18
+ * Report templates are large pre-printed MOH forms; rendering one with every
19
+ * value blank reads as "the facility reported zeroes" rather than "you have not
20
+ * run this yet", so the template is held back until there is data to put in it.
21
+ * That leaves the page empty, hence the guidance here.
22
+ */
23
+ const ReportPlaceholder: React.FC<ReportPlaceholderProps> = ({ reportName, isEmptyResult = false }) => (
24
+ <Layer className={styles.placeholder}>
25
+ <Report size={32} className={styles.icon} />
26
+
27
+ {isEmptyResult ? (
28
+ <>
29
+ <h5>No records for this period</h5>
30
+ <p>
31
+ {reportName ?? 'The report'} ran successfully but returned no data for the period you selected. Try a
32
+ different reporting period, or confirm that data has been entered for this facility.
33
+ </p>
34
+ </>
35
+ ) : (
36
+ <>
37
+ <h5>No report generated yet</h5>
38
+ <p>
39
+ {reportName ? `The ${reportName} form` : 'The report'} will appear here, filled in with your facility&rsquo;s
40
+ figures, once you generate it.
41
+ </p>
42
+ <ol className={styles.steps}>
43
+ <li>Choose the reporting period in the filters above.</li>
44
+ <li>
45
+ Select <strong>Generate report</strong>.
46
+ </li>
47
+ <li>
48
+ Use <strong>Download PDF</strong> to save or print the completed form.
49
+ </li>
50
+ </ol>
51
+ </>
52
+ )}
53
+ </Layer>
54
+ );
55
+
56
+ export default ReportPlaceholder;
@@ -0,0 +1,46 @@
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
+ margin-bottom: layout.$spacing-05;
12
+ text-align: center;
13
+ background-color: $ui-02;
14
+ border: 1px solid $ui-03;
15
+
16
+ h5 {
17
+ @include type.type-style('productive-heading-02');
18
+ color: $color-gray-100;
19
+ }
20
+
21
+ p {
22
+ @include type.type-style('body-long-01');
23
+ color: $text-02;
24
+ }
25
+ }
26
+
27
+ .icon {
28
+ fill: $ui-04;
29
+ }
30
+
31
+ /* The steps that produce a report. Left aligned inside the centred card: a
32
+ numbered sequence is read down its left edge, not from a ragged centre. */
33
+
34
+ .steps {
35
+ max-width: 28rem;
36
+ margin-top: layout.$spacing-03;
37
+ padding-left: layout.$spacing-06;
38
+ text-align: left;
39
+ list-style: decimal;
40
+
41
+ li {
42
+ @include type.type-style('body-long-01');
43
+ color: $text-02;
44
+ margin-bottom: layout.$spacing-02;
45
+ }
46
+ }
@@ -0,0 +1,5 @@
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';
@@ -0,0 +1,34 @@
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
+
7
+ /** The age bands the registers tabulate against, in the order they appear */
8
+ export const AGE_BANDS = [
9
+ { label: '< 1 yrs', min: 0, max: 0 },
10
+ { label: '1-4 yrs', min: 1, max: 4 },
11
+ { label: '5-9 yrs', min: 5, max: 9 },
12
+ { label: '10-14 yrs', min: 10, max: 14 },
13
+ { label: '15-19 yrs', min: 15, max: 19 },
14
+ { label: '20-24 yrs', min: 20, max: 24 },
15
+ { label: '25+ yrs', min: 25, max: Number.POSITIVE_INFINITY },
16
+ ] as const;
17
+
18
+ /**
19
+ * Expands one indicator into the fourteen age-band by gender columns the
20
+ * register prints for it.
21
+ *
22
+ * The registers spell these out as fourteen near-identical cells; describing
23
+ * the rule once keeps the export in step with the table without restating it
24
+ * fourteen times per block.
25
+ */
26
+ export function ageBandColumns(heading: string, value: (row: Record<string, any>) => unknown): RegisterColumn[] {
27
+ return AGE_BANDS.flatMap((band) =>
28
+ (['M', 'F'] as const).map((gender) => ({
29
+ header: `${heading} ${band.label} (${gender})`,
30
+ value: (row: Record<string, any>) =>
31
+ row.age >= band.min && row.age <= band.max && row.gender === gender ? value(row) : '',
32
+ })),
33
+ );
34
+ }