@ampath/esm-reports-app 1.0.0-next.7 → 1.0.0-next.71
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/197.js +1 -1
- package/dist/282.js +1 -1
- package/dist/282.js.map +1 -1
- package/dist/295.css +1 -0
- package/dist/295.css.map +1 -0
- package/dist/295.js +1 -0
- package/dist/295.js.map +1 -0
- package/dist/299.js +2 -0
- package/dist/299.js.LICENSE.txt +3 -0
- package/dist/299.js.map +1 -0
- package/dist/300.js +1 -1
- package/dist/311.js +1 -1
- package/dist/311.js.map +1 -1
- package/dist/335.js +1 -1
- package/dist/353.js +1 -1
- package/dist/353.js.map +1 -1
- package/dist/354.js +2 -0
- package/dist/354.js.LICENSE.txt +20 -0
- package/dist/354.js.map +1 -0
- package/dist/410.js +1 -0
- package/dist/410.js.map +1 -0
- package/dist/457.js +2 -0
- package/dist/457.js.LICENSE.txt +261 -0
- package/dist/457.js.map +1 -0
- package/dist/478.js +1 -1
- package/dist/478.js.map +1 -1
- package/dist/480.js +1 -0
- package/dist/540.js +1 -1
- package/dist/540.js.map +1 -1
- package/dist/55.js +1 -1
- package/dist/580.js +1 -0
- package/dist/580.js.map +1 -0
- package/dist/600.js +2 -0
- package/dist/600.js.LICENSE.txt +35 -0
- package/dist/600.js.map +1 -0
- package/dist/652.js +1 -1
- package/dist/781.js +1 -0
- package/dist/781.js.map +1 -0
- package/dist/919.css +1 -0
- package/dist/919.css.map +1 -0
- package/dist/919.js +1 -0
- package/dist/919.js.map +1 -0
- package/dist/961.js +1 -1
- package/dist/961.js.map +1 -1
- package/dist/99.js +1 -1
- package/dist/99.js.map +1 -0
- package/dist/ampath-esm-reports-app.js +1 -1
- package/dist/ampath-esm-reports-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +4 -2
- 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-error/index.ts +1 -0
- package/src/common/report-error/show-report-error.ts +25 -0
- package/src/common/report-filters/date-range.ts +137 -0
- package/src/common/report-filters/index.ts +4 -0
- package/src/common/report-filters/report-filters.component.tsx +317 -99
- package/src/common/report-filters/report-filters.scss +255 -32
- package/src/common/report-filters/types.ts +79 -0
- package/src/common/report-filters/use-restore-period.ts +43 -0
- package/src/common/report-header/index.ts +1 -0
- package/src/common/report-header/use-location-details.ts +58 -0
- package/src/common/report-page/download-sheet-pdf.ts +59 -0
- package/src/common/report-page/index.ts +3 -0
- package/src/common/report-page/report-page.component.tsx +32 -0
- package/src/common/report-page/report-page.scss +45 -0
- package/src/common/report-page/use-page-scale.ts +60 -0
- package/src/common/report-placeholder/has-report-data.ts +15 -0
- package/src/common/report-placeholder/index.ts +3 -0
- package/src/common/report-placeholder/report-placeholder.component.tsx +81 -0
- package/src/common/report-placeholder/report-placeholder.scss +48 -0
- package/src/common/report-placeholder/use-generated-location.ts +62 -0
- package/src/common/report-register/index.ts +6 -0
- package/src/common/report-register/register-columns.ts +46 -0
- package/src/common/report-register/register-layout.component.tsx +265 -0
- package/src/common/report-register/register-layout.scss +150 -0
- package/src/common/report-register/register-table.scss +71 -0
- package/src/common/report-register/report-parents.ts +27 -0
- package/src/common/report-register/rows-to-csv.ts +89 -0
- package/src/common/report-register/use-location-name.ts +54 -0
- package/src/common/report-register/use-patient-list.ts +92 -0
- package/src/common/report-skeleton/index.ts +1 -0
- package/src/common/report-skeleton/report-skeleton.component.tsx +46 -0
- package/src/common/report-skeleton/report-skeleton.scss +29 -0
- package/src/config-schema.ts +9 -1
- package/src/dashboard/report-catalog.ts +173 -0
- package/src/dashboard/reports-dasboard.tsx +189 -18
- package/src/dashboard/reports-dashboard.module.scss +357 -14
- package/src/index.ts +13 -1
- package/src/registers/moh-240/moh-240-register.scss +41 -0
- package/src/registers/moh-240/moh-240.component.test.tsx +166 -0
- package/src/registers/moh-240/moh-240.component.tsx +164 -0
- package/src/registers/moh-362/moh-362-lab-register.component.tsx +392 -0
- package/src/registers/moh-362/moh-362-linkage-register.component.tsx +120 -0
- package/src/registers/moh-362/moh-362-register.scss +222 -0
- package/src/registers/moh-362/moh-362.component.tsx +99 -0
- package/src/registers/moh-362/moh-362.scss +11 -0
- package/src/registers/moh-362/moh-362.types.ts +97 -0
- package/src/registers/register-catalog.ts +96 -0
- package/src/registers/tb-lab-register/tb-lab-register-table.component.tsx +116 -0
- package/src/registers/tb-lab-register/tb-lab-register-table.scss +123 -0
- package/src/registers/tb-lab-register/tb-lab-register.columns.ts +164 -0
- package/src/registers/tb-lab-register/tb-lab-register.component.test.tsx +232 -0
- package/src/registers/tb-lab-register/tb-lab-register.component.tsx +153 -0
- package/src/reports/datatable-wrapper/datatable-wrapper.component.tsx +63 -48
- package/src/reports/moh-240/moh-240-header.component.tsx +62 -0
- package/src/reports/moh-240/moh-240.columns.ts +113 -0
- package/src/reports/moh-240/moh-240.component.tsx +72 -9
- package/src/reports/moh-240/moh-240.scss +40 -0
- package/src/reports/moh-240/sub-reports/moh-240-register.component.tsx +31 -37
- package/src/reports/moh-240/sub-reports/page-summary.component.tsx +143 -106
- package/src/reports/moh-505/moh-505-header.component.tsx +67 -0
- package/src/reports/moh-505/moh-505.component.tsx +441 -0
- package/src/reports/moh-505/moh-505.scss +117 -0
- package/src/reports/moh-705B/moh-204b-register.component.tsx +244 -0
- package/src/reports/moh-705B/moh-204b.columns.ts +39 -0
- package/src/reports/moh-705B/moh-705b.component.tsx +659 -0
- package/src/reports/moh-705B/moh-705b.scss +172 -0
- package/src/reports/moh-705a/moh-705a.component.tsx +661 -0
- package/src/reports/moh-705a/moh-705a.scss +172 -0
- package/src/reports/moh-705a/registers/moh-204a-register.component.tsx +329 -0
- package/src/reports/moh-705a/registers/moh-204a.columns.ts +57 -0
- package/src/reports/moh-705a/type.ts +6 -0
- package/src/reports/moh-706/moh-706-header.component.tsx +66 -0
- package/src/reports/moh-706/moh-706.component.tsx +150 -24
- package/src/reports/moh-706/moh-706.scss +119 -11
- package/src/reports/moh-706/sub-reports/bacteriology/bacteriology.component.tsx +54 -62
- package/src/reports/moh-706/sub-reports/blood-chemistry/blood-chemistry.component.tsx +228 -317
- package/src/reports/moh-706/sub-reports/drug-susceptibility-testing/drug-susceptibility-testing.component.tsx +31 -46
- package/src/reports/moh-706/sub-reports/haematology/haematology.component.tsx +47 -55
- package/src/reports/moh-706/sub-reports/histology-and-cytology/histology-and-cytology.component.tsx +43 -58
- package/src/reports/moh-706/sub-reports/parasitology/parasitology.component.tsx +31 -39
- package/src/reports/moh-706/sub-reports/serology/serology.component.tsx +26 -34
- package/src/reports/moh-706/sub-reports/specimen-referral-to-higher-levels/specimen-referral-to-higher-levels.component.tsx +9 -24
- package/src/reports/moh-706/sub-reports/urine-analysis/urine-analysis.component.tsx +43 -51
- package/src/reports/moh-710/moh-710.component.tsx +799 -632
- package/src/reports/moh-710/moh-710.scss +142 -21
- package/src/reports/moh-710/registers/moh-510/moh-510.columns.ts +40 -0
- package/src/reports/moh-710/registers/moh-510/moh-510.component.tsx +144 -0
- package/src/reports/moh-710/registers/moh-510/moh-510.scss +130 -0
- package/src/reports/moh-710/registers/moh-511-710-register.component.tsx +268 -0
- package/src/reports/moh-710/registers/moh-511-710.columns.ts +52 -0
- package/src/reports/moh-710/registers/type.ts +31 -0
- package/src/reports/moh-711/moh-711.component.tsx +138 -1269
- package/src/reports/moh-711/moh711.scss +135 -20
- package/src/reports/moh-711/registers/moh-333-register.component.tsx +637 -0
- package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
- package/src/reports/moh-711/registers/moh-405-register.component.tsx +520 -0
- package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
- package/src/reports/moh-711/registers/moh-406-register.component.tsx +530 -0
- package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
- package/src/reports/moh-711/registers/moh-510-register.component.tsx +55 -0
- package/src/reports/moh-711/registers/moh-511-register.component.tsx +270 -0
- package/src/reports/moh-711/registers/moh-511.columns.ts +59 -0
- package/src/reports/moh-711/registers/type.ts +192 -0
- package/src/reports/moh-711/sections/anc.component.tsx +159 -0
- package/src/reports/moh-711/sections/cervical-cancer.component.tsx +148 -0
- package/src/reports/moh-711/sections/chanis.component.tsx +366 -0
- package/src/reports/moh-711/sections/family-planning.component.tsx +221 -0
- package/src/reports/moh-711/sections/gbv.component.tsx +115 -0
- package/src/reports/moh-711/sections/maternity.component.tsx +321 -0
- package/src/reports/moh-711/sections/medical-social-work.component.tsx +73 -0
- package/src/reports/moh-711/sections/other.component.tsx +47 -0
- package/src/reports/moh-711/sections/physiotherapy.component.tsx +61 -0
- package/src/reports/moh-711/sections/pnc.component.tsx +127 -0
- package/src/reports/moh-711/sections/post-abortion.component.tsx +42 -0
- package/src/reports/moh-711/sections/rehabilitation.component.tsx +57 -0
- package/src/reports/moh-711/sections/report-compiled-by.component.tsx +50 -0
- package/src/reports/moh-711/sections/tb-screening.component.tsx +57 -0
- package/src/reports/moh-717/moh-717.component.tsx +149 -985
- package/src/reports/moh-717/moh717.scss +217 -21
- package/src/reports/moh-717/registers/nutrition-register.component.tsx +48 -0
- package/src/reports/moh-717/sections/finance.component.tsx +42 -0
- package/src/reports/moh-717/sections/inpatient.component.tsx +374 -0
- package/src/reports/moh-717/sections/maternity.component.tsx +126 -0
- package/src/reports/moh-717/sections/medical-records.component.tsx +35 -0
- package/src/reports/moh-717/sections/mortuary.component.tsx +43 -0
- package/src/reports/moh-717/sections/operations.component.tsx +53 -0
- package/src/reports/moh-717/sections/orthopaedic-trauma.component.tsx +98 -0
- package/src/reports/moh-717/sections/outpatient.component.tsx +597 -0
- package/src/reports/moh-717/sections/pharmacy.component.tsx +43 -0
- package/src/reports/moh-717/sections/preparedby.component.tsx +52 -0
- package/src/reports/moh-717/sections/special-services.component.tsx +114 -0
- package/src/reports/moh-731/linelist/moh-731-indicators.ts +1122 -0
- package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +472 -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 +266 -0
- package/src/reports/moh-731/moh-731.scss +499 -0
- package/src/reports/moh-731/registers/moh-366.columns.ts +38 -0
- package/src/reports/moh-731/registers/moh-366.component.tsx +471 -0
- package/src/reports/moh-731/registers/prep-register.component.tsx +284 -0
- package/src/reports/moh-731/registers/prep.columns.ts +34 -0
- package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +1206 -0
- package/src/reports/moh-731/sections/hiv-testing-and-pre-exposure.component.tsx +727 -0
- package/src/reports/moh-731/sections/pmtct.component.tsx +572 -0
- package/src/reports/moh-731/summary/moh-731-summary.component.tsx +313 -0
- package/src/reports/moh-731/summary/moh-731-summary.config.ts +529 -0
- package/src/reports/moh-731/summary/moh-731-summary.scss +242 -0
- package/src/reports/moh-740/moh-740.component.scss +202 -0
- package/src/reports/moh-740/moh-740.component.tsx +1332 -0
- package/src/reports/moh-740/moh-740.resource.ts +54 -0
- package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +149 -0
- package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +123 -0
- package/src/reports/moh-740/registers/moh-740-patient-list.scss +0 -0
- package/src/reports/moh-740/registers/moh-740-patient-list.tsx +60 -0
- package/src/reports/moh-740/registers/moh-740.columns.ts +54 -0
- package/src/reports/moh-740/shared/data-cell/data-cell.scss +6 -0
- package/src/reports/moh-740/shared/data-cell/data-cell.tsx +23 -0
- package/src/reports/moh-740/shared/utils/indicator-register-map.ts +159 -0
- package/src/reports/moh-740/types/index.ts +223 -0
- package/src/reports/moh-745/moh-745.component.tsx +1021 -0
- package/src/reports/moh-745/moh-745.scss +132 -0
- package/src/reports/moh-745/registers/moh-412-register.component.tsx +249 -0
- package/src/reports/moh-745/registers/moh-412.columns.ts +52 -0
- package/src/reports/moh-745/registers/type.ts +181 -0
- package/src/reports/table-wrapper/table-row-mapper.component.tsx +57 -0
- package/src/reports/table-wrapper/table-wrapper.component.tsx +23 -14
- package/src/reports/table-wrapper/table-wrapper.scss +1 -16
- package/src/resources/moh-362.resource.ts +27 -0
- package/src/resources/moh-505.resource.ts +69 -0
- package/src/resources/moh-705.resource.ts +106 -0
- package/src/resources/moh-706.resource.ts +80 -0
- package/src/resources/moh-710.resource.ts +32 -9
- package/src/resources/moh-711.resource.ts +137 -9
- package/src/resources/moh-717.resource.ts +26 -9
- package/src/resources/moh-731.resource.ts +90 -0
- package/src/resources/moh-745.resource.ts +76 -0
- package/src/resources/patient-list-page.ts +65 -0
- package/src/root.component.tsx +59 -8
- package/src/root.scss +36 -0
- package/src/routes.json +3 -1
- package/src/utils/format-indicator.ts +39 -0
- package/src/utils/get-base-url.ts +39 -7
- package/src/utils/utils.ts +36 -0
- package/tsconfig.json +1 -1
- package/webpack.config.js +22 -1
- package/dist/138.js +0 -2
- package/dist/138.js.LICENSE.txt +0 -20
- package/dist/138.js.map +0 -1
- package/dist/184.js +0 -2
- package/dist/184.js.LICENSE.txt +0 -14
- package/dist/184.js.map +0 -1
- package/dist/205.js +0 -1
- package/dist/205.js.map +0 -1
- package/dist/711.js +0 -1
- package/dist/711.js.map +0 -1
- package/dist/812.js +0 -1
- package/dist/812.js.map +0 -1
- package/dist/ampath-esm-reports-app.js.buildmanifest.json +0 -539
- package/dist/main.js +0 -1
- package/dist/main.js.map +0 -1
|
@@ -0,0 +1,71 @@
|
|
|
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
|
+
/* Long prose columns (diagnoses, referrals, comments) wrap inside a bounded
|
|
48
|
+
width instead of stretching the row past the fold. */
|
|
49
|
+
.tableBordered td.wrapCell {
|
|
50
|
+
white-space: normal;
|
|
51
|
+
min-width: 8rem;
|
|
52
|
+
max-width: 16rem;
|
|
53
|
+
overflow-wrap: anywhere;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.tableStriped tbody tr:nth-of-type(odd) {
|
|
57
|
+
background-color: $ui-01;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
.tableStriped tbody tr:hover {
|
|
61
|
+
background-color: $brand-primary-10;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.textCentre {
|
|
65
|
+
text-align: center;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.verticalText {
|
|
69
|
+
writing-mode: vertical-rl;
|
|
70
|
+
transform: rotate(180deg);
|
|
71
|
+
}
|
|
@@ -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,89 @@
|
|
|
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
|
+
/**
|
|
15
|
+
* Forces text Excel cannot mangle. A cell starting with `=`, `+`, `-` or `@`
|
|
16
|
+
* is parsed as a formula (`+2547...` becomes `#NAME?`), and a long digit-only
|
|
17
|
+
* cell is reinterpreted as a number (leading zeros dropped, switched to
|
|
18
|
+
* scientific notation), so both are emitted as an explicit text formula
|
|
19
|
+
* (`"..."` quoted inside `=`, which Excel renders as literal text).
|
|
20
|
+
*/
|
|
21
|
+
const FORMULA_PREFIX = /^[=+\-@]/;
|
|
22
|
+
const LONG_DIGITS = /^\d{9,}$/;
|
|
23
|
+
|
|
24
|
+
function escapeCell(value: unknown, forceText = false): string {
|
|
25
|
+
const text = value === null || value === undefined ? '' : String(value).replace(/\s+/g, ' ').trim();
|
|
26
|
+
|
|
27
|
+
if (forceText && text !== '') {
|
|
28
|
+
return `="${text.replace(/"/g, '""')}"`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (text !== '' && (FORMULA_PREFIX.test(text) || LONG_DIGITS.test(text))) {
|
|
32
|
+
return `="${text.replace(/"/g, '""')}"`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Serialises using the register's own columns, so the file matches the form */
|
|
39
|
+
export function columnsToCsv(
|
|
40
|
+
rows: Array<Record<string, any>>,
|
|
41
|
+
columns: RegisterColumn[],
|
|
42
|
+
options?: {
|
|
43
|
+
/** A second header row (e.g. the printed form's column letters) */
|
|
44
|
+
subHeaderRow?: string[];
|
|
45
|
+
},
|
|
46
|
+
): string {
|
|
47
|
+
const headerRows = [
|
|
48
|
+
columns.map((column) => escapeCell(column.header)).join(','),
|
|
49
|
+
...(options?.subHeaderRow ? [options.subHeaderRow.map((cell) => escapeCell(cell)).join(',')] : []),
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
return [
|
|
53
|
+
...headerRows,
|
|
54
|
+
...rows.map((row, index) => columns.map((column) => escapeCell(column.value(row, index), column.text)).join(',')),
|
|
55
|
+
].join('\r\n');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Fallback for registers with no column spec yet: the response's own fields */
|
|
59
|
+
export function rowsToCsv(rows: Array<Record<string, any>>): string {
|
|
60
|
+
if (!rows.length) {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Union of keys, not just the first row's: endpoints omit fields that are
|
|
65
|
+
null for a given client, so keying off row one drops columns. */
|
|
66
|
+
const keys = new Set<string>();
|
|
67
|
+
rows.forEach((row) => Object.keys(row).forEach((key) => keys.add(key)));
|
|
68
|
+
const columns = Array.from(keys);
|
|
69
|
+
|
|
70
|
+
return [
|
|
71
|
+
columns.map((column) => escapeCell(column)).join(','),
|
|
72
|
+
...rows.map((row) => columns.map((column) => escapeCell(row[column])).join(',')),
|
|
73
|
+
].join('\r\n');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Prompts the browser to save `csv` under `filename` */
|
|
77
|
+
export function downloadCsv(csv: string, filename: string): void {
|
|
78
|
+
// the BOM is what makes Excel read the file as UTF-8 rather than latin-1
|
|
79
|
+
const blob = new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' });
|
|
80
|
+
const url = URL.createObjectURL(blob);
|
|
81
|
+
const link = document.createElement('a');
|
|
82
|
+
|
|
83
|
+
link.href = url;
|
|
84
|
+
link.download = filename;
|
|
85
|
+
document.body.appendChild(link);
|
|
86
|
+
link.click();
|
|
87
|
+
document.body.removeChild(link);
|
|
88
|
+
URL.revokeObjectURL(url);
|
|
89
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/src/config-schema.ts
CHANGED
|
@@ -3,10 +3,18 @@ export const configSchema = {
|
|
|
3
3
|
etlBaseUrl: {
|
|
4
4
|
_type: Type.String,
|
|
5
5
|
_default: '',
|
|
6
|
-
_description: 'ETL
|
|
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
|
+
}
|