@ampath/esm-reports-app 1.0.0-next.40 → 1.0.0-next.42

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 (147) hide show
  1. package/dist/110.js +2 -1
  2. package/dist/110.js.LICENSE.txt +35 -0
  3. package/dist/110.js.map +1 -1
  4. package/dist/205.js +1 -1
  5. package/dist/441.js +1 -0
  6. package/dist/441.js.map +1 -0
  7. package/dist/738.js +1 -0
  8. package/dist/738.js.map +1 -0
  9. package/dist/812.js +1 -1
  10. package/dist/812.js.map +1 -1
  11. package/dist/ampath-esm-reports-app.js +1 -1
  12. package/dist/ampath-esm-reports-app.js.buildmanifest.json +66 -39
  13. package/dist/main.js +1 -1
  14. package/dist/main.js.map +1 -1
  15. package/dist/routes.json +1 -1
  16. package/package.json +1 -1
  17. package/src/common/report-error/index.ts +1 -0
  18. package/src/common/report-error/show-report-error.ts +25 -0
  19. package/src/common/report-filters/date-range.ts +137 -0
  20. package/src/common/report-filters/index.ts +4 -0
  21. package/src/common/report-filters/report-filters.component.tsx +291 -104
  22. package/src/common/report-filters/report-filters.scss +199 -32
  23. package/src/common/report-filters/types.ts +66 -0
  24. package/src/common/report-filters/use-restore-period.ts +43 -0
  25. package/src/common/report-page/index.ts +2 -0
  26. package/src/common/report-page/report-page.component.tsx +32 -0
  27. package/src/common/report-page/report-page.scss +45 -0
  28. package/src/common/report-page/use-page-scale.ts +50 -0
  29. package/src/common/report-placeholder/has-report-data.ts +15 -0
  30. package/src/common/report-placeholder/index.ts +2 -0
  31. package/src/common/report-placeholder/report-placeholder.component.tsx +56 -0
  32. package/src/common/report-placeholder/report-placeholder.scss +46 -0
  33. package/src/common/report-register/index.ts +5 -0
  34. package/src/common/report-register/register-columns.ts +34 -0
  35. package/src/common/report-register/register-layout.component.tsx +237 -0
  36. package/src/common/report-register/register-layout.scss +140 -0
  37. package/src/common/report-register/register-table.scss +62 -0
  38. package/src/common/report-register/report-parents.ts +27 -0
  39. package/src/common/report-register/rows-to-csv.ts +59 -0
  40. package/src/common/report-register/use-patient-list.ts +92 -0
  41. package/src/common/report-skeleton/index.ts +1 -0
  42. package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
  43. package/src/common/report-skeleton/report-skeleton.scss +29 -0
  44. package/src/config-schema.ts +9 -1
  45. package/src/dashboard/report-catalog.ts +173 -0
  46. package/src/dashboard/reports-dasboard.tsx +101 -35
  47. package/src/dashboard/reports-dashboard.module.scss +269 -11
  48. package/src/index.ts +13 -1
  49. package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +50 -48
  50. package/src/reports/moh-240/moh-240-header.component.tsx +54 -56
  51. package/src/reports/moh-240/moh-240.columns.ts +64 -0
  52. package/src/reports/moh-240/moh-240.component.tsx +65 -84
  53. package/src/reports/moh-240/moh-240.scss +36 -7
  54. package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +27 -65
  55. package/src/reports/moh-240/sub-reports/page-summary.component.tsx +2 -2
  56. package/src/reports/moh-505/moh-505-header.component.tsx +59 -57
  57. package/src/reports/moh-505/moh-505.component.tsx +51 -56
  58. package/src/reports/moh-505/moh-505.scss +116 -2
  59. package/src/reports/moh-705B/moh-204b-register.component.tsx +41 -42
  60. package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
  61. package/src/reports/moh-705B/moh-705b.component.tsx +492 -473
  62. package/src/reports/moh-705B/moh-705b.scss +110 -24
  63. package/src/reports/moh-705a/moh-705a.component.tsx +496 -476
  64. package/src/reports/moh-705a/moh-705a.scss +109 -24
  65. package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +41 -42
  66. package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
  67. package/src/reports/moh-706/moh-706-header.component.tsx +58 -56
  68. package/src/reports/moh-706/moh-706.component.tsx +84 -55
  69. package/src/reports/moh-706/moh-706.scss +115 -19
  70. package/src/reports/moh-710/moh-710.component.tsx +743 -727
  71. package/src/reports/moh-710/moh-710.scss +140 -23
  72. package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +48 -43
  73. package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
  74. package/src/reports/moh-711/moh-711.component.tsx +94 -106
  75. package/src/reports/moh-711/moh711.scss +95 -64
  76. package/src/reports/moh-711/registers/moh-333-register.component.tsx +92 -94
  77. package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
  78. package/src/reports/moh-711/registers/moh-405-register.component.tsx +88 -89
  79. package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
  80. package/src/reports/moh-711/registers/moh-406-register.component.tsx +85 -88
  81. package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
  82. package/src/reports/moh-711/registers/moh-510-register.component.tsx +38 -44
  83. package/src/reports/moh-711/registers/moh-511-register.component.tsx +42 -43
  84. package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
  85. package/src/reports/moh-711/sections/anc.component.tsx +3 -3
  86. package/src/reports/moh-711/sections/cervical-cancer.component.tsx +3 -3
  87. package/src/reports/moh-711/sections/chanis.component.tsx +2 -3
  88. package/src/reports/moh-711/sections/family-planning.component.tsx +2 -2
  89. package/src/reports/moh-711/sections/gbv.component.tsx +2 -2
  90. package/src/reports/moh-711/sections/maternity.component.tsx +3 -8
  91. package/src/reports/moh-711/sections/medical-social-work.component.tsx +2 -12
  92. package/src/reports/moh-711/sections/other.component.tsx +2 -2
  93. package/src/reports/moh-711/sections/physiotherapy.component.tsx +2 -2
  94. package/src/reports/moh-711/sections/pnc.component.tsx +7 -5
  95. package/src/reports/moh-711/sections/post-abortion.component.tsx +2 -2
  96. package/src/reports/moh-711/sections/rehabilitation.component.tsx +2 -2
  97. package/src/reports/moh-711/sections/report-compiled-by.component.tsx +12 -4
  98. package/src/reports/moh-711/sections/tb-screening.component.tsx +2 -2
  99. package/src/reports/moh-717/moh-717.component.tsx +100 -121
  100. package/src/reports/moh-717/moh717.scss +163 -102
  101. package/src/reports/moh-717/sections/maternity.component.tsx +1 -1
  102. package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +1 -1
  103. package/src/reports/moh-717/sections/outpatient.component.tsx +23 -23
  104. package/src/reports/moh-717/sections/preparedby.component.tsx +7 -2
  105. package/src/reports/moh-717/sections/special-services.component.tsx +1 -1
  106. package/src/reports/moh-731/moh-731.component.tsx +127 -110
  107. package/src/reports/moh-731/moh-731.scss +396 -68
  108. package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
  109. package/src/reports/moh-731/registers/moh-366.component.tsx +76 -83
  110. package/src/reports/moh-731/registers/prep-register.component.tsx +70 -78
  111. package/src/reports/moh-731/registers/prep.columns.ts +34 -0
  112. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1031 -751
  113. package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +230 -86
  114. package/src/reports/moh-731/sections/pmtct.component.tsx +185 -64
  115. package/src/reports/moh-740/moh-740.component.scss +159 -79
  116. package/src/reports/moh-740/moh-740.component.tsx +258 -227
  117. package/src/reports/moh-740/moh-740.resource.ts +17 -10
  118. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +77 -96
  119. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +50 -70
  120. package/src/reports/moh-740/registers/moh-740-patient-list.tsx +61 -32
  121. package/src/reports/moh-740/registers/moh-740.columns.ts +52 -0
  122. package/src/reports/moh-745/moh-745.component.tsx +897 -877
  123. package/src/reports/moh-745/moh-745.scss +104 -46
  124. package/src/reports/moh-745/registers/moh-412-register.component.tsx +42 -43
  125. package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
  126. package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
  127. package/src/reports/table-wrapper/table-wrapper.scss +1 -16
  128. package/src/resources/moh-505.resource.ts +18 -35
  129. package/src/resources/moh-705.resource.ts +28 -39
  130. package/src/resources/moh-706.resource.ts +17 -30
  131. package/src/resources/moh-710.resource.ts +7 -17
  132. package/src/resources/moh-711.resource.ts +46 -64
  133. package/src/resources/moh-717.resource.ts +7 -17
  134. package/src/resources/moh-731.resource.ts +26 -32
  135. package/src/resources/moh-745.resource.ts +18 -20
  136. package/src/resources/patient-list-page.ts +66 -0
  137. package/src/root.component.tsx +28 -26
  138. package/src/root.scss +39 -0
  139. package/src/utils/get-base-url.ts +39 -7
  140. package/dist/822.js +0 -2
  141. package/dist/822.js.LICENSE.txt +0 -20
  142. package/dist/822.js.map +0 -1
  143. package/src/reports/moh-731/registers/moh-366.scss +0 -45
  144. package/src/reports/moh-731/registers/prep-registers.scss +0 -45
  145. package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.scss +0 -18
  146. package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.scss +0 -18
  147. /package/src/{reports/moh-740/shared/utils → utils}/format-indicator.ts +0 -0
