@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.
Files changed (67) hide show
  1. package/dist/295.js +1 -1
  2. package/dist/410.js +1 -1
  3. package/dist/580.js +1 -0
  4. package/dist/580.js.map +1 -0
  5. package/dist/907.js +2 -1
  6. package/dist/907.js.map +1 -1
  7. package/dist/998.css +1 -0
  8. package/dist/998.css.map +1 -0
  9. package/dist/998.js +1 -0
  10. package/dist/998.js.map +1 -0
  11. package/dist/ampath-esm-reports-app.js +1 -1
  12. package/dist/routes.json +1 -1
  13. package/package.json +1 -1
  14. package/src/common/daily-summary-table.tsx +55 -0
  15. package/src/common/linelist-table/index.ts +3 -0
  16. package/src/common/linelist-table/linelist-columns.ts +48 -0
  17. package/src/common/linelist-table/linelist-table.component.tsx +442 -0
  18. package/src/common/linelist-table/linelist-table.scss +306 -0
  19. package/src/common/linelist-table/linelist-table.test.tsx +120 -0
  20. package/src/common/linelist-table/use-all-rows.ts +93 -0
  21. package/src/common/linelist-table/use-column-layout.ts +123 -0
  22. package/src/common/report-filters/report-filters.component.tsx +11 -5
  23. package/src/common/report-filters/report-filters.scss +63 -7
  24. package/src/common/report-page/use-page-scale.ts +27 -17
  25. package/src/common/report-placeholder/index.ts +1 -0
  26. package/src/common/report-placeholder/report-placeholder.component.tsx +57 -32
  27. package/src/common/report-placeholder/report-placeholder.scss +2 -0
  28. package/src/common/report-placeholder/use-generated-location.ts +62 -0
  29. package/src/common/report-register/index.ts +1 -0
  30. package/src/common/report-register/register-layout.component.tsx +26 -4
  31. package/src/common/report-register/register-layout.scss +10 -0
  32. package/src/common/report-register/use-location-name.ts +54 -0
  33. package/src/reports/moh-505/moh-505.component.tsx +9 -3
  34. package/src/reports/moh-705B/moh-204b-register.component.tsx +11 -2
  35. package/src/reports/moh-705B/moh-705b.component.tsx +533 -418
  36. package/src/reports/moh-705B/moh-705b.scss +44 -2
  37. package/src/reports/moh-705a/moh-705a.component.tsx +533 -421
  38. package/src/reports/moh-705a/moh-705a.scss +44 -2
  39. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +12 -3
  40. package/src/reports/moh-706/moh-706.component.tsx +9 -3
  41. package/src/reports/moh-710/moh-710.component.tsx +9 -3
  42. package/src/reports/moh-711/moh-711.component.tsx +9 -3
  43. package/src/reports/moh-717/moh-717.component.tsx +12 -3
  44. package/src/reports/moh-731/linelist/moh-731-indicators.ts +1040 -0
  45. package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +423 -0
  46. package/src/reports/moh-731/linelist/moh-731-linelist.component.tsx +102 -0
  47. package/src/reports/moh-731/linelist/use-open-moh-731-linelist.ts +50 -0
  48. package/src/reports/moh-731/moh-731.component.tsx +138 -85
  49. package/src/reports/moh-731/moh-731.scss +51 -4
  50. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +74 -77
  51. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +84 -70
  52. package/src/reports/moh-731/sections/pmtct.component.tsx +43 -43
  53. package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
  54. package/src/reports/moh-731/summary/moh-731-summary.config.ts +520 -0
  55. package/src/reports/moh-731/summary/moh-731-summary.scss +242 -0
  56. package/src/reports/moh-740/moh-740.component.scss +3 -1
  57. package/src/reports/moh-740/moh-740.component.tsx +14 -4
  58. package/src/reports/moh-745/moh-745.component.tsx +7 -3
  59. package/src/root.component.tsx +2 -0
  60. package/webpack.config.js +22 -1
  61. package/dist/427.js +0 -2
  62. package/dist/427.js.map +0 -1
  63. package/dist/441.js +0 -1
  64. package/dist/441.js.map +0 -1
  65. package/dist/907.css +0 -1
  66. package/dist/907.css.map +0 -1
  67. /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. Overridden through the range class so this wins on specificity
