@ampath/esm-reports-app 1.0.0-next.8 → 1.0.0-next.81
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/549.css +1 -0
- package/dist/549.css.map +1 -0
- package/dist/549.js +1 -0
- package/dist/549.js.map +1 -0
- 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 +1339 -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.scss +20 -0
- package/src/reports/moh-740/registers/moh-222-daily-register/moh-222-daily-register.tsx +183 -0
- package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.scss +6 -0
- package/src/reports/moh-740/registers/moh-270-permanent-register/moh-270-permanent-register.tsx +151 -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.scss +6 -0
- package/src/reports/moh-745/registers/moh-412-register.component.tsx +275 -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,116 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { TB_LAB_COLUMNS, type TbLabColumn, type TbLabRow, text } from './tb-lab-register.columns';
|
|
4
|
+
import styles from './tb-lab-register-table.scss';
|
|
5
|
+
|
|
6
|
+
interface TbLabRegisterTableProps {
|
|
7
|
+
rows: TbLabRow[];
|
|
8
|
+
facility?: string;
|
|
9
|
+
startDate?: string;
|
|
10
|
+
endDate?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface HeaderCell {
|
|
14
|
+
/** A two-tier group spanning its member columns; undefined for a tall column */
|
|
15
|
+
group?: string;
|
|
16
|
+
/** The column's own label, repeated once per member under a group */
|
|
17
|
+
label: string;
|
|
18
|
+
/** Columns the header spans */
|
|
19
|
+
span: number;
|
|
20
|
+
/** 2 for a column that stands alone across both header rows */
|
|
21
|
+
height: number;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* The printed template's two-tier header, rebuilt from the column spec:
|
|
26
|
+
* consecutive members of a group sit under one spanning group cell, standalone
|
|
27
|
+
* columns stand two rows tall.
|
|
28
|
+
*/
|
|
29
|
+
export function headerCellsFor(columns: TbLabColumn[]): HeaderCell[][] {
|
|
30
|
+
const top: HeaderCell[] = [];
|
|
31
|
+
const bottom: HeaderCell[] = [];
|
|
32
|
+
|
|
33
|
+
columns.forEach((column, index) => {
|
|
34
|
+
if (!column.group) {
|
|
35
|
+
top.push({ group: undefined, label: column.header, span: 1, height: 2 });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const previous = columns[index - 1];
|
|
39
|
+
if (previous?.group === column.group) {
|
|
40
|
+
top[top.length - 1].span += 1;
|
|
41
|
+
} else {
|
|
42
|
+
top.push({ group: column.group, label: column.group, span: 1, height: 1 });
|
|
43
|
+
}
|
|
44
|
+
bottom.push({ group: undefined, label: column.header, span: 1, height: 1 });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return [top, bottom];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* The TB Laboratory Register as the printed template lays it out: one row per
|
|
52
|
+
* specimen tested, the two-tier header over the appearance and microscopy
|
|
53
|
+
* columns, drawn as a landscape sheet that scrolls sideways in its frame.
|
|
54
|
+
*/
|
|
55
|
+
const TbLabRegisterTable: React.FC<TbLabRegisterTableProps> = ({ rows, facility = '', startDate, endDate }) => {
|
|
56
|
+
const [topRow, bottomRow] = headerCellsFor(TB_LAB_COLUMNS);
|
|
57
|
+
const period = [startDate, endDate]
|
|
58
|
+
.filter(Boolean)
|
|
59
|
+
.map((value) => asDay(value))
|
|
60
|
+
.join(' to ');
|
|
61
|
+
|
|
62
|
+
return (
|
|
63
|
+
<div className={styles.sheet}>
|
|
64
|
+
<div className={styles.runningHead}>
|
|
65
|
+
<span>Ministry of Health</span>
|
|
66
|
+
<span>TB LABORATORY REGISTER</span>
|
|
67
|
+
<span>{facility}</span>
|
|
68
|
+
</div>
|
|
69
|
+
{period && <div className={styles.period}>{period}</div>}
|
|
70
|
+
|
|
71
|
+
<div className={styles.tableScroll}>
|
|
72
|
+
<table className={styles.register}>
|
|
73
|
+
<thead>
|
|
74
|
+
<tr>
|
|
75
|
+
{topRow.map((cell, index) =>
|
|
76
|
+
cell.group ? (
|
|
77
|
+
<th key={index} colSpan={cell.span}>
|
|
78
|
+
{cell.label}
|
|
79
|
+
</th>
|
|
80
|
+
) : (
|
|
81
|
+
<th key={index} rowSpan={2} className={styles.tall}>
|
|
82
|
+
{cell.label}
|
|
83
|
+
</th>
|
|
84
|
+
),
|
|
85
|
+
)}
|
|
86
|
+
</tr>
|
|
87
|
+
<tr>
|
|
88
|
+
{bottomRow.map((cell, index) => (
|
|
89
|
+
<th key={index}>{cell.label}</th>
|
|
90
|
+
))}
|
|
91
|
+
</tr>
|
|
92
|
+
</thead>
|
|
93
|
+
<tbody>
|
|
94
|
+
{rows.map((row, index) => (
|
|
95
|
+
<tr key={text(row.lab_no) || index}>
|
|
96
|
+
{TB_LAB_COLUMNS.map((column, cellIndex) => (
|
|
97
|
+
<td key={cellIndex} className={column.align === 'left' ? styles.left : undefined}>
|
|
98
|
+
{text(column.value(row, index))}
|
|
99
|
+
</td>
|
|
100
|
+
))}
|
|
101
|
+
</tr>
|
|
102
|
+
))}
|
|
103
|
+
</tbody>
|
|
104
|
+
</table>
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
);
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Renders a filter-bar period (YYYY-MM-DD) the way the register reads it */
|
|
111
|
+
function asDay(value: string): string {
|
|
112
|
+
const parts = value.match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
|
113
|
+
return parts ? `${parts[3]}/${parts[2]}/${parts[1]}` : value;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export default TbLabRegisterTable;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
@use '@carbon/layout';
|
|
2
|
+
@use '@openmrs/esm-styleguide/src/vars' as *;
|
|
3
|
+
|
|
4
|
+
/* The TB Laboratory Register sheet, drawn as on the printed template: black
|
|
5
|
+
rules, small type, a two-tier header over the appearance and microscopy
|
|
6
|
+
columns. It is a wide landscape sheet, so it scrolls sideways inside its
|
|
7
|
+
frame rather than widening the page. */
|
|
8
|
+
|
|
9
|
+
$ink: #000;
|
|
10
|
+
$rule: 1px solid $ink;
|
|
11
|
+
$text: 0.6875rem; // 11px body
|
|
12
|
+
$small: 0.625rem; // 10px codes
|
|
13
|
+
|
|
14
|
+
.sheet {
|
|
15
|
+
margin-top: layout.$spacing-05;
|
|
16
|
+
padding: layout.$spacing-05;
|
|
17
|
+
color: $ink;
|
|
18
|
+
font-size: $text;
|
|
19
|
+
line-height: 1.25;
|
|
20
|
+
background-color: $ui-02;
|
|
21
|
+
border: 1px solid $ui-03;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
.runningHead {
|
|
25
|
+
display: grid;
|
|
26
|
+
grid-template-columns: 1fr auto 1fr;
|
|
27
|
+
gap: layout.$spacing-05;
|
|
28
|
+
font-size: $small;
|
|
29
|
+
font-weight: 700;
|
|
30
|
+
|
|
31
|
+
span:nth-child(2) {
|
|
32
|
+
text-align: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
span:last-child {
|
|
36
|
+
text-align: right;
|
|
37
|
+
font-weight: 400;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.period {
|
|
42
|
+
margin-top: layout.$spacing-02;
|
|
43
|
+
font-size: $small;
|
|
44
|
+
text-align: right;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.tableScroll {
|
|
48
|
+
margin-top: layout.$spacing-03;
|
|
49
|
+
overflow-x: auto;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.register {
|
|
53
|
+
width: 100%;
|
|
54
|
+
min-width: 105rem;
|
|
55
|
+
border-collapse: collapse;
|
|
56
|
+
|
|
57
|
+
th,
|
|
58
|
+
td {
|
|
59
|
+
padding: layout.$spacing-02;
|
|
60
|
+
text-align: center;
|
|
61
|
+
vertical-align: middle;
|
|
62
|
+
border: $rule;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
th {
|
|
66
|
+
font-weight: 400;
|
|
67
|
+
vertical-align: top;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/* a group header stays centred over its member columns */
|
|
71
|
+
thead tr:first-child th[colspan] {
|
|
72
|
+
font-weight: 700;
|
|
73
|
+
text-align: center;
|
|
74
|
+
vertical-align: middle;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
td {
|
|
78
|
+
height: 1.125rem;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* a column that stands alone across both header rows reads from the middle */
|
|
83
|
+
.tall {
|
|
84
|
+
vertical-align: middle !important;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.left {
|
|
88
|
+
text-align: left !important;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/* Page furniture around the sheet */
|
|
92
|
+
|
|
93
|
+
.toolbar {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
justify-content: flex-end;
|
|
97
|
+
gap: layout.$spacing-05;
|
|
98
|
+
margin-top: layout.$spacing-05;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.total {
|
|
102
|
+
margin-right: auto;
|
|
103
|
+
font-size: 0.8125rem;
|
|
104
|
+
color: $text-02;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.pager {
|
|
108
|
+
margin-top: layout.$spacing-04;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.empty {
|
|
112
|
+
display: flex;
|
|
113
|
+
flex-direction: column;
|
|
114
|
+
align-items: center;
|
|
115
|
+
gap: layout.$spacing-03;
|
|
116
|
+
margin-top: layout.$spacing-06;
|
|
117
|
+
padding: layout.$spacing-08 layout.$spacing-05;
|
|
118
|
+
text-align: center;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.emptyIcon {
|
|
122
|
+
fill: $text-03;
|
|
123
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
|
|
3
|
+
import { type RegisterColumn } from '../../common/report-register';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* One row of the TB Laboratory Register: a lab-706 patient-list payload row for
|
|
7
|
+
* a TB test, fetched with the `total_tb_smear` indicator -- the one TB row
|
|
8
|
+
* filter the existing dataset carries (MOH 706 5.29). The columns the dataset
|
|
9
|
+
* does not carry yet (time in/out, ID/NEMIS, patient type, HIV status, TB-LAMP,
|
|
10
|
+
* LF-LAM, GeneXpert/Truenat) read blank until the ETL layer grows them.
|
|
11
|
+
*/
|
|
12
|
+
export type TbLabRow = Record<string, any>;
|
|
13
|
+
|
|
14
|
+
/** Blank rather than "null" / "undefined" in a cell */
|
|
15
|
+
export const text = (value: unknown) => (value === null || value === undefined ? '' : String(value).trim());
|
|
16
|
+
|
|
17
|
+
/** DD/MM/YYYY, as the register prints it; values already in that shape pass through */
|
|
18
|
+
export function asRegisterDate(value: unknown): string {
|
|
19
|
+
const raw = text(value);
|
|
20
|
+
if (!raw) {
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
return /^\d{4}-\d{2}-\d{2}/.test(raw) ? dayjs(raw.slice(0, 10)).format('DD/MM/YYYY') : raw;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The date part of either an ISO datetime or an already-formatted DD/MM/YYYY, as YYYY-MM-DD */
|
|
27
|
+
const toIsoDate = (value: unknown): string => {
|
|
28
|
+
const raw = text(value);
|
|
29
|
+
if (/^\d{4}-\d{2}-\d{2}/.test(raw)) {
|
|
30
|
+
return raw.slice(0, 10);
|
|
31
|
+
}
|
|
32
|
+
const parts = raw.match(/^(\d{2})\/(\d{2})\/(\d{4})/);
|
|
33
|
+
return parts ? `${parts[3]}-${parts[2]}-${parts[1]}` : '';
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/** Turnaround in whole calendar days, from sample collected to results dispatched */
|
|
37
|
+
export function tatDays(row: TbLabRow): string {
|
|
38
|
+
const from = toIsoDate(row.test_datetime);
|
|
39
|
+
const to = toIsoDate(row.date_results_dispatched);
|
|
40
|
+
if (!from || !to) {
|
|
41
|
+
return '';
|
|
42
|
+
}
|
|
43
|
+
const days = dayjs(to).diff(dayjs(from), 'day');
|
|
44
|
+
return days >= 0 ? String(days) : '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Which sputum the row's sample is. The dataset does not carry the sample's
|
|
49
|
+
* timing yet, so it is read off the specimen label; rows that do not say print
|
|
50
|
+
* under Spot rather than being dropped, until the ETL carries the timing.
|
|
51
|
+
*/
|
|
52
|
+
export function timingOf(row: TbLabRow): 'spot' | 'morning' {
|
|
53
|
+
return /morning/i.test(text(row.type_of_specimen)) ? 'morning' : 'spot';
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The stain the microscopy used, as the template's Type ZN/FM column reads it */
|
|
57
|
+
export function stainType(row: TbLabRow): string {
|
|
58
|
+
const investigation = text(row.investigation_required).toLowerCase();
|
|
59
|
+
if (/fluor|\bfm\b/.test(investigation)) {
|
|
60
|
+
return 'FM';
|
|
61
|
+
}
|
|
62
|
+
if (/ziehl|\bzn\b/.test(investigation)) {
|
|
63
|
+
return 'ZN';
|
|
64
|
+
}
|
|
65
|
+
return '';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Which of the register's result columns the row's test result belongs in */
|
|
69
|
+
function resultKindOf(row: TbLabRow): 'microscopy' | 'tb_lamp' | 'lf_lam' | 'genexpert' {
|
|
70
|
+
const investigation = text(row.investigation_required).toLowerCase();
|
|
71
|
+
if (/lamp/.test(investigation)) {
|
|
72
|
+
return 'tb_lamp';
|
|
73
|
+
}
|
|
74
|
+
if (/\blam\b|lf.?lam|lateral flow/.test(investigation)) {
|
|
75
|
+
return 'lf_lam';
|
|
76
|
+
}
|
|
77
|
+
if (/xpert|genexp|truenat|mtb|rif/.test(investigation)) {
|
|
78
|
+
return 'genexpert';
|
|
79
|
+
}
|
|
80
|
+
return 'microscopy';
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** The row's result, placed in its own column: blank in the other result columns */
|
|
84
|
+
const resultIn = (kind: 'microscopy' | 'tb_lamp' | 'lf_lam' | 'genexpert') => (row: TbLabRow) =>
|
|
85
|
+
resultKindOf(row) === kind ? text(row.results) : '';
|
|
86
|
+
|
|
87
|
+
/** The sample appearance, under the sub-column its timing names */
|
|
88
|
+
const appearanceIn = (timing: 'spot' | 'morning') => (row: TbLabRow) =>
|
|
89
|
+
timingOf(row) === timing ? text(row.condition_of_specimen) : '';
|
|
90
|
+
|
|
91
|
+
/** The microscopy result, under the sub-column its timing names */
|
|
92
|
+
const microscopyIn = (timing: 'spot' | 'morning') => (row: TbLabRow) =>
|
|
93
|
+
resultKindOf(row) === 'microscopy' && timingOf(row) === timing ? text(row.results) : '';
|
|
94
|
+
|
|
95
|
+
export interface TbLabColumn {
|
|
96
|
+
/** The two-tier header group the column sits under, e.g. `Microscopy result` */
|
|
97
|
+
group?: string;
|
|
98
|
+
/** The column's own label, as the printed template reads it */
|
|
99
|
+
header: string;
|
|
100
|
+
value: (row: TbLabRow, index: number) => unknown;
|
|
101
|
+
/** Identifiers and phones stay text in the CSV */
|
|
102
|
+
text?: boolean;
|
|
103
|
+
/** Prose columns read left-aligned; the rest centre, as on the template */
|
|
104
|
+
align?: 'left';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** The register's columns, in the order the printed template runs them */
|
|
108
|
+
export const TB_LAB_COLUMNS: TbLabColumn[] = [
|
|
109
|
+
{ header: 'Lab S/No.', value: (row, index) => text(row.lab_no) || index + 1 },
|
|
110
|
+
{ header: 'Date sample collected', value: (row) => asRegisterDate(row.test_datetime) },
|
|
111
|
+
{ header: 'Date sample received', value: (row) => asRegisterDate(row.date_sample_received) },
|
|
112
|
+
/* not in the dataset yet: the ETL layer will carry it with the sample's timing */
|
|
113
|
+
{ header: 'Time in', value: () => '' },
|
|
114
|
+
{ header: 'Name (full)', value: (row) => text(row.full_name), align: 'left' },
|
|
115
|
+
{ header: 'TB Reg. No. for follow ups / OPD No.', value: (row) => text(row.opd_ip_ref_no) },
|
|
116
|
+
{ header: 'Sex M/F', value: (row) => text(row.sex) },
|
|
117
|
+
{ header: 'Age (years)', value: (row) => text(row.age) },
|
|
118
|
+
/* not in the dataset yet: national ID / NEMIS / passport */
|
|
119
|
+
{ header: "Patient's ID No./ NEMIS/ Passport", value: () => '' },
|
|
120
|
+
{
|
|
121
|
+
header: "Patient's physical address",
|
|
122
|
+
value: (row) => [text(row.village_estate_landmark), text(row.county_sub_county)].filter(Boolean).join(', '),
|
|
123
|
+
align: 'left',
|
|
124
|
+
},
|
|
125
|
+
{ header: "Patient's/ guardian phone no.", value: (row) => text(row.telephone_number), text: true },
|
|
126
|
+
/* the dataset's referrals carry the from/to facility, which is what the
|
|
127
|
+
template's referring-facility column holds until a department is carried */
|
|
128
|
+
{ header: 'Name of referring facility/ Department', value: (row) => text(row.referrals), align: 'left' },
|
|
129
|
+
/* not in the dataset yet: N / F2/F3/F5/F6 / O */
|
|
130
|
+
{ header: 'Type of patient N, F2/F3/F5/F6, O', value: () => '' },
|
|
131
|
+
/* not in the dataset yet: Pos/Neg/ND/D */
|
|
132
|
+
{ header: 'HIV status Pos/Neg/ND/D', value: () => '' },
|
|
133
|
+
{ group: 'Sample appearance S / M / B', header: 'Spot', value: appearanceIn('spot') },
|
|
134
|
+
{ group: 'Sample appearance S / M / B', header: 'Morning', value: appearanceIn('morning') },
|
|
135
|
+
{ group: 'Microscopy result 0 = No AFB | actual no. | 1+ 2+ 3+', header: 'Type ZN/FM', value: stainType },
|
|
136
|
+
{ group: 'Microscopy result 0 = No AFB | actual no. | 1+ 2+ 3+', header: 'Spot', value: microscopyIn('spot') },
|
|
137
|
+
{
|
|
138
|
+
group: 'Microscopy result 0 = No AFB | actual no. | 1+ 2+ 3+',
|
|
139
|
+
header: 'Morning',
|
|
140
|
+
value: microscopyIn('morning'),
|
|
141
|
+
},
|
|
142
|
+
{ header: 'TB-LAMP', value: resultIn('tb_lamp') },
|
|
143
|
+
{ header: 'LF-LAM', value: resultIn('lf_lam') },
|
|
144
|
+
{ header: 'GeneXpert/ Truenat', value: resultIn('genexpert') },
|
|
145
|
+
{ header: 'Date tested', value: (row) => asRegisterDate(row.date_sample_analysed) },
|
|
146
|
+
{ header: 'Date results dispatched', value: (row) => asRegisterDate(row.date_results_dispatched) },
|
|
147
|
+
/* not in the dataset yet */
|
|
148
|
+
{ header: 'Time out (results dispatch)', value: () => '' },
|
|
149
|
+
{ header: 'TAT (days)', value: tatDays },
|
|
150
|
+
{ header: "Laboratory officer's name", value: (row) => text(row.testing_officer), align: 'left' },
|
|
151
|
+
{ header: 'Remarks (date & place sample referred etc.)', value: (row) => text(row.comments), align: 'left' },
|
|
152
|
+
];
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The register as exportable CSV columns: the two-tier header flattened into
|
|
156
|
+
* one unambiguous label per column, so the file reads without the grouping.
|
|
157
|
+
*/
|
|
158
|
+
export function tbLabCsvColumns(): RegisterColumn[] {
|
|
159
|
+
return TB_LAB_COLUMNS.map((column) => ({
|
|
160
|
+
header: column.group ? `${column.group} - ${column.header}` : column.header,
|
|
161
|
+
value: column.value,
|
|
162
|
+
text: column.text,
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { render, screen, waitFor, within } from '@testing-library/react';
|
|
3
|
+
import userEvent from '@testing-library/user-event';
|
|
4
|
+
import { MemoryRouter } from 'react-router-dom';
|
|
5
|
+
|
|
6
|
+
import TbLabRegister from './tb-lab-register.component';
|
|
7
|
+
import { TB_LAB_COLUMNS, tbLabCsvColumns } from './tb-lab-register.columns';
|
|
8
|
+
import { downloadCsv } from '../../common/report-register';
|
|
9
|
+
import { getMoh706PatientList } from '../../resources/moh-706.resource';
|
|
10
|
+
|
|
11
|
+
jest.mock('../../common/report-register', () => ({
|
|
12
|
+
...jest.requireActual('../../common/report-register'),
|
|
13
|
+
downloadCsv: jest.fn(),
|
|
14
|
+
}));
|
|
15
|
+
|
|
16
|
+
jest.mock('../../resources/moh-706.resource', () => ({
|
|
17
|
+
getMoh706PatientList: jest.fn(),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
jest.mock('@openmrs/esm-framework', () => ({
|
|
21
|
+
useSession: () => ({ sessionLocation: { uuid: 'loc-uuid', display: 'Test Facility' } }),
|
|
22
|
+
openmrsFetch: jest.fn().mockResolvedValue({ ok: false }),
|
|
23
|
+
showSnackbar: jest.fn(),
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
/* The filter bar is Carbon-heavy and its period picking is not what this page
|
|
27
|
+
is about, so it is stood in by a button that generates a fixed period. The
|
|
28
|
+
restore-from-URL hook stays real, as it is part of the page's behaviour. */
|
|
29
|
+
jest.mock('../../common/report-filters', () => {
|
|
30
|
+
const actual = jest.requireActual('../../common/report-filters');
|
|
31
|
+
return {
|
|
32
|
+
...actual,
|
|
33
|
+
ReportFiltersComponent: ({
|
|
34
|
+
onGenerate,
|
|
35
|
+
}: {
|
|
36
|
+
onGenerate: (period: { startDate: string; endDate: string; label?: string }) => void;
|
|
37
|
+
}) => (
|
|
38
|
+
<button onClick={() => onGenerate({ startDate: '2026-10-01', endDate: '2026-10-02', label: 'period' })}>
|
|
39
|
+
generate
|
|
40
|
+
</button>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
/** A TB smear row as the lab-706 patient list returns it: a spot sputum, ZN stain */
|
|
46
|
+
const aRow = (overrides: Record<string, unknown> = {}) => ({
|
|
47
|
+
lab_no: 'L-0007',
|
|
48
|
+
test_datetime: '2026-10-01T09:00:00.000Z',
|
|
49
|
+
date_sample_received: '2026-10-01',
|
|
50
|
+
full_name: 'Jane Akinyi',
|
|
51
|
+
opd_ip_ref_no: 'OPD-12',
|
|
52
|
+
sex: 'F',
|
|
53
|
+
age: 34,
|
|
54
|
+
village_estate_landmark: 'Mama Ngina',
|
|
55
|
+
county_sub_county: 'Kisumu East',
|
|
56
|
+
telephone_number: '0700123456',
|
|
57
|
+
referrals: 'Kisumu County Hospital',
|
|
58
|
+
type_of_specimen: 'Spot',
|
|
59
|
+
condition_of_specimen: 'B',
|
|
60
|
+
investigation_required: 'Ziehl-Neelsen stain',
|
|
61
|
+
results: '1+',
|
|
62
|
+
date_sample_analysed: '2026-10-02',
|
|
63
|
+
date_results_dispatched: '2026-10-03',
|
|
64
|
+
testing_officer: 'J. Otieno',
|
|
65
|
+
comments: '',
|
|
66
|
+
...overrides,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const renderRegister = () =>
|
|
70
|
+
render(
|
|
71
|
+
<MemoryRouter>
|
|
72
|
+
<TbLabRegister />
|
|
73
|
+
</MemoryRouter>,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const page = (results: Array<Record<string, unknown>>, total = results.length) => ({
|
|
77
|
+
results,
|
|
78
|
+
total,
|
|
79
|
+
isTotalExact: true,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
const cellValues = (row: HTMLTableRowElement) =>
|
|
83
|
+
within(row)
|
|
84
|
+
.getAllByRole('cell')
|
|
85
|
+
.map((cell) => cell.textContent);
|
|
86
|
+
|
|
87
|
+
describe('TbLabRegister', () => {
|
|
88
|
+
beforeEach(() => {
|
|
89
|
+
jest.mocked(downloadCsv).mockClear();
|
|
90
|
+
jest.mocked(getMoh706PatientList).mockReset();
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('asks for the register to be generated before showing anything', () => {
|
|
94
|
+
renderRegister();
|
|
95
|
+
|
|
96
|
+
expect(screen.getByText('No report generated yet')).toBeInTheDocument();
|
|
97
|
+
expect(getMoh706PatientList).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('fetches the whole TB workload and lays the template out', async () => {
|
|
101
|
+
jest
|
|
102
|
+
.mocked(getMoh706PatientList)
|
|
103
|
+
.mockResolvedValue(
|
|
104
|
+
page([
|
|
105
|
+
aRow(),
|
|
106
|
+
aRow({
|
|
107
|
+
type_of_specimen: 'Morning',
|
|
108
|
+
investigation_required: 'Fluorescent microscopy',
|
|
109
|
+
results: 'No AFB seen',
|
|
110
|
+
condition_of_specimen: 'S',
|
|
111
|
+
}),
|
|
112
|
+
]),
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
const { container } = renderRegister();
|
|
116
|
+
await userEvent.click(screen.getByRole('button', { name: 'generate' }));
|
|
117
|
+
|
|
118
|
+
await waitFor(() => expect(screen.getByText('2 records')).toBeInTheDocument());
|
|
119
|
+
|
|
120
|
+
/* the register's own row filter: every TB smear of the period */
|
|
121
|
+
expect(getMoh706PatientList).toHaveBeenCalledWith(
|
|
122
|
+
expect.objectContaining({
|
|
123
|
+
locationUuids: 'loc-uuid',
|
|
124
|
+
startDate: '2026-10-01',
|
|
125
|
+
endDate: '2026-10-02',
|
|
126
|
+
indicators: 'total_tb_smear',
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
const table = within(container.querySelector('table') as HTMLTableElement);
|
|
131
|
+
const headerRows = within(table.getAllByRole('rowgroup')[0]).getAllByRole('row');
|
|
132
|
+
expect(headerRows).toHaveLength(2);
|
|
133
|
+
|
|
134
|
+
/* the top tier: the standalone columns, with the two groups sitting
|
|
135
|
+
between the columns they span */
|
|
136
|
+
const tallLabels = TB_LAB_COLUMNS.filter((column) => !column.group).map((column) => column.header);
|
|
137
|
+
const groups = [...new Set(TB_LAB_COLUMNS.filter((column) => column.group).map((column) => column.group!))];
|
|
138
|
+
const topCells = within(headerRows[0])
|
|
139
|
+
.getAllByRole('columnheader')
|
|
140
|
+
.map((cell) => cell.textContent);
|
|
141
|
+
expect(topCells).toEqual([...tallLabels.slice(0, 14), ...groups, ...tallLabels.slice(14)]);
|
|
142
|
+
/* the bottom tier: the five sub-columns under the groups */
|
|
143
|
+
expect(
|
|
144
|
+
within(headerRows[1])
|
|
145
|
+
.getAllByRole('columnheader')
|
|
146
|
+
.map((cell) => cell.textContent),
|
|
147
|
+
).toEqual(['Spot', 'Morning', 'Type ZN/FM', 'Spot', 'Morning']);
|
|
148
|
+
|
|
149
|
+
const bodyRows = within(table.getAllByRole('rowgroup')[1]).getAllByRole('row');
|
|
150
|
+
/* the spot row: appearance and result under Spot, the stain read off the test, two-day TAT */
|
|
151
|
+
expect(cellValues(bodyRows[0] as HTMLTableRowElement)).toEqual([
|
|
152
|
+
'L-0007',
|
|
153
|
+
'01/10/2026',
|
|
154
|
+
'01/10/2026',
|
|
155
|
+
'',
|
|
156
|
+
'Jane Akinyi',
|
|
157
|
+
'OPD-12',
|
|
158
|
+
'F',
|
|
159
|
+
'34',
|
|
160
|
+
'',
|
|
161
|
+
'Mama Ngina, Kisumu East',
|
|
162
|
+
'0700123456',
|
|
163
|
+
'Kisumu County Hospital',
|
|
164
|
+
'',
|
|
165
|
+
'',
|
|
166
|
+
'B',
|
|
167
|
+
'',
|
|
168
|
+
'ZN',
|
|
169
|
+
'1+',
|
|
170
|
+
'',
|
|
171
|
+
'',
|
|
172
|
+
'',
|
|
173
|
+
'',
|
|
174
|
+
'02/10/2026',
|
|
175
|
+
'03/10/2026',
|
|
176
|
+
'',
|
|
177
|
+
'2',
|
|
178
|
+
'J. Otieno',
|
|
179
|
+
'',
|
|
180
|
+
]);
|
|
181
|
+
/* the morning row: appearance and result under Morning, stain FM */
|
|
182
|
+
expect(cellValues(bodyRows[1] as HTMLTableRowElement)[15]).toBe('S');
|
|
183
|
+
expect(cellValues(bodyRows[1] as HTMLTableRowElement)[16]).toBe('FM');
|
|
184
|
+
expect(cellValues(bodyRows[1] as HTMLTableRowElement)[18]).toBe('No AFB seen');
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('exports every page with the two-tier header flattened into one label per column', async () => {
|
|
188
|
+
jest.mocked(getMoh706PatientList).mockResolvedValue(page([aRow()]));
|
|
189
|
+
|
|
190
|
+
renderRegister();
|
|
191
|
+
await userEvent.click(screen.getByRole('button', { name: 'generate' }));
|
|
192
|
+
await waitFor(() => expect(screen.getByRole('button', { name: /Export CSV/ })).toBeInTheDocument());
|
|
193
|
+
|
|
194
|
+
await userEvent.click(screen.getByRole('button', { name: /Export CSV/ }));
|
|
195
|
+
|
|
196
|
+
await waitFor(() => expect(jest.mocked(downloadCsv)).toHaveBeenCalledTimes(1));
|
|
197
|
+
const [csv, filename] = jest.mocked(downloadCsv).mock.calls[0];
|
|
198
|
+
const lines = (csv as string).split('\r\n');
|
|
199
|
+
/* labels holding commas come out quoted, as CSV requires */
|
|
200
|
+
expect(lines[0]).toBe(
|
|
201
|
+
tbLabCsvColumns()
|
|
202
|
+
.map((column) => (column.header.includes(',') ? `"${column.header}"` : column.header))
|
|
203
|
+
.join(','),
|
|
204
|
+
);
|
|
205
|
+
/* phones and lab numbers survive Excel as text */
|
|
206
|
+
expect(lines[1]).toContain('="0700123456"');
|
|
207
|
+
expect(filename).toBe('tb-laboratory-register-2026-10-01-2026-10-02.csv');
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('pagers past the first page when the period holds more rows than one page', async () => {
|
|
211
|
+
jest.mocked(getMoh706PatientList).mockResolvedValue(
|
|
212
|
+
page(
|
|
213
|
+
Array.from({ length: 50 }, () => aRow()),
|
|
214
|
+
120,
|
|
215
|
+
),
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
renderRegister();
|
|
219
|
+
await userEvent.click(screen.getByRole('button', { name: 'generate' }));
|
|
220
|
+
|
|
221
|
+
await waitFor(() => expect(screen.getByText(/120 items/)).toBeInTheDocument());
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('falls back to the placeholder when the register cannot be fetched', async () => {
|
|
225
|
+
jest.mocked(getMoh706PatientList).mockRejectedValue(new Error('etl down'));
|
|
226
|
+
|
|
227
|
+
renderRegister();
|
|
228
|
+
await userEvent.click(screen.getByRole('button', { name: 'generate' }));
|
|
229
|
+
|
|
230
|
+
await waitFor(() => expect(screen.getByText('No report generated yet')).toBeInTheDocument());
|
|
231
|
+
});
|
|
232
|
+
});
|