@@ -0,0 +1,140 @@
1
+ @use '@carbon/layout';
2
+ @use '@carbon/type';
3
+ @use '@openmrs/esm-styleguide/src/vars' as *;
4
+
5
+ .register {
6
+ padding: layout.$spacing-05 0 layout.$spacing-07;
7
+ }
8
+
9
+ .breadcrumb {
10
+ margin-bottom: layout.$spacing-05;
11
+
12
+ :global(.cds--breadcrumb-item) {
13
+ display: inline-flex;
14
+ align-items: baseline;
15
+ }
16
+ }
17
+
18
+ /* The register name doubles as the page heading, so it stays an `h3` rather
19
+ than being repeated in a title row of its own. */
20
+
21
+ .title {
22
+ @include type.type-style('productive-heading-03');
23
+ display: inline;
24
+ color: $color-gray-100;
25
+ }
26
+
27
+ /* Registers run to dozens of columns, so the table scrolls inside the page
28
+ instead of pushing the whole layout sideways. */
29
+
30
+ .tableScroll {
31
+ overflow-x: auto;
32
+ padding: layout.$spacing-05;
33
+ background-color: $ui-02;
34
+ border: 1px solid $ui-03;
35
+ border-top: none;
36
+
37
+ /* Each register carries its own table margins -- 1rem here, 0.5rem there,
38
+ 0.75rem elsewhere -- which land inside this frame and give every register a
39
+ different gap to the border. The padding above is the spacing now, so those
40
+ are levelled out. */
41
+
42
+ table,
43
+ > div {
44
+ margin: 0;
45
+ }
46
+ }
47
+
48
+
49
+ .empty {
50
+ display: flex;
51
+ flex-direction: column;
52
+ align-items: center;
53
+ gap: layout.$spacing-03;
54
+ padding: layout.$spacing-07 layout.$spacing-05;
55
+ text-align: center;
56
+ background-color: $ui-02;
57
+ border: 1px solid $ui-03;
58
+ border-top: none;
59
+
60
+ h4 {
61
+ @include type.type-style('productive-heading-02');
62
+ color: $color-gray-100;
63
+ }
64
+
65
+ p {
66
+ @include type.type-style('body-long-01');
67
+ color: $text-02;
68
+ }
69
+ }
70
+
71
+ .emptyIcon {
72
+ fill: $ui-04;
73
+ }
74
+
75
+ /* The caption and the table below it are one card: the bar carries the register
76
+ name, the period it covers and the export, and sits flush on whatever follows
77
+ -- skeleton, empty state or table -- so the page reads as a single object
78
+ rather than three stacked fragments. */
79
+
80
+ .context {
81
+ display: flex;
82
+ flex-wrap: wrap;
83
+ align-items: center;
84
+ justify-content: space-between;
85
+ gap: layout.$spacing-03 layout.$spacing-05;
86
+ padding: layout.$spacing-04 layout.$spacing-05;
87
+ background-color: $ui-02;
88
+ border: 1px solid $ui-03;
89
+ border-bottom: none;
90
+ }
91
+
92
+ /* Register name over the period it covers: side by side they made one long
93
+ line that crowded the action; stacked, the name leads and the period reads
94
+ as its caption. */
95
+
96
+ .summary {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: layout.$spacing-02;
100
+ min-width: 0;
101
+ }
102
+
103
+ .summaryTitle {
104
+ @include type.type-style('body-short-01');
105
+ font-weight: 600;
106
+ color: $color-gray-100;
107
+ }
108
+
109
+ .period {
110
+ @include type.type-style('label-01');
111
+ display: inline-flex;
112
+ align-items: center;
113
+ gap: layout.$spacing-02;
114
+ color: $text-02;
115
+
116
+ svg {
117
+ fill: var(--brand-01);
118
+ transform: translateY(0.125rem);
119
+ }
120
+ }
121
+
122
+ .export {
123
+ flex-shrink: 0;
124
+ margin-left: auto;
125
+ }
126
+
127
+ /* Sits directly under the table, completing the card */
128
+
129
+ .pagination {
130
+ border: 1px solid $ui-03;
131
+ border-top: none;
132
+ }
133
+
134
+ /* A register rendered as a view on its report's own route has nowhere to
135
+ navigate, so the trail is a button and the register name beside it. */
136
+
137
+ .back {
138
+ padding-inline: 0.25rem;
139
+ min-block-size: auto;
140
+ }
@@ -0,0 +1,62 @@
1
+ @use '@openmrs/esm-styleguide/src/vars' as *;
2
+
3
+ /* The one stylesheet every register table uses.
4
+ *
5
+ * Registers previously borrowed their parent report's sheet, so a report sized
6
+ * for an A4 page dragged its paper typography into its line lists, and the same
7
+ * table rules were restated in half a dozen files that drifted apart. These are
8
+ * screen tables: dense, scrollable, and independent of how their report prints.
9
+ */
10
+
11
+ .tableContainer {
12
+ width: 100%;
13
+ }
14
+
15
+ /* Sized to content rather than to the frame, so a long value -- a full name,
16
+ say -- widens its own column instead of wrapping. `.tableScroll` in the
17
+ layout scrolls when the table outgrows the page. */
18
+
19
+ .table {
20
+ width: auto;
21
+ min-width: 100%;
22
+ margin: 0;
23
+ font-size: 0.75rem;
24
+ border-collapse: collapse;
25
+ }
26
+
27
+ .tableBordered td,
28
+ .tableBordered th {
29
+ padding: 0.125rem 0.5rem;
30
+ font-size: 0.75rem;
31
+ line-height: 1.3;
32
+ border: 1px solid $ui-04;
33
+ }
34
+
35
+ .tableBordered th {
36
+ font-weight: 600;
37
+ color: $color-gray-100;
38
+ background-color: $ui-01;
39
+ /* headings carry deliberate line breaks, so they wrap where the markup says */
40
+ white-space: normal;
41
+ }
42
+
43
+ .tableBordered td {
44
+ white-space: nowrap;
45
+ }
46
+
47
+ .tableStriped tbody tr:nth-of-type(odd) {
48
+ background-color: $ui-01;
49
+ }
50
+
51
+ .tableStriped tbody tr:hover {
52
+ background-color: $brand-primary-10;
53
+ }
54
+
55
+ .textCentre {
56
+ text-align: center;
57
+ }
58
+
59
+ .verticalText {
60
+ writing-mode: vertical-rl;
61
+ transform: rotate(180deg);
62
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The report each register can be opened from.
3
+ *
4
+ * Several registers serve more than one report -- MOH 333, 405 and 406 are
5
+ * reached from both MOH-711 and MOH-717, and MOH 412 from both MOH-711 and
6
+ * MOH-745. A register cannot name its parent as a constant, then: whichever
7
+ * report it named, the trail would be wrong for everyone arriving from the
8
+ * other one. The link carries `from`, and the trail is resolved from it.
9
+ */
10
+ export interface ReportParent {
11
+ label: string;
12
+ path: string;
13
+ }
14
+
15
+ export const REPORT_PARENTS: Record<string, ReportParent> = {
16
+ 'moh-240': { label: 'MOH-240 Register', path: '/moh-240' },
17
+ 'moh-505': { label: 'MOH-505 Report', path: '/moh-505' },
18
+ 'moh-705a': { label: 'MOH-705A Report', path: '/moh-705a' },
19
+ 'moh-705b': { label: 'MOH-705B Report', path: '/moh-705b' },
20
+ 'moh-706': { label: 'MOH-706 Report', path: '/moh-706' },
21
+ 'moh-710': { label: 'MOH-710 Report', path: '/moh-710' },
22
+ 'moh-711': { label: 'MOH-711 Report', path: '/moh-711' },
23
+ 'moh-717': { label: 'MOH-717 Report', path: '/moh-717' },
24
+ 'moh-731': { label: 'MOH-731 Report', path: '/moh-731' },
25
+ 'moh-740': { label: 'MOH-740 Report', path: '/moh-740' },
26
+ 'moh-745': { label: 'MOH-745 Report', path: '/moh-745' },
27
+ };
@@ -0,0 +1,59 @@
1
+ import { type RegisterColumn } from './register-columns';
2
+
3
+ /**
4
+ * Serialises line-list rows to CSV.
5
+ *
6
+ * Built from the data rather than from the rendered table: with pagination the
7
+ * DOM only ever holds one page, so scraping it would quietly export 50 rows
8
+ * while looking like it exported all 2,000 -- the worst way for an export to
9
+ * fail. The trade-off is that the file carries the API's own fields rather
10
+ * than the register's printed column headings, which is what a spreadsheet
11
+ * wants anyway.
12
+ */
13
+
14
+ function escapeCell(value: unknown): string {
15
+ const text = value === null || value === undefined ? '' : String(value).replace(/\s+/g, ' ').trim();
16
+
17
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
18
+ }
19
+
20
+ /** Serialises using the register's own columns, so the file matches the form */
21
+ export function columnsToCsv(rows: Array<Record<string, any>>, columns: RegisterColumn[]): string {
22
+ return [
23
+ columns.map((column) => escapeCell(column.header)).join(','),
24
+ ...rows.map((row, index) => columns.map((column) => escapeCell(column.value(row, index))).join(',')),
25
+ ].join('\r\n');
26
+ }
27
+
28
+ /** Fallback for registers with no column spec yet: the response's own fields */
29
+ export function rowsToCsv(rows: Array<Record<string, any>>): string {
30
+ if (!rows.length) {
31
+ return '';
32
+ }
33
+
34
+ /* Union of keys, not just the first row's: endpoints omit fields that are
35
+ null for a given client, so keying off row one drops columns. */
36
+ const keys = new Set<string>();
37
+ rows.forEach((row) => Object.keys(row).forEach((key) => keys.add(key)));
38
+ const columns = Array.from(keys);
39
+
40
+ return [
41
+ columns.map(escapeCell).join(','),
42
+ ...rows.map((row) => columns.map((column) => escapeCell(row[column])).join(',')),
43
+ ].join('\r\n');
44
+ }
45
+
46
+ /** Prompts the browser to save `csv` under `filename` */
47
+ export function downloadCsv(csv: string, filename: string): void {
48
+ // the BOM is what makes Excel read the file as UTF-8 rather than latin-1
49
+ const blob = new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' });
50
+ const url = URL.createObjectURL(blob);
51
+ const link = document.createElement('a');
52
+
53
+ link.href = url;
54
+ link.download = filename;
55
+ document.body.appendChild(link);
56
+ link.click();
57
+ document.body.removeChild(link);
58
+ URL.revokeObjectURL(url);
59
+ }
@@ -0,0 +1,92 @@
1
+ import { useCallback, useEffect, useRef, useState } from 'react';
2
+
3
+ import { PAGE_SIZE, type PatientListPage } from '../../resources/patient-list-page';
4
+ import { showReportError } from '../report-error';
5
+
6
+ type Fetcher = (paging: { startIndex: number; limit: number }) => Promise<PatientListPage>;
7
+
8
+ /**
9
+ * Page state and fetching for a register's line list.
10
+ *
11
+ * Registers used to ask for a hardcoded 300 rows and render every one of them.
12
+ * At 41 columns that is 12,300 cells, and anything past 300 was silently
13
+ * dropped. This fetches a page at a time, so the DOM stays flat however large
14
+ * the list is, and reports the total so truncation is never invisible.
15
+ */
16
+ export function usePatientList(fetcher: Fetcher, deps: React.DependencyList) {
17
+ const [rows, setRows] = useState<Array<Record<string, any>>>([]);
18
+ const [total, setTotal] = useState(0);
19
+ const [isTotalExact, setIsTotalExact] = useState(true);
20
+ const [isLoading, setIsLoading] = useState(false);
21
+ const [page, setPage] = useState(1);
22
+ const [pageSize, setPageSize] = useState(PAGE_SIZE);
23
+
24
+ /* Guards against an earlier page landing after a later one and overwriting
25
+ it, which is easy to trigger by clicking through pages quickly. */
26
+ const request = useRef(0);
27
+
28
+ const load = useCallback(
29
+ async (nextPage: number, size: number) => {
30
+ const id = ++request.current;
31
+
32
+ setIsLoading(true);
33
+
34
+ try {
35
+ const result = await fetcher({ startIndex: (nextPage - 1) * size, limit: size });
36
+
37
+ if (id !== request.current) {
38
+ return;
39
+ }
40
+
41
+ setRows(result.results);
42
+ setTotal(result.total);
43
+ setIsTotalExact(result.isTotalExact);
44
+ } catch (error) {
45
+ if (id === request.current) {
46
+ setRows([]);
47
+ showReportError('the register', error);
48
+ }
49
+ } finally {
50
+ if (id === request.current) {
51
+ setIsLoading(false);
52
+ }
53
+ }
54
+ },
55
+ // eslint-disable-next-line react-hooks/exhaustive-deps
56
+ deps,
57
+ );
58
+
59
+ /** Back to page one whenever the query behind the list changes */
60
+ useEffect(() => {
61
+ setPage(1);
62
+ load(1, pageSize);
63
+ // eslint-disable-next-line react-hooks/exhaustive-deps
64
+ }, deps);
65
+
66
+ const onPageChange = useCallback(
67
+ ({ page: nextPage, pageSize: nextSize }: { page: number; pageSize: number }) => {
68
+ setPage(nextPage);
69
+ setPageSize(nextSize);
70
+ load(nextPage, nextSize);
71
+ },
72
+ [load],
73
+ );
74
+
75
+ /** Every row across every page, for the CSV export */
76
+ const fetchAll = useCallback(async () => {
77
+ const all: Array<Record<string, any>> = [];
78
+ const size = 500;
79
+
80
+ for (let index = 0; ; index += size) {
81
+ const result = await fetcher({ startIndex: index, limit: size });
82
+ all.push(...result.results);
83
+
84
+ if (result.results.length < size) {
85
+ return all;
86
+ }
87
+ }
88
+ // eslint-disable-next-line react-hooks/exhaustive-deps
89
+ }, deps);
90
+
91
+ return { rows, total, isTotalExact, isLoading, page, pageSize, onPageChange, fetchAll };
92
+ }
@@ -0,0 +1 @@
1
+ export * from './report-skeleton.component';
@@ -0,0 +1,46 @@
1
+ import React from 'react';
2
+ import { DataTableSkeleton, SkeletonText } from '@carbon/react';
3
+
4
+ import styles from './report-skeleton.scss';
5
+
6
+ interface ReportSkeletonProps {
7
+ /** How many indicator blocks to outline. Roughly the sections a form has. */
8
+ blocks?: number;
9
+ }
10
+
11
+ /**
12
+ * Placeholder in the shape of a generated report.
13
+ *
14
+ * Reports replaced their whole body while fetching, so a centred spinner left
15
+ * the page blank and shifted everything when the data landed. Outlining the
16
+ * masthead and the indicator tables keeps the layout stable and shows how much
17
+ * is coming.
18
+ */
19
+ export const ReportSkeleton: React.FC<ReportSkeletonProps> = ({ blocks = 2 }) => (
20
+ <div className={styles.skeleton} aria-busy="true" aria-label="Loading report">
21
+ <div className={styles.header}>
22
+ <SkeletonText heading width="45%" />
23
+ <SkeletonText width="70%" />
24
+ </div>
25
+
26
+ {Array.from({ length: blocks }, (_, index) => (
27
+ <div key={index} className={styles.block}>
28
+ <SkeletonText heading width="30%" />
29
+ <DataTableSkeleton showHeader={false} showToolbar={false} columnCount={4} rowCount={5} size="sm" />
30
+ </div>
31
+ ))}
32
+ </div>
33
+ );
34
+
35
+ /**
36
+ * Placeholder for the register and patient-list pages, which are a single
37
+ * table rather than a form.
38
+ */
39
+ export const RegisterSkeleton: React.FC<{ columnCount?: number; rowCount?: number }> = ({
40
+ columnCount = 6,
41
+ rowCount = 8,
42
+ }) => (
43
+ <div className={styles.skeleton} aria-busy="true" aria-label="Loading register">
44
+ <DataTableSkeleton showHeader={false} showToolbar={false} columnCount={columnCount} rowCount={rowCount} size="sm" />
45
+ </div>
46
+ );
@@ -0,0 +1,29 @@
1
+ @use '@carbon/layout';
2
+ @use '@openmrs/esm-styleguide/src/vars' as *;
3
+
4
+ .skeleton {
5
+ padding: layout.$spacing-05;
6
+ margin-bottom: layout.$spacing-05;
7
+ background-color: $ui-02;
8
+ border: 1px solid $ui-03;
9
+ }
10
+
11
+ /* When it stands in for a register table it sits under the caption bar */
12
+ .skeleton:not(:first-child) {
13
+ margin-bottom: 0;
14
+ border-top: none;
15
+ }
16
+
17
+ .header {
18
+ padding-bottom: layout.$spacing-05;
19
+ margin-bottom: layout.$spacing-05;
20
+ border-bottom: 1px solid $ui-03;
21
+ }
22
+
23
+ .block {
24
+ margin-bottom: layout.$spacing-06;
25
+
26
+ &:last-child {
27
+ margin-bottom: 0;
28
+ }
29
+ }
@@ -3,10 +3,18 @@ export const configSchema = {
3
3
  etlBaseUrl: {
4
4
  _type: Type.String,
5
5
  _default: '',
6
- _description: 'ETL endpoint',
6
+ _description: 'Base URL of the ETL service every report reads from, e.g. "/etl"',
7
+ },
8
+ /* Declared because the deployment sets it in this section. Nothing reads it
9
+ yet; without it the config validator reports it as an unknown key. */
10
+ subDomainUrl: {
11
+ _type: Type.String,
12
+ _default: '',
13
+ _description: 'Origin this instance is served from',
7
14
  },
8
15
  };
9
16
 
10
17
  export type Config = {
11
18
  etlBaseUrl: string;
19
+ subDomainUrl: string;
12
20
  };
@@ -0,0 +1,173 @@
1
+ import {
2
+ Activity,
3
+ ChartColumn,
4
+ Document,
5
+ DocumentTasks,
6
+ GenderFemale,
7
+ Growth,
8
+ HealthCross,
9
+ Microscope,
10
+ UserMultiple,
11
+ Warning,
12
+ type CarbonIconType,
13
+ } from '@carbon/react/icons';
14
+
15
+ export type ReportTone = 'violet' | 'magenta' | 'teal' | 'blue';
16
+
17
+ export interface ReportDefinition {
18
+ /** The MOH form number, e.g. `MOH 731` */
19
+ code: string;
20
+ /** Human readable name of the form */
21
+ name: string;
22
+ description: string;
23
+ /** Route registered in root.component.tsx */
24
+ path: string;
25
+ icon: CarbonIconType;
26
+ /** Extra terms matched by the search field */
27
+ keywords: string[];
28
+ }
29
+
30
+ export interface ReportCategory {
31
+ id: string;
32
+ title: string;
33
+ description: string;
34
+ tone: ReportTone;
35
+ reports: ReportDefinition[];
36
+ }
37
+
38
+ export const reportCategories: ReportCategory[] = [
39
+ {
40
+ id: 'hiv-and-chronic-care',
41
+ title: 'HIV & chronic care',
42
+ description: 'Comprehensive care programme summaries',
43
+ tone: 'violet',
44
+ reports: [
45
+ {
46
+ code: 'MOH 731',
47
+ name: 'Comprehensive HIV/AIDS',
48
+ description: 'NASCOP monthly summary for HIV testing, EMTCT, care and treatment.',
49
+ path: '/moh-731',
50
+ icon: HealthCross,
51
+ keywords: ['hiv', 'nascop', 'art', 'prep', 'emtct', 'pmtct', 'tb'],
52
+ },
53
+ {
54
+ code: 'MOH 740',
55
+ name: 'Diabetes & hypertension',
56
+ description: 'Monthly summary for the NCD comprehensive care clinic.',
57
+ path: '/moh-740',
58
+ icon: Activity,
59
+ keywords: ['ncd', 'diabetes', 'hypertension', 'chronic'],
60
+ },
61
+ ],
62
+ },
63
+ {
64
+ id: 'reproductive-and-child-health',
65
+ title: 'Reproductive & child health',
66
+ description: 'RMNCAH service delivery summaries',
67
+ tone: 'magenta',
68
+ reports: [
69
+ {
70
+ code: 'MOH 710',
71
+ name: 'Immunization',
72
+ description: 'Child immunization and vitamin A supplementation summary.',
73
+ path: '/moh-710',
74
+ icon: Growth,
75
+ keywords: ['immunization', 'vaccine', 'child', 'khis', 'bcg', 'polio'],
76
+ },
77
+ {
78
+ code: 'MOH 711',
79
+ name: 'Integrated programme summary',
80
+ description: 'Reproductive and child health, medical and rehabilitation services.',
81
+ path: '/moh-711',
82
+ icon: UserMultiple,
83
+ keywords: ['rmncah', 'anc', 'pnc', 'maternity', 'family planning', 'gbv', 'physiotherapy'],
84
+ },
85
+ {
86
+ code: 'MOH 745',
87
+ name: 'Cancer screening',
88
+ description: 'Monthly cervical and breast cancer screening summary.',
89
+ path: '/moh-745',
90
+ icon: GenderFemale,
91
+ keywords: ['cancer', 'cervical', 'screening', 'via', 'hpv'],
92
+ },
93
+ ],
94
+ },
95
+ {
96
+ id: 'outpatient-and-workload',
97
+ title: 'Outpatient & workload',
98
+ description: 'Morbidity and facility workload returns',
99
+ tone: 'teal',
100
+ reports: [
101
+ {
102
+ code: 'MOH 705A',
103
+ name: 'Outpatient morbidity, under 5',
104
+ description: 'Summary of diagnoses for children under five years.',
105
+ path: '/moh-705a',
106
+ icon: DocumentTasks,
107
+ keywords: ['morbidity', 'outpatient', 'under five', 'children', 'khis'],
108
+ },
109
+ {
110
+ code: 'MOH 705B',
111
+ name: 'Outpatient morbidity, over 5',
112
+ description: 'Summary of diagnoses for clients aged five years and above.',
113
+ path: '/moh-705b',
114
+ icon: Document,
115
+ keywords: ['morbidity', 'outpatient', 'above five', 'adults', 'khis'],
116
+ },
117
+ {
118
+ code: 'MOH 717',
119
+ name: 'Service workload',
120
+ description: 'Monthly service workload across facility departments.',
121
+ path: '/moh-717',
122
+ icon: ChartColumn,
123
+ keywords: ['workload', 'inpatient', 'pharmacy', 'mortuary', 'theatre', 'maternity'],
124
+ },
125
+ ],
126
+ },
127
+ {
128
+ id: 'diagnostics-and-surveillance',
129
+ title: 'Diagnostics & surveillance',
130
+ description: 'Laboratory and disease surveillance returns',
131
+ tone: 'blue',
132
+ reports: [
133
+ {
134
+ code: 'MOH 706',
135
+ name: 'Laboratory services',
136
+ description: 'Monthly summary of laboratory investigations and results.',
137
+ path: '/moh-706',
138
+ icon: Microscope,
139
+ keywords: ['laboratory', 'lab', 'urine', 'parasitology', 'blood chemistry'],
140
+ },
141
+ {
142
+ code: 'MOH 505',
143
+ name: 'IDSR epidemic monitoring',
144
+ description: 'Weekly monitoring form for priority notifiable diseases.',
145
+ path: '/moh-505',
146
+ icon: Warning,
147
+ keywords: ['idsr', 'surveillance', 'epidemic', 'notifiable', 'outbreak'],
148
+ },
149
+ ],
150
+ },
151
+ ];
152
+
153
+ export const totalReportCount = reportCategories.reduce((count, category) => count + category.reports.length, 0);
154
+
155
+ export function filterCategories(categories: ReportCategory[], searchTerm: string): ReportCategory[] {
156
+ const term = searchTerm.trim().toLowerCase();
157
+
158
+ if (!term) {
159
+ return categories;
160
+ }
161
+
162
+ return categories
163
+ .map((category) => ({
164
+ ...category,
165
+ reports: category.reports.filter((report) =>
166
+ [report.code, report.name, report.description, category.title, ...report.keywords]
167
+ .join(' ')
168
+ .toLowerCase()
169
+ .includes(term),
170
+ ),
171
+ }))
172
+ .filter((category) => category.reports.length > 0);
173
+ }