111
- rather than on stylesheet order. */
112
- .fields .datePicker:global(.cds--date-picker--range) {
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: inline-flex;
140
- align-items: center;
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-02 layout.$spacing-03;
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, 210mm, in CSS pixels at the reference 96dpi */
4
- const A4_WIDTH_PX = (210 * 96) / 25.4;
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((node: T | null) => {
29
- observer.current?.disconnect();
35
+ const ref = useCallback(
36
+ (node: T | null) => {
37
+ observer.current?.disconnect();
30
38
 
31
- if (!node) {
32
- return;
33
- }
39
+ if (!node) {
40
+ return;
41
+ }
34
42
 
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
- };
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
- measure();
49
+ measure();
42
50
 
43
- observer.current = new ResizeObserver(measure);
44
- observer.current.observe(node);
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,2 +1,3 @@
1
1
  export { default as ReportPlaceholder } from './report-placeholder.component';
2
2
  export * from './has-report-data';
3
+ export * from './use-generated-location';
@@ -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> = ({ reportName, isEmptyResult = false }) => (
24
- <Layer className={styles.placeholder}>
25
- <Report size={32} className={styles.icon} />
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
- {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
- );
34
+ return (
35
+ <Layer className={styles.placeholder}>
36
+ {isLocationChanged ? (
37
+ <Location size={32} className={styles.icon} />
38
+ ) : (
39
+ <Report size={32} className={styles.icon} />
40
+ )}
41
+
42
+ {isLocationChanged ? (
43
+ <>
44
+ <h5>Location changed</h5>
45
+ <p>
46
+ The last {reportName ?? 'report'} was generated for a different location, so it is not shown here. Select{' '}
47
+ <strong>Generate report</strong> to produce it for{' '}
48
+ {location ? <strong>{location}</strong> : 'this location'}.
49
+ </p>
50
+ </>
51
+ ) : isEmptyResult ? (
52
+ <>
53
+ <h5>No records for this period</h5>
54
+ <p>
55
+ {reportName ?? 'The report'} ran successfully but returned no data for the period you selected. Try a
56
+ different reporting period, or confirm that data has been entered for this facility.
57
+ </p>
58
+ </>
59
+ ) : (
60
+ <>
61
+ <h5>No report generated yet</h5>
62
+ <p>
63
+ {reportName ? `The ${reportName} form` : 'The report'} will appear here, filled in with your
64
+ facility&rsquo;s figures, once you generate it.
65
+ </p>
66
+ <ol className={styles.steps}>
67
+ <li>Choose the reporting period in the filters above.</li>
68
+ <li>
69
+ Select <strong>Generate report</strong>.
70
+ </li>
71
+ <li>
72
+ Use <strong>Download PDF</strong> to save or print the completed form.
73
+ </li>
74
+ </ol>
75
+ </>
76
+ )}
77
+ </Layer>
78
+ );
79
+ };
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
+ }
@@ -3,3 +3,4 @@ export * from './register-columns';
3
3
  export * from './report-parents';
4
4
  export * from './rows-to-csv';
5
5
  export * from './use-patient-list';
6
+ export * from './use-location-name';
@@ -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 = label
115
- ? `${label}${gender && GENDERS[gender] ? ` (${GENDERS[gender]})` : ''}`
116
- : formatIndicatorName(indicator ?? '');
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
- const isReportGenerated = hasReportData(moh505Data);
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 && <ReportPlaceholder reportName="MOH-505" />}
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={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>