@ampath/esm-reports-app 1.0.0-next.55 → 1.0.0-next.57
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.
- package/dist/295.js +1 -1
- package/dist/410.js +1 -1
- package/dist/580.js +1 -0
- package/dist/580.js.map +1 -0
- package/dist/907.js +2 -1
- package/dist/907.js.map +1 -1
- package/dist/998.css +1 -0
- package/dist/998.css.map +1 -0
- package/dist/998.js +1 -0
- package/dist/998.js.map +1 -0
- package/dist/ampath-esm-reports-app.js +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/common/daily-summary-table.tsx +55 -0
- package/src/common/linelist-table/index.ts +3 -0
- package/src/common/linelist-table/linelist-columns.ts +48 -0
- package/src/common/linelist-table/linelist-table.component.tsx +442 -0
- package/src/common/linelist-table/linelist-table.scss +306 -0
- package/src/common/linelist-table/linelist-table.test.tsx +120 -0
- package/src/common/linelist-table/use-all-rows.ts +93 -0
- package/src/common/linelist-table/use-column-layout.ts +123 -0
- package/src/common/report-filters/report-filters.component.tsx +11 -5
- package/src/common/report-filters/report-filters.scss +63 -7
- package/src/common/report-page/use-page-scale.ts +27 -17
- package/src/common/report-placeholder/index.ts +1 -0
- package/src/common/report-placeholder/report-placeholder.component.tsx +57 -32
- package/src/common/report-placeholder/report-placeholder.scss +2 -0
- package/src/common/report-placeholder/use-generated-location.ts +62 -0
- package/src/common/report-register/index.ts +1 -0
- package/src/common/report-register/register-layout.component.tsx +26 -4
- package/src/common/report-register/register-layout.scss +10 -0
- package/src/common/report-register/use-location-name.ts +54 -0
- package/src/reports/moh-505/moh-505.component.tsx +9 -3
- package/src/reports/moh-705B/moh-204b-register.component.tsx +11 -2
- package/src/reports/moh-705B/moh-705b.component.tsx +533 -418
- package/src/reports/moh-705B/moh-705b.scss +44 -2
- package/src/reports/moh-705a/moh-705a.component.tsx +533 -421
- package/src/reports/moh-705a/moh-705a.scss +44 -2
- package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +12 -3
- package/src/reports/moh-706/moh-706.component.tsx +9 -3
- package/src/reports/moh-710/moh-710.component.tsx +9 -3
- package/src/reports/moh-711/moh-711.component.tsx +9 -3
- package/src/reports/moh-717/moh-717.component.tsx +12 -3
- package/src/reports/moh-731/linelist/moh-731-indicators.ts +1040 -0
- package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +423 -0
- package/src/reports/moh-731/linelist/moh-731-linelist.component.tsx +102 -0
- package/src/reports/moh-731/linelist/use-open-moh-731-linelist.ts +50 -0
- package/src/reports/moh-731/moh-731.component.tsx +138 -85
- package/src/reports/moh-731/moh-731.scss +51 -4
- package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +74 -77
- package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +84 -70
- package/src/reports/moh-731/sections/pmtct.component.tsx +43 -43
- package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
- package/src/reports/moh-731/summary/moh-731-summary.config.ts +520 -0
- package/src/reports/moh-731/summary/moh-731-summary.scss +242 -0
- package/src/reports/moh-740/moh-740.component.scss +3 -1
- package/src/reports/moh-740/moh-740.component.tsx +14 -4
- package/src/reports/moh-745/moh-745.component.tsx +7 -3
- package/src/root.component.tsx +2 -0
- package/webpack.config.js +22 -1
- package/dist/427.js +0 -2
- package/dist/427.js.map +0 -1
- package/dist/441.js +0 -1
- package/dist/441.js.map +0 -1
- package/dist/907.css +0 -1
- package/dist/907.css.map +0 -1
- /package/dist/{427.js.LICENSE.txt → 907.js.LICENSE.txt} +0 -0
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
padding: layout.$spacing-04 layout.$spacing-05;
|
|
33
33
|
background-color: $ui-02;
|
|
34
34
|
border: 1px solid $ui-03;
|
|
35
|
-
border-left: layout.$spacing-02 solid var(--brand-01);
|
|
36
35
|
}
|
|
37
36
|
|
|
38
37
|
.fields {
|
|
@@ -107,9 +106,13 @@
|
|
|
107
106
|
}
|
|
108
107
|
|
|
109
108
|
/* Carbon pins `inline-size: 143.5px` on both the container and the input in
|
|
110
|
-
range mode
|
|
111
|
-
|
|
112
|
-
|
|
109
|
+
range mode, so the two dates never fill the field. `.datePicker` lands on
|
|
110
|
+
the form-item wrapper, not on the range element itself, so the range is
|
|
111
|
+
matched as its descendant. */
|
|
112
|
+
.fields .datePicker :global(.cds--date-picker--range) {
|
|
113
|
+
gap: layout.$spacing-05;
|
|
114
|
+
width: 100%;
|
|
115
|
+
|
|
113
116
|
:global(.cds--date-picker-container) {
|
|
114
117
|
flex: 1 1 0;
|
|
115
118
|
inline-size: auto;
|
|
@@ -136,18 +139,26 @@
|
|
|
136
139
|
|
|
137
140
|
.summary {
|
|
138
141
|
@include type.type-style('body-short-01');
|
|
139
|
-
display:
|
|
140
|
-
|
|
142
|
+
display: flex;
|
|
143
|
+
flex: 1 1 18rem;
|
|
144
|
+
align-items: flex-start;
|
|
141
145
|
gap: layout.$spacing-03;
|
|
146
|
+
min-width: 0;
|
|
147
|
+
margin: 0;
|
|
142
148
|
color: $text-02;
|
|
143
149
|
|
|
150
|
+
/* level with the first line however many lines the message wraps to */
|
|
144
151
|
svg {
|
|
152
|
+
flex-shrink: 0;
|
|
153
|
+
margin-top: 0.0625rem;
|
|
145
154
|
fill: var(--brand-01);
|
|
146
155
|
}
|
|
147
156
|
|
|
157
|
+
/* a period such as "01 Sep 2026 to 30 Sep 2026" never breaks mid-range */
|
|
148
158
|
strong {
|
|
149
159
|
color: $color-gray-100;
|
|
150
160
|
font-weight: 600;
|
|
161
|
+
white-space: nowrap;
|
|
151
162
|
}
|
|
152
163
|
}
|
|
153
164
|
|
|
@@ -206,7 +217,7 @@
|
|
|
206
217
|
error-toned: nothing is wrong, the figures are simply for another period. */
|
|
207
218
|
|
|
208
219
|
.summaryStale {
|
|
209
|
-
padding: layout.$spacing-
|
|
220
|
+
padding: layout.$spacing-03 layout.$spacing-04;
|
|
210
221
|
color: $color-gray-100;
|
|
211
222
|
background-color: $warning-background;
|
|
212
223
|
|
|
@@ -214,3 +225,48 @@
|
|
|
214
225
|
fill: $color-gray-70;
|
|
215
226
|
}
|
|
216
227
|
}
|
|
228
|
+
|
|
229
|
+
/* Stacked fields fill the card. The desktop caps above (a year box no wider
|
|
230
|
+
than a year needs) would otherwise leave each stacked field a different,
|
|
231
|
+
arbitrary width. Placed after them so it wins at equal specificity. */
|
|
232
|
+
@media (max-width: $breakpoint-tablet-max) {
|
|
233
|
+
.fields .periodField,
|
|
234
|
+
.fields .yearField,
|
|
235
|
+
.fields .monthField,
|
|
236
|
+
.fields .rangeField {
|
|
237
|
+
max-width: none;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/* Carbon caps buttons at 20rem; sharing the row, they should split it */
|
|
241
|
+
.actionButtons :global(.cds--btn) {
|
|
242
|
+
max-width: none;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/* Phones: one column throughout. Side by side, "Download PDF" and "Generate
|
|
247
|
+
report" each wrapped onto two lines and together overran the card. */
|
|
248
|
+
@media (max-width: $breakpoint-phone-max) {
|
|
249
|
+
.filtersContainer {
|
|
250
|
+
padding: layout.$spacing-04;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
.actions {
|
|
254
|
+
flex-direction: column;
|
|
255
|
+
align-items: stretch;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* Its flex basis is a width on the row; stacked, it would become a height */
|
|
259
|
+
.summary {
|
|
260
|
+
flex: none;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
.actionButtons {
|
|
264
|
+
flex-direction: column;
|
|
265
|
+
align-items: stretch;
|
|
266
|
+
|
|
267
|
+
:global(.cds--btn) {
|
|
268
|
+
flex: none;
|
|
269
|
+
width: 100%;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
2
|
|
|
3
|
-
/** A4 portrait width
|
|
4
|
-
const
|
|
3
|
+
/** A4 portrait width */
|
|
4
|
+
const A4_WIDTH_MM = 210;
|
|
5
|
+
|
|
6
|
+
/** Millimetres to CSS pixels at the reference 96dpi */
|
|
7
|
+
const mmToPx = (mm: number) => (mm * 96) / 25.4;
|
|
5
8
|
|
|
6
9
|
/**
|
|
7
10
|
* Scales the sheet to fill whatever width it is given, never going below A4.
|
|
@@ -20,29 +23,36 @@ const A4_WIDTH_PX = (210 * 96) / 25.4;
|
|
|
20
23
|
*
|
|
21
24
|
* Returned as a factor for `zoom`, which scales the layout box as well as the
|
|
22
25
|
* paint, so the scroll container stays the right size around the page.
|
|
26
|
+
*
|
|
27
|
+
* `pageWidthMm` is the width of the sheet as drawn on screen: the full A4 by
|
|
28
|
+
* default, or less for a sheet drawn without its paper margins (they are added
|
|
29
|
+
* back only for print).
|
|
23
30
|
*/
|
|
24
|
-
export function usePageScale<T extends HTMLElement>() {
|
|
31
|
+
export function usePageScale<T extends HTMLElement>(pageWidthMm = A4_WIDTH_MM) {
|
|
25
32
|
const [scale, setScale] = useState(1);
|
|
26
33
|
const observer = useRef<ResizeObserver | null>(null);
|
|
27
34
|
|
|
28
|
-
const ref = useCallback(
|
|
29
|
-
|
|
35
|
+
const ref = useCallback(
|
|
36
|
+
(node: T | null) => {
|
|
37
|
+
observer.current?.disconnect();
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
39
|
+
if (!node) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
34
42
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
43
|
+
const measure = () => {
|
|
44
|
+
// clientWidth excludes the scrollbar, so the page cannot chase a
|
|
45
|
+
// scrollbar it has just caused
|
|
46
|
+
setScale(Math.max(1, node.clientWidth / mmToPx(pageWidthMm)));
|
|
47
|
+
};
|
|
40
48
|
|
|
41
|
-
|
|
49
|
+
measure();
|
|
42
50
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
51
|
+
observer.current = new ResizeObserver(measure);
|
|
52
|
+
observer.current.observe(node);
|
|
53
|
+
},
|
|
54
|
+
[pageWidthMm],
|
|
55
|
+
);
|
|
46
56
|
|
|
47
57
|
useEffect(() => () => observer.current?.disconnect(), []);
|
|
48
58
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Layer } from '@carbon/react';
|
|
3
|
-
import { Report } from '@carbon/react/icons';
|
|
3
|
+
import { Location, Report } from '@carbon/react/icons';
|
|
4
|
+
import { useSession } from '@openmrs/esm-framework';
|
|
4
5
|
|
|
5
6
|
import styles from './report-placeholder.scss';
|
|
6
7
|
|
|
@@ -9,6 +10,8 @@ interface ReportPlaceholderProps {
|
|
|
9
10
|
reportName?: string;
|
|
10
11
|
/** Shown when a report was generated but came back with nothing in it */
|
|
11
12
|
isEmptyResult?: boolean;
|
|
13
|
+
/** A report was generated, but for another location than the current one */
|
|
14
|
+
isLocationChanged?: boolean;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
17
|
/**
|
|
@@ -20,37 +23,59 @@ interface ReportPlaceholderProps {
|
|
|
20
23
|
* run this yet", so the template is held back until there is data to put in it.
|
|
21
24
|
* That leaves the page empty, hence the guidance here.
|
|
22
25
|
*/
|
|
23
|
-
const ReportPlaceholder: React.FC<ReportPlaceholderProps> = ({
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
const ReportPlaceholder: React.FC<ReportPlaceholderProps> = ({
|
|
27
|
+
reportName,
|
|
28
|
+
isEmptyResult = false,
|
|
29
|
+
isLocationChanged = false,
|
|
30
|
+
}) => {
|
|
31
|
+
const session = useSession();
|
|
32
|
+
const location = session?.sessionLocation?.display;
|
|
26
33
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
</
|
|
47
|
-
<
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
34
|
+
return (
|
|
35
|
+
<Layer className={styles.placeholder}>
|
|
36
|
+
{isLocationChanged ? (
|
|
37
|
+
<Location size={32} className={styles.icon} />
|
|
38
|
+
) : (
|
|
39
|
+
<Report size={32} className={styles.icon} />
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{isLocationChanged ? (
|
|
43
|
+
<>
|
|
44
|
+
<h5>Location changed</h5>
|
|
45
|
+
<p>
|
|
46
|
+
The last {reportName ?? 'report'} was generated for a different location, so it is not shown here. Select{' '}
|
|
47
|
+
<strong>Generate report</strong> to produce it for{' '}
|
|
48
|
+
{location ? <strong>{location}</strong> : 'this location'}.
|
|
49
|
+
</p>
|
|
50
|
+
</>
|
|
51
|
+
) : isEmptyResult ? (
|
|
52
|
+
<>
|
|
53
|
+
<h5>No records for this period</h5>
|
|
54
|
+
<p>
|
|
55
|
+
{reportName ?? 'The report'} ran successfully but returned no data for the period you selected. Try a
|
|
56
|
+
different reporting period, or confirm that data has been entered for this facility.
|
|
57
|
+
</p>
|
|
58
|
+
</>
|
|
59
|
+
) : (
|
|
60
|
+
<>
|
|
61
|
+
<h5>No report generated yet</h5>
|
|
62
|
+
<p>
|
|
63
|
+
{reportName ? `The ${reportName} form` : 'The report'} will appear here, filled in with your
|
|
64
|
+
facility’s figures, once you generate it.
|
|
65
|
+
</p>
|
|
66
|
+
<ol className={styles.steps}>
|
|
67
|
+
<li>Choose the reporting period in the filters above.</li>
|
|
68
|
+
<li>
|
|
69
|
+
Select <strong>Generate report</strong>.
|
|
70
|
+
</li>
|
|
71
|
+
<li>
|
|
72
|
+
Use <strong>Download PDF</strong> to save or print the completed form.
|
|
73
|
+
</li>
|
|
74
|
+
</ol>
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
</Layer>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
55
80
|
|
|
56
81
|
export default ReportPlaceholder;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
align-items: center;
|
|
9
9
|
gap: layout.$spacing-03;
|
|
10
10
|
padding: layout.$spacing-07 layout.$spacing-05;
|
|
11
|
+
/* the same gap below the filter card as a generated report's panel has */
|
|
12
|
+
margin-top: layout.$spacing-05;
|
|
11
13
|
margin-bottom: layout.$spacing-05;
|
|
12
14
|
text-align: center;
|
|
13
15
|
background-color: $ui-02;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { useCallback, useState } from 'react';
|
|
2
|
+
import { useSession } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
function readLocation(storageKey?: string): string {
|
|
5
|
+
if (!storageKey) {
|
|
6
|
+
return '';
|
|
7
|
+
}
|
|
8
|
+
try {
|
|
9
|
+
return sessionStorage.getItem(storageKey) ?? '';
|
|
10
|
+
} catch {
|
|
11
|
+
return '';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function writeLocation(storageKey: string | undefined, locationUuid: string) {
|
|
16
|
+
if (!storageKey) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
sessionStorage.setItem(storageKey, locationUuid);
|
|
21
|
+
} catch {
|
|
22
|
+
/* storage unavailable: the report just won't survive a reload */
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Which location the report on screen was generated for, compared with the
|
|
28
|
+
* one the user is at now.
|
|
29
|
+
*
|
|
30
|
+
* A report's figures belong to the location they were fetched for, but its
|
|
31
|
+
* masthead names the session location. After switching location, the old
|
|
32
|
+
* figures would be shown -- and labelled -- as the new location's. So a report
|
|
33
|
+
* only counts as generated while it is for the current location; otherwise the
|
|
34
|
+
* page falls back to the placeholder and asks for it to be generated again.
|
|
35
|
+
*
|
|
36
|
+
* Reports that keep their figures in session storage pass `storageKey`, so the
|
|
37
|
+
* location is restored alongside them. Figures restored without a location
|
|
38
|
+
* (saved before this was recorded) are treated as not generated, rather than
|
|
39
|
+
* assumed to be for wherever the user happens to be.
|
|
40
|
+
*/
|
|
41
|
+
export function useGeneratedLocation(storageKey?: string) {
|
|
42
|
+
const session = useSession();
|
|
43
|
+
const currentLocation = session?.sessionLocation?.uuid ?? '';
|
|
44
|
+
const [generatedLocation, setGeneratedLocation] = useState(() => readLocation(storageKey));
|
|
45
|
+
|
|
46
|
+
/** Call once a report has been fetched, with the location it was fetched for */
|
|
47
|
+
const markGenerated = useCallback(
|
|
48
|
+
(locationUuid: string) => {
|
|
49
|
+
setGeneratedLocation(locationUuid);
|
|
50
|
+
writeLocation(storageKey, locationUuid);
|
|
51
|
+
},
|
|
52
|
+
[storageKey],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
/** The report held is for the location the user is at now */
|
|
57
|
+
isForCurrentLocation: Boolean(generatedLocation) && generatedLocation === currentLocation,
|
|
58
|
+
/** A report is held, but for a different location than the current one */
|
|
59
|
+
hasLocationChanged: Boolean(generatedLocation) && generatedLocation !== currentLocation,
|
|
60
|
+
markGenerated,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
import { Breadcrumb, BreadcrumbItem, Button, Layer, Pagination } from '@carbon/react';
|
|
3
3
|
import { Link, useSearchParams } from 'react-router-dom';
|
|
4
|
-
import { ArrowLeft, Calendar, Document, Download } from '@carbon/react/icons';
|
|
4
|
+
import { ArrowLeft, Calendar, Document, Download, Location } from '@carbon/react/icons';
|
|
5
5
|
import { formatDate } from '@openmrs/esm-framework';
|
|
6
6
|
|
|
7
7
|
import { formatIndicatorName } from '../../utils/format-indicator';
|
|
@@ -29,6 +29,13 @@ interface RegisterLayoutProps {
|
|
|
29
29
|
onBack?: () => void;
|
|
30
30
|
/** Register name, e.g. `HIV Care Treatment Daily Activity Register MOH 366` */
|
|
31
31
|
title: string;
|
|
32
|
+
/**
|
|
33
|
+
* The page heading, when the register can name the figure itself. Without
|
|
34
|
+
* it the heading is built from the link's `label` and `gender`.
|
|
35
|
+
*/
|
|
36
|
+
heading?: string;
|
|
37
|
+
/** The location the register was generated for, shown beside the period */
|
|
38
|
+
location?: string;
|
|
32
39
|
isLoading: boolean;
|
|
33
40
|
/** `true` once loaded with nothing to show */
|
|
34
41
|
isEmpty: boolean;
|
|
@@ -69,6 +76,8 @@ const RegisterLayout: React.FC<RegisterLayoutProps> = ({
|
|
|
69
76
|
parentPath,
|
|
70
77
|
onBack,
|
|
71
78
|
title,
|
|
79
|
+
heading: headingProp,
|
|
80
|
+
location,
|
|
72
81
|
isLoading,
|
|
73
82
|
isEmpty,
|
|
74
83
|
page,
|
|
@@ -109,11 +118,18 @@ const RegisterLayout: React.FC<RegisterLayoutProps> = ({
|
|
|
109
118
|
if (locationUuids) parentQuery.append('locationUuids', locationUuids);
|
|
110
119
|
if (startDate) parentQuery.append('startDate', startDate);
|
|
111
120
|
if (endDate) parentQuery.append('endDate', endDate);
|
|
121
|
+
/* back to the report tab the register was opened from, e.g. a summary view */
|
|
122
|
+
const view = params.get('view');
|
|
123
|
+
if (view) parentQuery.append('view', view);
|
|
124
|
+
const section = params.get('section');
|
|
125
|
+
if (section) parentQuery.append('section', section);
|
|
112
126
|
const parentLink = parentQuery.toString() ? `${parent.path}?${parentQuery}` : parent.path;
|
|
113
127
|
|
|
114
|
-
const heading =
|
|
115
|
-
|
|
116
|
-
|
|
128
|
+
const heading =
|
|
129
|
+
headingProp ??
|
|
130
|
+
(label
|
|
131
|
+
? `${label}${gender && GENDERS[gender] ? ` (${GENDERS[gender]})` : ''}`
|
|
132
|
+
: formatIndicatorName(indicator ?? ''));
|
|
117
133
|
|
|
118
134
|
/* Exports every page, not the one on screen: the DOM only holds the current
|
|
119
135
|
page once paginated, so a scrape would silently export 50 of 2,000. */
|
|
@@ -190,6 +206,12 @@ const RegisterLayout: React.FC<RegisterLayoutProps> = ({
|
|
|
190
206
|
{period}
|
|
191
207
|
</>
|
|
192
208
|
)}
|
|
209
|
+
{location && (
|
|
210
|
+
<span className={styles.location}>
|
|
211
|
+
<Location size={14} />
|
|
212
|
+
{location}
|
|
213
|
+
</span>
|
|
214
|
+
)}
|
|
193
215
|
{typeof total === 'number' && total > 0 && (
|
|
194
216
|
<span>{isTotalExact ? `${total.toLocaleString()} records` : `${total.toLocaleString()}+ records`}</span>
|
|
195
217
|
)}
|
|
@@ -109,6 +109,7 @@
|
|
|
109
109
|
.period {
|
|
110
110
|
@include type.type-style('label-01');
|
|
111
111
|
display: inline-flex;
|
|
112
|
+
flex-wrap: wrap;
|
|
112
113
|
align-items: center;
|
|
113
114
|
gap: layout.$spacing-02;
|
|
114
115
|
color: $text-02;
|
|
@@ -119,6 +120,15 @@
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
|
|
123
|
+
/* Location sits after the period as one unit, so on a narrow screen it wraps
|
|
124
|
+
to its own line whole rather than leaving its pin behind */
|
|
125
|
+
.location {
|
|
126
|
+
display: inline-flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
gap: layout.$spacing-02;
|
|
129
|
+
margin-left: layout.$spacing-03;
|
|
130
|
+
}
|
|
131
|
+
|
|
122
132
|
.export {
|
|
123
133
|
flex-shrink: 0;
|
|
124
134
|
margin-left: auto;
|
|
@@ -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
|
+
}
|
|
@@ -5,7 +5,7 @@ import TableRowMapper from '../table-wrapper/table-row-mapper.component';
|
|
|
5
5
|
import { getCell } from '../../utils/utils';
|
|
6
6
|
import { ReportFiltersComponent, type ReportPeriod } from '../../common/report-filters';
|
|
7
7
|
import { showReportError } from '../../common/report-error';
|
|
8
|
-
import { hasReportData, ReportPlaceholder } from '../../common/report-placeholder';
|
|
8
|
+
import { hasReportData, ReportPlaceholder, useGeneratedLocation } from '../../common/report-placeholder';
|
|
9
9
|
import { getMoh505 } from '../../resources/moh-505.resource';
|
|
10
10
|
import MOH505Header from './moh-505-header.component';
|
|
11
11
|
import { useSearchParams } from 'react-router-dom';
|
|
@@ -15,7 +15,10 @@ import styles from './moh-505.scss';
|
|
|
15
15
|
|
|
16
16
|
const Moh505Report: React.FC = () => {
|
|
17
17
|
const [moh505Data, setMoh505Data] = useState<any>({});
|
|
18
|
-
|
|
18
|
+
/* Only a report for the current location counts: after switching location the
|
|
19
|
+
old figures would otherwise be shown, and labelled, as the new location's. */
|
|
20
|
+
const { isForCurrentLocation, hasLocationChanged, markGenerated } = useGeneratedLocation();
|
|
21
|
+
const isReportGenerated = hasReportData(moh505Data) && isForCurrentLocation;
|
|
19
22
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
20
23
|
const [filters, setFilters] = useState<{ locationUuids?: string; startDate?: string; endDate?: string }>({});
|
|
21
24
|
|
|
@@ -52,6 +55,7 @@ const Moh505Report: React.FC = () => {
|
|
|
52
55
|
const result = await getMoh505(params);
|
|
53
56
|
const flatData = Object.assign({}, ...result);
|
|
54
57
|
setMoh505Data(flatData);
|
|
58
|
+
markGenerated(params.locationUuids);
|
|
55
59
|
setFilters({ locationUuids: params.locationUuids, startDate, endDate });
|
|
56
60
|
} catch (error: any) {
|
|
57
61
|
showReportError('the MOH-505 report', error);
|
|
@@ -397,7 +401,9 @@ const Moh505Report: React.FC = () => {
|
|
|
397
401
|
isReportGenerated={isReportGenerated}
|
|
398
402
|
/>
|
|
399
403
|
{isLoading && <ReportSkeleton />}
|
|
400
|
-
{!isLoading && !isReportGenerated &&
|
|
404
|
+
{!isLoading && !isReportGenerated && (
|
|
405
|
+
<ReportPlaceholder reportName="MOH-505" isLocationChanged={hasReportData(moh505Data) && hasLocationChanged} />
|
|
406
|
+
)}
|
|
401
407
|
{!isLoading && isReportGenerated && (
|
|
402
408
|
<ReportPage>
|
|
403
409
|
<div className={styles.sheet}>
|
|
@@ -37,6 +37,15 @@ const Moh204BRegisterComponent: React.FC = () => {
|
|
|
37
37
|
[startDate, endDate, locationUuids, indicator],
|
|
38
38
|
);
|
|
39
39
|
|
|
40
|
+
const removePatientListDuplicates = async () => {
|
|
41
|
+
const list = await fetchAll();
|
|
42
|
+
const filtered = uniqueBy(list, 'person_id');
|
|
43
|
+
return filtered;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
const uniqueBy = (arr: any, key: string) =>
|
|
47
|
+
[...new Map(arr.map((item: any) => [item[key], item])).values()] as Array<Record<string, any>>;
|
|
48
|
+
|
|
40
49
|
return (
|
|
41
50
|
<RegisterLayout
|
|
42
51
|
parentLabel="MOH-705B Report"
|
|
@@ -49,7 +58,7 @@ const Moh204BRegisterComponent: React.FC = () => {
|
|
|
49
58
|
total={total}
|
|
50
59
|
isTotalExact={isTotalExact}
|
|
51
60
|
onPageChange={onPageChange}
|
|
52
|
-
fetchAll={
|
|
61
|
+
fetchAll={removePatientListDuplicates}
|
|
53
62
|
columns={moh204bExportColumns}
|
|
54
63
|
>
|
|
55
64
|
<div className={styles.tableContainer}>
|
|
@@ -190,7 +199,7 @@ const Moh204BRegisterComponent: React.FC = () => {
|
|
|
190
199
|
</TableHead>
|
|
191
200
|
<TableBody>
|
|
192
201
|
{patientlist?.length > 0 ? (
|
|
193
|
-
patientlist?.map((patient, index) => (
|
|
202
|
+
uniqueBy(patientlist, 'person_id')?.map((patient, index) => (
|
|
194
203
|
<TableRow key={patient.person_id || index}>
|
|
195
204
|
<TableCell>{patient.date}</TableCell>
|
|
196
205
|
<TableCell>{patient.opd_number_new_visit}</TableCell>
|