@ampath/esm-reports-app 1.0.0-next.8 → 1.0.0-next.80
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/0.css +1 -0
- package/dist/0.css.map +1 -0
- package/dist/0.js +1 -0
- package/dist/0.js.map +1 -0
- 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/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/757.js +1 -0
- package/dist/757.js.map +1 -0
- package/dist/781.js +1 -0
- package/dist/781.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 +264 -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 +225 -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 +29 -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 +191 -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-267/moh-267-register.component.tsx +407 -0
- package/src/registers/moh-267/moh-267-register.scss +298 -0
- package/src/registers/moh-267/moh-267-summary.ts +102 -0
- package/src/registers/moh-267/moh-267.component.tsx +94 -0
- package/src/registers/moh-267/moh-267.types.ts +47 -0
- package/src/registers/moh-362/moh-362-lab-register.component.tsx +551 -0
- package/src/registers/moh-362/moh-362-lab-register.scss +412 -0
- package/src/registers/moh-362/moh-362-linkage-register.component.tsx +215 -0
- package/src/registers/moh-362/moh-362-linkage-register.scss +91 -0
- package/src/registers/moh-362/moh-362.component.tsx +117 -0
- package/src/registers/moh-362/moh-362.scss +11 -0
- package/src/registers/moh-362/moh-362.types.ts +87 -0
- package/src/registers/presumptive-tb/presumptive-tb-register.component.tsx +391 -0
- package/src/registers/presumptive-tb/presumptive-tb-register.scss +262 -0
- package/src/registers/presumptive-tb/presumptive-tb.component.tsx +92 -0
- package/src/registers/presumptive-tb/presumptive-tb.types.ts +88 -0
- package/src/registers/register-catalog.ts +202 -0
- package/src/registers/register-file-name.ts +18 -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/registers/tb4/tb4-parts.tsx +164 -0
- package/src/registers/tb4/tb4-patient-sheet.component.tsx +418 -0
- package/src/registers/tb4/tb4-register.scss +282 -0
- package/src/registers/tb4/tb4-treatment-sheet.component.tsx +349 -0
- package/src/registers/tb4/tb4.component.tsx +120 -0
- package/src/registers/tb4/tb4.types.ts +129 -0
- package/src/reports/acf-summary/acf-summary-form.component.tsx +176 -0
- package/src/reports/acf-summary/acf-summary-linelist.component.tsx +86 -0
- package/src/reports/acf-summary/acf-summary.component.tsx +97 -0
- package/src/reports/acf-summary/acf-summary.counts.ts +37 -0
- package/src/reports/acf-summary/acf-summary.scss +162 -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 +246 -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 +331 -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 +35 -28
- package/src/reports/moh-706/sub-reports/blood-chemistry/blood-chemistry.component.tsx +229 -305
- package/src/reports/moh-706/sub-reports/haematology/haematology.component.tsx +26 -19
- package/src/reports/moh-706/sub-reports/histology-and-cytology/histology-and-cytology.component.tsx +23 -23
- package/src/reports/moh-706/sub-reports/parasitology/parasitology.component.tsx +25 -18
- package/src/reports/moh-706/sub-reports/serology/serology.component.tsx +21 -14
- package/src/reports/moh-706/sub-reports/specimen-referral-to-higher-levels/specimen-referral-to-higher-levels.component.tsx +2 -2
- package/src/reports/moh-706/sub-reports/urine-analysis/urine-analysis.component.tsx +27 -20
- package/src/reports/moh-710/moh-710.component.tsx +815 -632
- package/src/reports/moh-710/moh-710.scss +142 -21
- package/src/reports/moh-710/registers/moh-413/moh-413-columns.ts +22 -0
- package/src/reports/moh-710/registers/moh-413/moh-413.component.tsx +140 -0
- package/src/reports/moh-710/registers/moh-413/moh-413.scss +64 -0
- 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 +191 -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 +153 -1269
- package/src/reports/moh-711/moh711.scss +135 -20
- package/src/reports/moh-711/registers/moh-333-register.component.tsx +680 -0
- package/src/reports/moh-711/registers/moh-333.columns.ts +119 -0
- package/src/reports/moh-711/registers/moh-405-register.component.tsx +566 -0
- package/src/reports/moh-711/registers/moh-405.columns.ts +120 -0
- package/src/reports/moh-711/registers/moh-406-register.component.tsx +581 -0
- package/src/reports/moh-711/registers/moh-406.columns.ts +133 -0
- package/src/reports/moh-711/registers/moh-407-register.component.tsx +157 -0
- package/src/reports/moh-711/registers/moh-407.columns.ts +24 -0
- package/src/reports/moh-711/registers/moh-407.scss +64 -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/moh-512-columns.ts +94 -0
- package/src/reports/moh-711/registers/moh-512.component.tsx +191 -0
- package/src/reports/moh-711/registers/moh-512.scss +64 -0
- package/src/reports/moh-711/registers/tb-register-columns.ts +17 -0
- package/src/reports/moh-711/registers/tb-register.component.tsx +96 -0
- package/src/reports/moh-711/registers/tb-register.scss +64 -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 +389 -0
- package/src/reports/moh-711/sections/family-planning.component.tsx +299 -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 +94 -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-729b/moh-729b-form.component.tsx +385 -0
- package/src/reports/moh-729b/moh-729b-linelist.component.tsx +205 -0
- package/src/reports/moh-729b/moh-729b-summary.component.tsx +258 -0
- package/src/reports/moh-729b/moh-729b-summary.config.ts +229 -0
- package/src/reports/moh-729b/moh-729b.component.tsx +157 -0
- package/src/reports/moh-729b/moh-729b.config.ts +235 -0
- package/src/reports/moh-729b/moh-729b.scss +276 -0
- package/src/reports/moh-729b/moh-729b.types.ts +111 -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 +269 -0
- package/src/reports/moh-731/moh-731.scss +496 -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 +527 -0
- package/src/reports/moh-731/summary/moh-731-summary.scss +247 -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 +2058 -0
- package/src/reports/moh-745/moh-745.scss +142 -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 +223 -0
- package/src/reports/table-wrapper/table-row-mapper.component.tsx +20 -5
- 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-267.resource.ts +27 -0
- 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 +118 -0
- package/src/resources/moh-706.resource.ts +80 -0
- package/src/resources/moh-710.resource.ts +60 -9
- package/src/resources/moh-711.resource.ts +206 -9
- package/src/resources/moh-717.resource.ts +26 -9
- package/src/resources/moh-729b.resource.ts +25 -0
- 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 +66 -0
- package/src/resources/presumptive-tb.resource.ts +27 -0
- package/src/root.component.tsx +79 -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 +30 -1
- 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/812.js +0 -1
- package/dist/812.js.map +0 -1
- package/dist/879.js +0 -1
- package/dist/879.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,225 @@
|
|
|
1
|
+
import type { toCanvas as ToCanvas } from 'html-to-image';
|
|
2
|
+
|
|
3
|
+
/** Paper sizes, in millimetres, portrait */
|
|
4
|
+
const PAPER_MM = {
|
|
5
|
+
a4: { width: 210, height: 297 },
|
|
6
|
+
a3: { width: 297, height: 420 },
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export type PaperSize = keyof typeof PAPER_MM;
|
|
10
|
+
export type PageOrientation = 'portrait' | 'landscape';
|
|
11
|
+
|
|
12
|
+
/** Capture resolution: 3 device pixels per CSS pixel keeps 5.5pt type sharp */
|
|
13
|
+
const PIXEL_RATIO = 3;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Pages stored as JPEG are captured at 2.5 device pixels per CSS pixel (some
|
|
17
|
+
* 240 dpi on paper) and quality 0.8: 3.6pt type stays crisp, and a page is
|
|
18
|
+
* about 0.7MB rather than 1.1MB at 3x and 0.92
|
|
19
|
+
*/
|
|
20
|
+
const JPEG_PIXEL_RATIO = 2.5;
|
|
21
|
+
const JPEG_QUALITY = 0.8;
|
|
22
|
+
|
|
23
|
+
/** Waits for the browser to lay out and paint a change */
|
|
24
|
+
const nextPaint = () =>
|
|
25
|
+
new Promise<void>((resolve) => requestAnimationFrame(() => requestAnimationFrame(() => resolve())));
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A copy of a sheet at paper size, without the on-screen magnification
|
|
29
|
+
* (`--page-zoom`), laid out off screen: the sheet the user is looking at never
|
|
30
|
+
* changes while the file is prepared, and a sheet in a hidden tab, which has
|
|
31
|
+
* no size of its own, can be measured and saved without showing it.
|
|
32
|
+
*/
|
|
33
|
+
const stageCopy = (sheet: HTMLElement) => {
|
|
34
|
+
const stage = document.createElement('div');
|
|
35
|
+
stage.setAttribute('aria-hidden', 'true');
|
|
36
|
+
Object.assign(stage.style, { position: 'fixed', top: '0', left: '-100000px', pointerEvents: 'none' });
|
|
37
|
+
const copy = sheet.cloneNode(true) as HTMLElement;
|
|
38
|
+
copy.style.setProperty('--page-zoom', '1');
|
|
39
|
+
copy.style.margin = '0';
|
|
40
|
+
stage.appendChild(copy);
|
|
41
|
+
document.body.appendChild(stage);
|
|
42
|
+
return { stage, copy };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Captures a sheet by copying every element's computed style onto the capture,
|
|
47
|
+
* so it works for any sheet, web fonts included.
|
|
48
|
+
*/
|
|
49
|
+
async function captureWithComputedStyles(
|
|
50
|
+
sheet: HTMLElement,
|
|
51
|
+
toCanvas: typeof ToCanvas,
|
|
52
|
+
fontEmbedCSS: string,
|
|
53
|
+
pixelRatio: number,
|
|
54
|
+
): Promise<HTMLCanvasElement> {
|
|
55
|
+
const { stage, copy } = stageCopy(sheet);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
await nextPaint();
|
|
59
|
+
return await toCanvas(copy, {
|
|
60
|
+
pixelRatio,
|
|
61
|
+
backgroundColor: '#fff',
|
|
62
|
+
width: copy.offsetWidth,
|
|
63
|
+
height: copy.offsetHeight,
|
|
64
|
+
fontEmbedCSS,
|
|
65
|
+
});
|
|
66
|
+
} finally {
|
|
67
|
+
stage.remove();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* The page's own CSS rules that can apply to the sheets: those naming a class
|
|
73
|
+
* used in them, and the element-only rules (the styleguide's reset), within
|
|
74
|
+
* the media queries that hold now.
|
|
75
|
+
*/
|
|
76
|
+
function sheetStyles(sheets: HTMLElement[]): string {
|
|
77
|
+
const classes = new Set<string>();
|
|
78
|
+
for (const sheet of sheets) {
|
|
79
|
+
for (const element of [sheet, ...Array.from(sheet.querySelectorAll('[class]'))]) {
|
|
80
|
+
element.classList.forEach((name) => classes.add(`.${CSS.escape(name)}`));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
const classList = Array.from(classes);
|
|
84
|
+
|
|
85
|
+
const rules: string[] = [];
|
|
86
|
+
const keep = (rule: CSSRule) => {
|
|
87
|
+
if (rule instanceof CSSStyleRule) {
|
|
88
|
+
const selector = rule.selectorText;
|
|
89
|
+
if (!/[.#]/.test(selector) || classList.some((name) => selector.includes(name))) {
|
|
90
|
+
rules.push(rule.cssText);
|
|
91
|
+
}
|
|
92
|
+
} else if (rule instanceof CSSMediaRule && window.matchMedia(rule.conditionText).matches) {
|
|
93
|
+
Array.from(rule.cssRules).forEach(keep);
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
for (const styleSheet of Array.from(document.styleSheets)) {
|
|
97
|
+
let sheetRules: CSSRuleList;
|
|
98
|
+
try {
|
|
99
|
+
sheetRules = styleSheet.cssRules;
|
|
100
|
+
} catch {
|
|
101
|
+
// a stylesheet from another origin cannot be read, and styles no sheet
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
Array.from(sheetRules).forEach(keep);
|
|
105
|
+
}
|
|
106
|
+
return rules.join('\n');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Captures a sheet styled wholly by the page's stylesheets and set in fonts
|
|
111
|
+
* every computer has, by drawing its markup with those rules: some twenty times
|
|
112
|
+
* quicker than copying every element's computed style, which matters for a
|
|
113
|
+
* register of many pages. Web fonts would not load in the drawing.
|
|
114
|
+
*/
|
|
115
|
+
async function captureWithStylesheets(sheet: HTMLElement, css: string, pixelRatio: number): Promise<HTMLCanvasElement> {
|
|
116
|
+
const { stage, copy } = stageCopy(sheet);
|
|
117
|
+
const width = copy.offsetWidth;
|
|
118
|
+
const height = copy.offsetHeight;
|
|
119
|
+
stage.remove();
|
|
120
|
+
|
|
121
|
+
const markup = new XMLSerializer().serializeToString(copy);
|
|
122
|
+
const svg =
|
|
123
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}">` +
|
|
124
|
+
`<foreignObject width="100%" height="100%"><div xmlns="http://www.w3.org/1999/xhtml">` +
|
|
125
|
+
`<style>${css.replace(/</g, '\\3c ')}</style>${markup}</div></foreignObject></svg>`;
|
|
126
|
+
|
|
127
|
+
const image = new Image();
|
|
128
|
+
image.src = `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svg)}`;
|
|
129
|
+
await image.decode();
|
|
130
|
+
|
|
131
|
+
const canvas = document.createElement('canvas');
|
|
132
|
+
canvas.width = Math.round(width * pixelRatio);
|
|
133
|
+
canvas.height = Math.round(height * pixelRatio);
|
|
134
|
+
const context = canvas.getContext('2d');
|
|
135
|
+
if (!context) {
|
|
136
|
+
throw new Error('This browser cannot draw the page for the PDF');
|
|
137
|
+
}
|
|
138
|
+
context.fillStyle = '#fff';
|
|
139
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
140
|
+
context.drawImage(image, 0, 0, canvas.width, canvas.height);
|
|
141
|
+
return canvas;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Saves sheets as a PDF file, one sheet to a page: A4 unless another paper
|
|
146
|
+
* size is given (the Presumptive TB Register is an A3 form).
|
|
147
|
+
*
|
|
148
|
+
* Each sheet is captured as drawn on screen and placed on its page within the
|
|
149
|
+
* given margin, scaled down only if it would not otherwise fit, so the file is
|
|
150
|
+
* the form itself rather than the browser's print layout (no headers, footers
|
|
151
|
+
* or print dialog). Sheets are captured one at a time, so a long register does
|
|
152
|
+
* not hold every page's image in memory at once.
|
|
153
|
+
*
|
|
154
|
+
* Two options keep a many-page file quick to make:
|
|
155
|
+
* - `stylesheetsOnly`, for sheets styled wholly by the page's stylesheets and
|
|
156
|
+
* set in fonts every computer has (Arial, Georgia): each is drawn from its
|
|
157
|
+
* markup and those rules, in a fraction of a second rather than seconds.
|
|
158
|
+
* - `image: 'jpeg'`: each page is stored as a JPEG rather than a PNG, which is
|
|
159
|
+
* many times quicker to put in the file and makes it smaller, with no
|
|
160
|
+
* visible loss in printed type.
|
|
161
|
+
*/
|
|
162
|
+
export async function downloadSheetsPdf(
|
|
163
|
+
sheets: HTMLElement[],
|
|
164
|
+
fileName: string,
|
|
165
|
+
{
|
|
166
|
+
paperSize = 'a4',
|
|
167
|
+
orientation = 'portrait',
|
|
168
|
+
marginMm = 8,
|
|
169
|
+
stylesheetsOnly = false,
|
|
170
|
+
image = 'png',
|
|
171
|
+
}: {
|
|
172
|
+
paperSize?: PaperSize;
|
|
173
|
+
orientation?: PageOrientation;
|
|
174
|
+
marginMm?: number;
|
|
175
|
+
stylesheetsOnly?: boolean;
|
|
176
|
+
image?: 'png' | 'jpeg';
|
|
177
|
+
} = {},
|
|
178
|
+
): Promise<void> {
|
|
179
|
+
if (!sheets.length) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const [{ toCanvas, getFontEmbedCSS }, { jsPDF }] = await Promise.all([import('html-to-image'), import('jspdf')]);
|
|
183
|
+
|
|
184
|
+
/* Rules and web fonts are gathered once for the whole file, not per page */
|
|
185
|
+
const css = stylesheetsOnly ? sheetStyles(sheets) : '';
|
|
186
|
+
const fontEmbedCSS = stylesheetsOnly ? '' : await getFontEmbedCSS(sheets[0]);
|
|
187
|
+
|
|
188
|
+
const pixelRatio = image === 'jpeg' ? JPEG_PIXEL_RATIO : PIXEL_RATIO;
|
|
189
|
+
const portrait = PAPER_MM[paperSize];
|
|
190
|
+
const paper = orientation === 'portrait' ? portrait : { width: portrait.height, height: portrait.width };
|
|
191
|
+
const maxWidth = paper.width - 2 * marginMm;
|
|
192
|
+
const maxHeight = paper.height - 2 * marginMm;
|
|
193
|
+
const pdf = new jsPDF({ orientation, unit: 'mm', format: paperSize, compress: true });
|
|
194
|
+
|
|
195
|
+
for (const [index, sheet] of sheets.entries()) {
|
|
196
|
+
const canvas = stylesheetsOnly
|
|
197
|
+
? await captureWithStylesheets(sheet, css, pixelRatio)
|
|
198
|
+
: await captureWithComputedStyles(sheet, toCanvas, fontEmbedCSS, pixelRatio);
|
|
199
|
+
const aspect = canvas.width / canvas.height;
|
|
200
|
+
const width = Math.min(maxWidth, maxHeight * aspect);
|
|
201
|
+
const height = width / aspect;
|
|
202
|
+
|
|
203
|
+
if (index > 0) {
|
|
204
|
+
pdf.addPage(paperSize, orientation);
|
|
205
|
+
}
|
|
206
|
+
const pageImage = image === 'jpeg' ? canvas.toDataURL('image/jpeg', JPEG_QUALITY) : canvas;
|
|
207
|
+
pdf.addImage(
|
|
208
|
+
pageImage,
|
|
209
|
+
image === 'jpeg' ? 'JPEG' : 'PNG',
|
|
210
|
+
(paper.width - width) / 2,
|
|
211
|
+
marginMm,
|
|
212
|
+
width,
|
|
213
|
+
height,
|
|
214
|
+
undefined,
|
|
215
|
+
'FAST',
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
pdf.save(fileName);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Saves an A4 sheet as a one-page A4 portrait PDF file */
|
|
223
|
+
export function downloadSheetPdf(sheet: HTMLElement, fileName: string, marginMm = 8): Promise<void> {
|
|
224
|
+
return downloadSheetsPdf([sheet], fileName, { marginMm });
|
|
225
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { usePageScale } from './use-page-scale';
|
|
4
|
+
import styles from './report-page.scss';
|
|
5
|
+
|
|
6
|
+
interface ReportPageProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* An A4 sheet that fills the width it is given.
|
|
12
|
+
*
|
|
13
|
+
* Reports that mirror a paper return are drawn at true A4 so they print 1:1,
|
|
14
|
+
* but a fixed page wastes most of a wide monitor. The sheet is scaled up to
|
|
15
|
+
* fill its container and floored at A4, below which the viewport scrolls
|
|
16
|
+
* instead -- shrinking a dense return past its paper size makes it unreadable.
|
|
17
|
+
*/
|
|
18
|
+
const ReportPage: React.FC<ReportPageProps> = ({ children }) => {
|
|
19
|
+
const { ref, scale } = usePageScale<HTMLDivElement>();
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<div className={styles.viewport} ref={ref}>
|
|
23
|
+
{/* a custom property rather than an inline `zoom`, so the print
|
|
24
|
+
stylesheet can still reset it to 1 */}
|
|
25
|
+
<div className={styles.sheet} style={{ '--page-zoom': scale } as React.CSSProperties}>
|
|
26
|
+
{children}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export default ReportPage;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
2
|
+
|
|
3
|
+
/* A4 portrait, in the units the paper is measured in, so the sheet occupies
|
|
4
|
+
exactly one page on screen and prints at 1:1. */
|
|
5
|
+
|
|
6
|
+
$page-w: 210mm;
|
|
7
|
+
$page-h: 297mm;
|
|
8
|
+
$page-margin: 10mm;
|
|
9
|
+
|
|
10
|
+
.viewport {
|
|
11
|
+
overflow-x: auto;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.sheet {
|
|
15
|
+
box-sizing: border-box;
|
|
16
|
+
width: $page-w;
|
|
17
|
+
min-height: $page-h;
|
|
18
|
+
padding: $page-margin;
|
|
19
|
+
margin: 0 auto;
|
|
20
|
+
background-color: #fff;
|
|
21
|
+
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.18);
|
|
22
|
+
/* set by `usePageScale` from the measured container width, floored at 1 */
|
|
23
|
+
zoom: var(--page-zoom, 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
@media print {
|
|
27
|
+
@page {
|
|
28
|
+
size: A4 portrait;
|
|
29
|
+
margin: $page-margin;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.viewport {
|
|
33
|
+
overflow: visible;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.sheet {
|
|
37
|
+
width: auto;
|
|
38
|
+
min-height: 0;
|
|
39
|
+
padding: 0;
|
|
40
|
+
margin: 0;
|
|
41
|
+
box-shadow: none;
|
|
42
|
+
/* paper geometry stands on its own; magnification is for screens */
|
|
43
|
+
zoom: 1;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
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;
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Scales the sheet to fill whatever width it is given, never going below A4.
|
|
11
|
+
*
|
|
12
|
+
* The report is drawn at true A4 so it prints at 1:1, but a fixed page wastes
|
|
13
|
+
* most of a wide monitor and is hard to read. This measures the containing
|
|
14
|
+
* element and returns the factor that makes the page fill it, floored at 1 so
|
|
15
|
+
* the sheet is never rendered smaller than the paper it represents -- below
|
|
16
|
+
* that width the container scrolls instead, which keeps the form legible rather
|
|
17
|
+
* than shrinking it into illegibility.
|
|
18
|
+
*
|
|
19
|
+
* A callback ref rather than `useRef` + `useEffect`, because the page it
|
|
20
|
+
* measures only mounts once a report has been generated: a mount effect would
|
|
21
|
+
* run while the placeholder is still showing, find nothing to measure, and
|
|
22
|
+
* never run again -- leaving the generated sheet stuck at A4.
|
|
23
|
+
*
|
|
24
|
+
* Returned as a factor for `zoom`, which scales the layout box as well as the
|
|
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).
|
|
30
|
+
*/
|
|
31
|
+
export function usePageScale<T extends HTMLElement>(pageWidthMm = A4_WIDTH_MM) {
|
|
32
|
+
const [scale, setScale] = useState(1);
|
|
33
|
+
const observer = useRef<ResizeObserver | null>(null);
|
|
34
|
+
|
|
35
|
+
const ref = useCallback(
|
|
36
|
+
(node: T | null) => {
|
|
37
|
+
observer.current?.disconnect();
|
|
38
|
+
|
|
39
|
+
if (!node) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
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
|
+
};
|
|
48
|
+
|
|
49
|
+
measure();
|
|
50
|
+
|
|
51
|
+
observer.current = new ResizeObserver(measure);
|
|
52
|
+
observer.current.observe(node);
|
|
53
|
+
},
|
|
54
|
+
[pageWidthMm],
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
useEffect(() => () => observer.current?.disconnect(), []);
|
|
58
|
+
|
|
59
|
+
return { ref, scale };
|
|
60
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a report has been generated and came back with something to show.
|
|
3
|
+
*
|
|
4
|
+
* Reports flatten their response with `Object.assign({}, ...data.result)`, but
|
|
5
|
+
* they initialise that state variously as `{}`, `[]` or a value restored from
|
|
6
|
+
* sessionStorage, so "has it run yet" is really "does it have any keys". An
|
|
7
|
+
* empty array has none, which is what makes this work across all of them.
|
|
8
|
+
*/
|
|
9
|
+
export function hasReportData(data: unknown): boolean {
|
|
10
|
+
if (!data || typeof data !== 'object') {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
return Object.keys(data).length > 0;
|
|
15
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Layer } from '@carbon/react';
|
|
3
|
+
import { Location, Report } from '@carbon/react/icons';
|
|
4
|
+
import { useSession } from '@openmrs/esm-framework';
|
|
5
|
+
|
|
6
|
+
import styles from './report-placeholder.scss';
|
|
7
|
+
|
|
8
|
+
interface ReportPlaceholderProps {
|
|
9
|
+
/** Named so the guidance says which return is about to be produced */
|
|
10
|
+
reportName?: string;
|
|
11
|
+
/** Shown when a report was generated but came back with nothing in it */
|
|
12
|
+
isEmptyResult?: boolean;
|
|
13
|
+
/** A report was generated, but for another location than the current one */
|
|
14
|
+
isLocationChanged?: boolean;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stands in for the report template until a report has actually been generated,
|
|
19
|
+
* and explains how to produce one.
|
|
20
|
+
*
|
|
21
|
+
* Report templates are large pre-printed MOH forms; rendering one with every
|
|
22
|
+
* value blank reads as "the facility reported zeroes" rather than "you have not
|
|
23
|
+
* run this yet", so the template is held back until there is data to put in it.
|
|
24
|
+
* That leaves the page empty, hence the guidance here.
|
|
25
|
+
*/
|
|
26
|
+
const ReportPlaceholder: React.FC<ReportPlaceholderProps> = ({
|
|
27
|
+
reportName,
|
|
28
|
+
isEmptyResult = false,
|
|
29
|
+
isLocationChanged = false,
|
|
30
|
+
}) => {
|
|
31
|
+
const session = useSession();
|
|
32
|
+
const location = session?.sessionLocation?.display;
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<Layer className={styles.placeholder}>
|
|
36
|
+
{isLocationChanged ? (
|
|
37
|
+
<Location size={32} className={styles.icon} />
|
|
38
|
+
) : (
|
|
39
|
+
<Report size={32} className={styles.icon} />
|
|
40
|
+
)}
|
|
41
|
+
|
|
42
|
+
{isLocationChanged ? (
|
|
43
|
+
<>
|
|
44
|
+
<h5>Location changed</h5>
|
|
45
|
+
<p>
|
|
46
|
+
The last {reportName ?? 'report'} was generated for a different location, so it is not shown here. Select{' '}
|
|
47
|
+
<strong>Generate report</strong> to produce it for{' '}
|
|
48
|
+
{location ? <strong>{location}</strong> : 'this location'}.
|
|
49
|
+
</p>
|
|
50
|
+
</>
|
|
51
|
+
) : isEmptyResult ? (
|
|
52
|
+
<>
|
|
53
|
+
<h5>No records for this period</h5>
|
|
54
|
+
<p>
|
|
55
|
+
{reportName ?? 'The report'} ran successfully but returned no data for the period you selected. Try a
|
|
56
|
+
different reporting period, or confirm that data has been entered for this facility.
|
|
57
|
+
</p>
|
|
58
|
+
</>
|
|
59
|
+
) : (
|
|
60
|
+
<>
|
|
61
|
+
<h5>No report generated yet</h5>
|
|
62
|
+
<p>
|
|
63
|
+
{reportName ? `The ${reportName} form` : 'The report'} will appear here, filled in with your
|
|
64
|
+
facility’s figures, once you generate it.
|
|
65
|
+
</p>
|
|
66
|
+
<ol className={styles.steps}>
|
|
67
|
+
<li>Choose the reporting period in the filters above.</li>
|
|
68
|
+
<li>
|
|
69
|
+
Select <strong>Generate report</strong>.
|
|
70
|
+
</li>
|
|
71
|
+
<li>
|
|
72
|
+
Use <strong>Download PDF</strong> to save or print the completed form.
|
|
73
|
+
</li>
|
|
74
|
+
</ol>
|
|
75
|
+
</>
|
|
76
|
+
)}
|
|
77
|
+
</Layer>
|
|
78
|
+
);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export default ReportPlaceholder;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@carbon/type';
|
|
3
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
4
|
+
|
|
5
|
+
.placeholder {
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
align-items: center;
|
|
9
|
+
gap: layout.$spacing-03;
|
|
10
|
+
padding: layout.$spacing-07 layout.$spacing-05;
|
|
11
|
+
/* the same gap below the filter card as a generated report's panel has */
|
|
12
|
+
margin-top: layout.$spacing-05;
|
|
13
|
+
margin-bottom: layout.$spacing-05;
|
|
14
|
+
text-align: center;
|
|
15
|
+
background-color: $ui-02;
|
|
16
|
+
border: 1px solid $ui-03;
|
|
17
|
+
|
|
18
|
+
h5 {
|
|
19
|
+
@include type.type-style('productive-heading-02');
|
|
20
|
+
color: $color-gray-100;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
p {
|
|
24
|
+
@include type.type-style('body-long-01');
|
|
25
|
+
color: $text-02;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.icon {
|
|
30
|
+
fill: $ui-04;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/* The steps that produce a report. Left aligned inside the centred card: a
|
|
34
|
+
numbered sequence is read down its left edge, not from a ragged centre. */
|
|
35
|
+
|
|
36
|
+
.steps {
|
|
37
|
+
max-width: 28rem;
|
|
38
|
+
margin-top: layout.$spacing-03;
|
|
39
|
+
padding-left: layout.$spacing-06;
|
|
40
|
+
text-align: left;
|
|
41
|
+
list-style: decimal;
|
|
42
|
+
|
|
43
|
+
li {
|
|
44
|
+
@include type.type-style('body-long-01');
|
|
45
|
+
color: $text-02;
|
|
46
|
+
margin-bottom: layout.$spacing-02;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** One exported column: the heading it carries on the register, and its value */
|
|
2
|
+
export interface RegisterColumn {
|
|
3
|
+
header: string;
|
|
4
|
+
value: (row: Record<string, any>, index: number) => unknown;
|
|
5
|
+
/**
|
|
6
|
+
* Keep the cell as text in the CSV so Excel never reinterprets it:
|
|
7
|
+
* identifiers lose leading zeros and phone numbers become `#NAME?`
|
|
8
|
+
* formulas when the spreadsheet parses them as numbers.
|
|
9
|
+
*/
|
|
10
|
+
text?: boolean;
|
|
11
|
+
/**
|
|
12
|
+
* Allow the cell to wrap on screen. Registers keep their columns
|
|
13
|
+
* `nowrap` so short cells stay compact; prose columns (diagnoses,
|
|
14
|
+
* referrals, comments) would otherwise stretch the whole row wide.
|
|
15
|
+
*/
|
|
16
|
+
wrap?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** The age bands the registers tabulate against, in the order they appear */
|
|
20
|
+
export const AGE_BANDS = [
|
|
21
|
+
{ label: '< 1 yrs', min: 0, max: 0 },
|
|
22
|
+
{ label: '1-4 yrs', min: 1, max: 4 },
|
|
23
|
+
{ label: '5-9 yrs', min: 5, max: 9 },
|
|
24
|
+
{ label: '10-14 yrs', min: 10, max: 14 },
|
|
25
|
+
{ label: '15-19 yrs', min: 15, max: 19 },
|
|
26
|
+
{ label: '20-24 yrs', min: 20, max: 24 },
|
|
27
|
+
{ label: '25+ yrs', min: 25, max: Number.POSITIVE_INFINITY },
|
|
28
|
+
] as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Expands one indicator into the fourteen age-band by gender columns the
|
|
32
|
+
* register prints for it.
|
|
33
|
+
*
|
|
34
|
+
* The registers spell these out as fourteen near-identical cells; describing
|
|
35
|
+
* the rule once keeps the export in step with the table without restating it
|
|
36
|
+
* fourteen times per block.
|
|
37
|
+
*/
|
|
38
|
+
export function ageBandColumns(heading: string, value: (row: Record<string, any>) => unknown): RegisterColumn[] {
|
|
39
|
+
return AGE_BANDS.flatMap((band) =>
|
|
40
|
+
(['M', 'F'] as const).map((gender) => ({
|
|
41
|
+
header: `${heading} ${band.label} (${gender})`,
|
|
42
|
+
value: (row: Record<string, any>) =>
|
|
43
|
+
row.age >= band.min && row.age <= band.max && row.gender === gender ? value(row) : '',
|
|
44
|
+
})),
|
|
45
|
+
);
|
|
46
|
+
}
|