@ampath/esm-reports-app 1.0.0-next.79 → 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/410.js +1 -1
- package/dist/410.js.map +1 -1
- package/dist/600.js +1 -1
- package/dist/600.js.map +1 -1
- package/dist/757.js +1 -0
- package/dist/757.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 +1 -1
- package/src/common/report-filters/report-filters.scss +15 -6
- package/src/common/report-page/download-sheet-pdf.ts +193 -27
- package/src/common/report-register/report-parents.ts +2 -0
- package/src/dashboard/report-catalog.ts +18 -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 +469 -310
- package/src/registers/moh-362/moh-362-lab-register.scss +412 -0
- package/src/registers/moh-362/moh-362-linkage-register.component.tsx +188 -93
- package/src/registers/moh-362/moh-362-linkage-register.scss +91 -0
- package/src/registers/moh-362/moh-362.component.tsx +33 -15
- package/src/registers/moh-362/moh-362.types.ts +15 -25
- 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 +37 -3
- package/src/registers/register-file-name.ts +18 -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/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/moh-731.component.tsx +10 -7
- package/src/reports/moh-731/moh-731.scss +3 -6
- package/src/reports/moh-731/summary/moh-731-summary.config.ts +2 -4
- package/src/reports/moh-731/summary/moh-731-summary.scss +5 -0
- package/src/resources/moh-267.resource.ts +27 -0
- package/src/resources/moh-729b.resource.ts +25 -0
- package/src/resources/presumptive-tb.resource.ts +27 -0
- package/src/root.component.tsx +14 -0
- package/dist/177.css +0 -1
- package/dist/177.css.map +0 -1
- package/dist/177.js +0 -1
- package/dist/177.js.map +0 -1
- package/dist/580.js +0 -1
- package/dist/580.js.map +0 -1
- package/src/registers/moh-362/moh-362-register.scss +0 -222
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import React, { useRef, useState } from 'react';
|
|
2
|
+
import { useSession } from '@openmrs/esm-framework';
|
|
3
|
+
|
|
4
|
+
import { ReportFiltersComponent, type ReportPeriod, useRestorePeriod } from '../../common/report-filters';
|
|
5
|
+
import { downloadSheetsPdf } from '../../common/report-page';
|
|
6
|
+
import { showReportError } from '../../common/report-error';
|
|
7
|
+
import { ReportSkeleton } from '../../common/report-skeleton';
|
|
8
|
+
import { getPresumptiveTb } from '../../resources/presumptive-tb.resource';
|
|
9
|
+
import { registerFileName } from '../register-file-name';
|
|
10
|
+
import { ReportPlaceholder, useGeneratedLocation } from '../../common/report-placeholder';
|
|
11
|
+
import PresumptiveTbRegister from './presumptive-tb-register.component';
|
|
12
|
+
import { type PresumptiveTbRow } from './presumptive-tb.types';
|
|
13
|
+
import styles from './presumptive-tb-register.scss';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Presumptive TB Register (MOH/REG/DNTLDP/02), opened from the landing page's
|
|
17
|
+
* Registers tab, with the reports' filter bar.
|
|
18
|
+
*
|
|
19
|
+
* The cases are the TB encounters (331) in the period, for the whole facility,
|
|
20
|
+
* already in the register's columns (/presumptive-tb-register).
|
|
21
|
+
*/
|
|
22
|
+
const PresumptiveTbRegisterPage: React.FC = () => {
|
|
23
|
+
const [rows, setRows] = useState<PresumptiveTbRow[] | null>(null);
|
|
24
|
+
const [isLoading, setIsLoading] = useState(false);
|
|
25
|
+
const [period, setPeriod] = useState<{ startDate: string; endDate: string }>({ startDate: '', endDate: '' });
|
|
26
|
+
|
|
27
|
+
const session = useSession();
|
|
28
|
+
const locationUuids = session?.sessionLocation?.uuid ?? '';
|
|
29
|
+
/* Only a register for the current location counts */
|
|
30
|
+
const { isForCurrentLocation, hasLocationChanged, markGenerated } = useGeneratedLocation('presumptiveTbLocation');
|
|
31
|
+
const isGenerated = rows !== null && isForCurrentLocation;
|
|
32
|
+
|
|
33
|
+
const generate = async ({ startDate, endDate }: ReportPeriod) => {
|
|
34
|
+
setIsLoading(true);
|
|
35
|
+
setPeriod({ startDate, endDate });
|
|
36
|
+
try {
|
|
37
|
+
setRows((await getPresumptiveTb({ locationUuids, startDate, endDate })) as PresumptiveTbRow[]);
|
|
38
|
+
markGenerated(locationUuids);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
showReportError('the Presumptive TB register', error);
|
|
41
|
+
} finally {
|
|
42
|
+
setIsLoading(false);
|
|
43
|
+
}
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
useRestorePeriod(generate);
|
|
47
|
+
|
|
48
|
+
/* Download PDF saves every page of the register as an A3 landscape page,
|
|
49
|
+
made from copies, so the pages on screen are left as they are */
|
|
50
|
+
const sheetsRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
const downloadPdf = async () => {
|
|
52
|
+
const pages = Array.from(sheetsRef.current?.querySelectorAll<HTMLElement>('[data-register-page]') ?? []);
|
|
53
|
+
const name = registerFileName('Presumptive-TB-Register', session?.sessionLocation?.display ?? '', period);
|
|
54
|
+
/* styled by its own stylesheets in Arial only, and may run to many pages */
|
|
55
|
+
await downloadSheetsPdf(pages, name, {
|
|
56
|
+
paperSize: 'a3',
|
|
57
|
+
orientation: 'landscape',
|
|
58
|
+
stylesheetsOnly: true,
|
|
59
|
+
image: 'jpeg',
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<>
|
|
65
|
+
<ReportFiltersComponent
|
|
66
|
+
reportName="Presumptive TB Register"
|
|
67
|
+
mode="both"
|
|
68
|
+
onGenerate={generate}
|
|
69
|
+
isLoading={isLoading}
|
|
70
|
+
isReportGenerated={isGenerated}
|
|
71
|
+
generatedPeriod={period}
|
|
72
|
+
defaultTab="custom"
|
|
73
|
+
backLink={{ label: 'All registers', to: '/?tab=registers' }}
|
|
74
|
+
onDownloadPdf={downloadPdf}
|
|
75
|
+
/>
|
|
76
|
+
{isLoading && <ReportSkeleton />}
|
|
77
|
+
{!isLoading && !isGenerated && (
|
|
78
|
+
<ReportPlaceholder
|
|
79
|
+
reportName="Presumptive TB Register"
|
|
80
|
+
isLocationChanged={rows !== null && hasLocationChanged}
|
|
81
|
+
/>
|
|
82
|
+
)}
|
|
83
|
+
{!isLoading && isGenerated && (
|
|
84
|
+
<div className={styles.reportFrame} ref={sheetsRef}>
|
|
85
|
+
<PresumptiveTbRegister rows={rows} />
|
|
86
|
+
</div>
|
|
87
|
+
)}
|
|
88
|
+
</>
|
|
89
|
+
);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export default PresumptiveTbRegisterPage;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* One presumptive TB case in the register, column by column as on the form
|
|
5
|
+
* (MOH/REG/DNTLDP/02, February 2023). Where a column holds two lines, the
|
|
6
|
+
* upper and lower are separate fields.
|
|
7
|
+
*
|
|
8
|
+
* These are the columns the ETL returns (/presumptive-tb-register), one row
|
|
9
|
+
* per TB encounter in the period.
|
|
10
|
+
*/
|
|
11
|
+
export interface PresumptiveTbRow {
|
|
12
|
+
/** (A) cumulative from the start of each year */
|
|
13
|
+
serial_number?: number | string | null;
|
|
14
|
+
/** (B) patient department number, e.g. CCC or OPD number */
|
|
15
|
+
department_number?: string | null;
|
|
16
|
+
/** (C) date registered as a presumed TB/DR-TB case */
|
|
17
|
+
date?: string | null;
|
|
18
|
+
/** (D) first, middle and last names */
|
|
19
|
+
full_name?: string | null;
|
|
20
|
+
/** (E) M, F or other */
|
|
21
|
+
sex?: string | null;
|
|
22
|
+
/** (F) completed years, or months for a child under one, e.g. `35 Yrs` or `8 Mo` */
|
|
23
|
+
age?: string | number | null;
|
|
24
|
+
/** (G) upper: national ID (identifier type 5) */
|
|
25
|
+
national_id?: string | null;
|
|
26
|
+
/** (G) lower: Client Registry number (identifier type 55), in place of UPI */
|
|
27
|
+
cr_number?: string | null;
|
|
28
|
+
/** (H) upper: nearest landmark */
|
|
29
|
+
physical_address?: string | null;
|
|
30
|
+
/** (H) lower */
|
|
31
|
+
phone_number?: string | null;
|
|
32
|
+
/** (I) PS, SR, CI, CP, CHV or ISP */
|
|
33
|
+
referred_by?: string | null;
|
|
34
|
+
/** (J) upper: Positive, Negative or Not Done, in full */
|
|
35
|
+
hiv_result?: string | null;
|
|
36
|
+
/** (J) lower */
|
|
37
|
+
hiv_test_date?: string | null;
|
|
38
|
+
/** (K) type of sample (code) */
|
|
39
|
+
sample_type?: string | null;
|
|
40
|
+
/** (K) date the sputum sample was collected */
|
|
41
|
+
sample_date?: string | null;
|
|
42
|
+
/** (L) upper: N, SG, AO or ND */
|
|
43
|
+
xray_result?: string | null;
|
|
44
|
+
/** (L) lower */
|
|
45
|
+
xray_date?: string | null;
|
|
46
|
+
/** (M) upper: other radiological test(s) and result */
|
|
47
|
+
other_radiology_result?: string | null;
|
|
48
|
+
/** (M) lower */
|
|
49
|
+
other_radiology_date?: string | null;
|
|
50
|
+
/** (N) upper: molecular / culture test(s) and result, e.g. MTB + RS */
|
|
51
|
+
molecular_result?: string | null;
|
|
52
|
+
/** (N) lower */
|
|
53
|
+
molecular_date?: string | null;
|
|
54
|
+
/** (O) upper: microscopy / TB LAMP result */
|
|
55
|
+
microscopy_result?: string | null;
|
|
56
|
+
/** (O) lower */
|
|
57
|
+
microscopy_date?: string | null;
|
|
58
|
+
/** (P) DSTB, DRTB or No TB */
|
|
59
|
+
outcome?: string | null;
|
|
60
|
+
/** (Q) upper: Yes or No */
|
|
61
|
+
started_tb_treatment?: string | null;
|
|
62
|
+
/** (Q) lower: sub-county TB registration number & start date */
|
|
63
|
+
tb_registration?: string | null;
|
|
64
|
+
/** (R) upper: Yes or No */
|
|
65
|
+
tpt_started?: string | null;
|
|
66
|
+
/** (R) lower: TPT sub-county register number & start date */
|
|
67
|
+
tpt_registration?: string | null;
|
|
68
|
+
remarks?: string | null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Blank rather than "null" / "undefined" in a cell */
|
|
72
|
+
export const text = (value: unknown) => (value === null || value === undefined ? '' : String(value));
|
|
73
|
+
|
|
74
|
+
/** DD/MM/YYYY, as the form asks */
|
|
75
|
+
export const formDate = (value: unknown) => (value ? dayjs(String(value)).format('DD/MM/YYYY') : '');
|
|
76
|
+
|
|
77
|
+
/** Diagnosed with TB: drug-sensitive or drug-resistant */
|
|
78
|
+
export const isDiagnosed = (row: PresumptiveTbRow) => ['DSTB', 'DRTB'].includes(text(row.outcome).toUpperCase());
|
|
79
|
+
|
|
80
|
+
/** Investigated for TB: a sample collected or any investigation recorded */
|
|
81
|
+
export const isInvestigated = (row: PresumptiveTbRow) =>
|
|
82
|
+
[row.sample_date, row.xray_result, row.other_radiology_result, row.molecular_result, row.microscopy_result].some(
|
|
83
|
+
(value) => text(value) !== '',
|
|
84
|
+
);
|
|
85
|
+
|
|
86
|
+
/** A diagnosed case started on anti-TB treatment */
|
|
87
|
+
export const isInitiated = (row: PresumptiveTbRow) =>
|
|
88
|
+
isDiagnosed(row) && /^y(es)?$/i.test(text(row.started_tb_treatment));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Activity, Chemistry, Microscope } from '@carbon/react/icons';
|
|
1
|
+
import { Activity, Chemistry, Medication, Microscope, Stethoscope, TaskView } from '@carbon/react/icons';
|
|
2
2
|
|
|
3
3
|
import { type ReportCategory } from '../dashboard/report-catalog';
|
|
4
4
|
|
|
@@ -11,8 +11,8 @@ import { type ReportCategory } from '../dashboard/report-catalog';
|
|
|
11
11
|
export const registerCategories: ReportCategory[] = [
|
|
12
12
|
{
|
|
13
13
|
id: 'hiv-testing',
|
|
14
|
-
title: 'HIV
|
|
15
|
-
description: 'HTS daily registers',
|
|
14
|
+
title: 'HIV Testing Services & Pre-Exposure Prophylaxis',
|
|
15
|
+
description: 'HTS and PrEP daily registers',
|
|
16
16
|
tone: 'violet',
|
|
17
17
|
reports: [
|
|
18
18
|
{
|
|
@@ -23,6 +23,40 @@ export const registerCategories: ReportCategory[] = [
|
|
|
23
23
|
icon: Microscope,
|
|
24
24
|
keywords: ['hts', 'hiv testing', 'lab', 'referral', 'linkage', 'kits'],
|
|
25
25
|
},
|
|
26
|
+
{
|
|
27
|
+
code: 'MOH 267',
|
|
28
|
+
name: 'PrEP daily activity',
|
|
29
|
+
description: 'PrEP visits: clients new, continuing or restarting, their PrEP method, HIV results and STIs.',
|
|
30
|
+
path: '/moh-267',
|
|
31
|
+
icon: Medication,
|
|
32
|
+
keywords: ['prep', 'pre-exposure', 'prophylaxis', 'daily activity', 'dar', 'sti'],
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: 'tb',
|
|
38
|
+
title: 'Tuberculosis',
|
|
39
|
+
description: 'TB case finding registers',
|
|
40
|
+
tone: 'teal',
|
|
41
|
+
reports: [
|
|
42
|
+
{
|
|
43
|
+
code: 'MOH/REG/DNTLDP/02',
|
|
44
|
+
name: 'Presumptive TB',
|
|
45
|
+
description:
|
|
46
|
+
'Presumptive TB cases, their investigations and results, and whether started on TB treatment or TPT.',
|
|
47
|
+
path: '/presumptive-tb',
|
|
48
|
+
icon: Stethoscope,
|
|
49
|
+
keywords: ['tb', 'tuberculosis', 'presumptive', 'acf', 'case finding', 'sputum', 'genexpert', 'tpt'],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: 'TB4',
|
|
53
|
+
name: 'Notification & treatment',
|
|
54
|
+
description:
|
|
55
|
+
'Drug-sensitive TB patients notified: diagnosis, investigations, treatment, follow-up and outcome.',
|
|
56
|
+
path: '/tb4',
|
|
57
|
+
icon: TaskView,
|
|
58
|
+
keywords: ['tb', 'tb4', 'tuberculosis', 'dstb', 'notification', 'treatment', 'outcome', 'smear', 'culture'],
|
|
59
|
+
},
|
|
26
60
|
],
|
|
27
61
|
},
|
|
28
62
|
{
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import dayjs from 'dayjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A register's PDF file name: its code, the facility and the period, e.g.
|
|
5
|
+
* `MOH-267_Kesses_2026-09.pdf` for a whole month, or
|
|
6
|
+
* `MOH-267_Kesses_2026-10-01_to_2026-10-28.pdf` for any other range.
|
|
7
|
+
*/
|
|
8
|
+
export function registerFileName(code: string, facility: string, period: { startDate: string; endDate: string }) {
|
|
9
|
+
const start = dayjs(period.startDate);
|
|
10
|
+
const end = dayjs(period.endDate);
|
|
11
|
+
const isWholeMonth = start.isSame(start.startOf('month'), 'day') && end.isSame(start.endOf('month'), 'day');
|
|
12
|
+
const range = isWholeMonth ? start.format('YYYY-MM') : `${start.format('YYYY-MM-DD')}_to_${end.format('YYYY-MM-DD')}`;
|
|
13
|
+
const name = [code, facility, period.startDate ? range : '']
|
|
14
|
+
.filter(Boolean)
|
|
15
|
+
.join('_')
|
|
16
|
+
.replace(/[^a-z0-9_-]+/gi, '-');
|
|
17
|
+
return `${name}.pdf`;
|
|
18
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
|
|
3
|
+
import { type Tb4Nutrition, type Tb4Row, text } from './tb4.types';
|
|
4
|
+
import styles from './tb4-register.scss';
|
|
5
|
+
|
|
6
|
+
/** Patients ruled on each page, on both facing pages, as on the form */
|
|
7
|
+
export const ROWS_PER_PAGE = 5;
|
|
8
|
+
|
|
9
|
+
/** A3 landscape less its 8mm margins: the page as drawn on screen, inside the report card */
|
|
10
|
+
export const PAGE_WIDTH_MM = 420 - 2 * 8;
|
|
11
|
+
|
|
12
|
+
export const paginate = (rows: Tb4Row[]) => {
|
|
13
|
+
const pages: Tb4Row[][] = [];
|
|
14
|
+
for (let start = 0; start < rows.length; start += ROWS_PER_PAGE) {
|
|
15
|
+
pages.push(rows.slice(start, start + ROWS_PER_PAGE));
|
|
16
|
+
}
|
|
17
|
+
return pages.length ? pages : [[]];
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** A box in a divided cell: a printed label filling it, a label in its corner, or an entry */
|
|
21
|
+
export interface Box {
|
|
22
|
+
heading?: string;
|
|
23
|
+
tag?: string;
|
|
24
|
+
value?: React.ReactNode;
|
|
25
|
+
/** relative width beside the other boxes of its part (default 1) */
|
|
26
|
+
grow?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A part of a divided cell: its share of the row's height and the boxes across it */
|
|
30
|
+
export interface Part {
|
|
31
|
+
share: number;
|
|
32
|
+
boxes?: Box[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A divided cell: parts stacked down the row, as printed */
|
|
36
|
+
export const Stack: React.FC<{ parts: Part[] }> = ({ parts }) => (
|
|
37
|
+
<div className={styles.stack} style={{ gridTemplateRows: parts.map((part) => `${part.share}fr`).join(' ') }}>
|
|
38
|
+
{parts.map((part, index) => (
|
|
39
|
+
<div key={index} className={styles.part}>
|
|
40
|
+
{(part.boxes ?? [{}]).map((box, boxIndex) => (
|
|
41
|
+
<div key={boxIndex} className={styles.box} style={box.grow ? { flexGrow: box.grow } : undefined}>
|
|
42
|
+
{box.heading && <span className={styles.heading}>{box.heading}</span>}
|
|
43
|
+
{box.tag && <span className={styles.tag}>{box.tag}</span>}
|
|
44
|
+
{box.value !== undefined && <span className={styles.boxValue}>{box.value}</span>}
|
|
45
|
+
</div>
|
|
46
|
+
))}
|
|
47
|
+
</div>
|
|
48
|
+
))}
|
|
49
|
+
</div>
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
/** A whole cell's entry */
|
|
53
|
+
export const Value: React.FC<{ children: React.ReactNode; left?: boolean; wrap?: boolean }> = ({
|
|
54
|
+
children,
|
|
55
|
+
left,
|
|
56
|
+
wrap,
|
|
57
|
+
}) => (
|
|
58
|
+
<span className={[styles.value, left && styles.valueLeft, wrap && styles.wrap].filter(Boolean).join(' ')}>
|
|
59
|
+
{children}
|
|
60
|
+
</span>
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Nutritional status, as printed in (K) and (AD): weight beside height, then
|
|
65
|
+
* BMI/Z score, then MUAC
|
|
66
|
+
*/
|
|
67
|
+
export const NutritionStack: React.FC<{ nutrition?: Tb4Nutrition | null }> = ({ nutrition }) => (
|
|
68
|
+
<Stack
|
|
69
|
+
parts={[
|
|
70
|
+
{ share: 0.146, boxes: [{ tag: 'Weight' }, { tag: 'Height' }] },
|
|
71
|
+
{ share: 0.295, boxes: [{ value: text(nutrition?.weight) }, { value: text(nutrition?.height) }] },
|
|
72
|
+
{ share: 0.146, boxes: [{ tag: 'BMI/Z score', value: text(nutrition?.bmi) }] },
|
|
73
|
+
{ share: 0.413, boxes: [{ tag: 'MUAC', value: text(nutrition?.muac) }] },
|
|
74
|
+
]}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* A key beneath the register: its heading across the top, then its rows. A row
|
|
80
|
+
* with fewer cells than the widest spans its last cell across the rest, and
|
|
81
|
+
* the rows listed in `subheadings` are set bold ("CT - Scan", "Comorbidities").
|
|
82
|
+
*/
|
|
83
|
+
export const KeyTable: React.FC<{
|
|
84
|
+
title: string;
|
|
85
|
+
rows: Array<Array<React.ReactNode>>;
|
|
86
|
+
widthMm: number;
|
|
87
|
+
subheadings?: number[];
|
|
88
|
+
leftTitle?: boolean;
|
|
89
|
+
}> = ({ title, rows, widthMm, subheadings = [], leftTitle }) => {
|
|
90
|
+
const columns = Math.max(...rows.map((cells) => cells.length));
|
|
91
|
+
return (
|
|
92
|
+
<table
|
|
93
|
+
className={[styles.keyTable, leftTitle && styles.leftHeadingKey].filter(Boolean).join(' ')}
|
|
94
|
+
style={{ width: `${widthMm}mm` }}
|
|
95
|
+
>
|
|
96
|
+
<thead>
|
|
97
|
+
<tr>
|
|
98
|
+
<th colSpan={columns}>{title}</th>
|
|
99
|
+
</tr>
|
|
100
|
+
</thead>
|
|
101
|
+
<tbody>
|
|
102
|
+
{rows.map((cells, index) => (
|
|
103
|
+
<tr key={index} className={subheadings.includes(index) ? styles.keySubheading : undefined}>
|
|
104
|
+
{cells.map((cell, cellIndex) => (
|
|
105
|
+
<td
|
|
106
|
+
key={cellIndex}
|
|
107
|
+
colSpan={cells.length < columns && cellIndex === cells.length - 1 ? columns - cellIndex : 1}
|
|
108
|
+
>
|
|
109
|
+
{cell}
|
|
110
|
+
</td>
|
|
111
|
+
))}
|
|
112
|
+
</tr>
|
|
113
|
+
))}
|
|
114
|
+
</tbody>
|
|
115
|
+
</table>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
/** Widths, measured off the form, as `<col>` percentages */
|
|
120
|
+
export const Columns: React.FC<{ widths: number[] }> = ({ widths }) => {
|
|
121
|
+
const total = widths.reduce((sum, width) => sum + width, 0);
|
|
122
|
+
return (
|
|
123
|
+
<colgroup>
|
|
124
|
+
{widths.map((width, index) => (
|
|
125
|
+
<col key={index} style={{ width: `${(width / total) * 100}%` }} />
|
|
126
|
+
))}
|
|
127
|
+
</colgroup>
|
|
128
|
+
);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** A column heading: its letter in bold over the printed heading */
|
|
132
|
+
export const Heading: React.FC<{
|
|
133
|
+
letter: string;
|
|
134
|
+
children?: React.ReactNode;
|
|
135
|
+
centre?: boolean;
|
|
136
|
+
rowSpan?: number;
|
|
137
|
+
colSpan?: number;
|
|
138
|
+
}> = ({ letter, children, centre, rowSpan, colSpan }) => (
|
|
139
|
+
<th rowSpan={rowSpan} colSpan={colSpan} className={centre ? styles.centreHeading : undefined}>
|
|
140
|
+
<span className={styles.letter}>({letter})</span>
|
|
141
|
+
{children}
|
|
142
|
+
</th>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
/** One A3 landscape page of the register: the ruled table, the keys beneath, and the page line */
|
|
146
|
+
export const Tb4Page: React.FC<{
|
|
147
|
+
page: number;
|
|
148
|
+
pages: number;
|
|
149
|
+
side: string;
|
|
150
|
+
scale: number;
|
|
151
|
+
table: React.ReactNode;
|
|
152
|
+
keys: React.ReactNode;
|
|
153
|
+
}> = ({ page, pages, side, scale, table, keys }) => (
|
|
154
|
+
<section className={styles.page} data-register-page style={{ '--page-zoom': scale } as React.CSSProperties}>
|
|
155
|
+
{table}
|
|
156
|
+
{keys}
|
|
157
|
+
<div className={styles.pageFoot}>
|
|
158
|
+
<span>
|
|
159
|
+
Page {page} of {pages} · {side}
|
|
160
|
+
</span>
|
|
161
|
+
<span>MOH/REG/DLTLDP/01 · February 2025</span>
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
);
|