@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
|
@@ -1,56 +1,124 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
finalResult,
|
|
7
|
-
formDate,
|
|
8
|
-
inBand,
|
|
9
|
-
testResult,
|
|
10
|
-
text,
|
|
11
|
-
type Moh362Row,
|
|
12
|
-
} from './moh-362.types';
|
|
13
|
-
import styles from './moh-362-register.scss';
|
|
3
|
+
import { usePageScale } from '../../common/report-page';
|
|
4
|
+
import { AGE_BANDS, clientName, formDate, inBand, text, type Moh362Row } from './moh-362.types';
|
|
5
|
+
import styles from './moh-362-lab-register.scss';
|
|
14
6
|
|
|
15
7
|
interface Moh362LabRegisterProps {
|
|
16
8
|
rows: Moh362Row[];
|
|
17
9
|
}
|
|
18
10
|
|
|
11
|
+
/** Rows ruled on the last page, which also holds the summary, as on the paper form */
|
|
12
|
+
const ROWS_ON_LAST_PAGE = 15;
|
|
13
|
+
|
|
14
|
+
/** Rows on every page before it: the summary's space ruled as further rows */
|
|
15
|
+
const ROWS_PER_FULL_PAGE = 23;
|
|
16
|
+
|
|
17
|
+
/** A4 landscape less its 8mm margins: the page as drawn on screen, inside the report card */
|
|
18
|
+
const PAGE_WIDTH_MM = 297 - 2 * 8;
|
|
19
|
+
|
|
19
20
|
const COLUMN_IDS = 'abcdefghijklmnopqr'.split('');
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Column widths, measured off page 5 of the form, as fractions of the sheet --
|
|
24
|
+
* except the three HIV tests, widened from the remarks and provider columns to
|
|
25
|
+
* fit each client's kit, lot and expiry under the result, the ID column, to fit
|
|
26
|
+
* a whole Client Registry ID, and the final result, to fit "Inconclusive".
|
|
27
|
+
*/
|
|
28
|
+
const COLUMN_WIDTHS = [72, 114, 104, 238, 52, 48, 138, 71, 95, 83, 111, 111, 101, 78, 76, 79, 133, 99];
|
|
29
|
+
const COLUMN_TOTAL = COLUMN_WIDTHS.reduce((sum, width) => sum + width, 0);
|
|
30
|
+
|
|
31
|
+
type TestField = 'hiv_test1' | 'hiv_test2' | 'hiv_test3';
|
|
32
|
+
|
|
33
|
+
const TESTS: Array<{
|
|
34
|
+
field: TestField;
|
|
35
|
+
label: string;
|
|
36
|
+
codes: string;
|
|
37
|
+
kit: keyof Moh362Row;
|
|
38
|
+
lot: keyof Moh362Row;
|
|
39
|
+
expiry: keyof Moh362Row;
|
|
40
|
+
}> = [
|
|
41
|
+
{ field: 'hiv_test1', label: 'HIV Test 1', codes: '(N,P,I)', kit: 'kit1_name', lot: 'lot1', expiry: 'expiry1' },
|
|
42
|
+
{ field: 'hiv_test2', label: 'HIV Test 2', codes: '(N,P,I,NA)', kit: 'kit2_name', lot: 'lot2', expiry: 'expiry2' },
|
|
43
|
+
{ field: 'hiv_test3', label: 'HIV Test 3', codes: '(N,P,I,NA)', kit: 'kit3_name', lot: 'lot3', expiry: 'expiry3' },
|
|
44
|
+
];
|
|
45
|
+
|
|
21
46
|
const count = (rows: Moh362Row[], test: (row: Moh362Row) => boolean) => rows.filter(test).length;
|
|
22
47
|
|
|
23
|
-
/**
|
|
24
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Full pages of rows, then a last page of at most 15 rows that carries the
|
|
50
|
+
* summary. A full page never takes every remaining row, so the last page always
|
|
51
|
+
* has at least one client on it.
|
|
52
|
+
*/
|
|
53
|
+
const paginate = (rows: Moh362Row[]) => {
|
|
54
|
+
const pages: Moh362Row[][] = [];
|
|
55
|
+
let start = 0;
|
|
56
|
+
while (rows.length - start > ROWS_ON_LAST_PAGE) {
|
|
57
|
+
const size = Math.min(ROWS_PER_FULL_PAGE, rows.length - start - 1);
|
|
58
|
+
pages.push(rows.slice(start, start + size));
|
|
59
|
+
start += size;
|
|
60
|
+
}
|
|
61
|
+
pages.push(rows.slice(start));
|
|
62
|
+
return pages;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One HIV test's header cell, ruled as on the form: the test, then the kit
|
|
67
|
+
* name, lot number and expiry date lines, then its result codes. Each client's
|
|
68
|
+
* own kit, lot and expiry are written under their result.
|
|
69
|
+
*/
|
|
70
|
+
const TestHeader: React.FC<{ test: (typeof TESTS)[number] }> = ({ test }) => (
|
|
25
71
|
<th className={styles.testHeader}>
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
72
|
+
<div className={styles.headerStack}>
|
|
73
|
+
<span className={styles.testName}>{test.label}</span>
|
|
74
|
+
<span className={styles.kit}>Kit Name:</span>
|
|
75
|
+
<span>Lot No.</span>
|
|
76
|
+
<span>
|
|
77
|
+
Expiry Date
|
|
78
|
+
<em>___/___/___</em>
|
|
79
|
+
</span>
|
|
80
|
+
<span className={styles.codes}>{test.codes}</span>
|
|
81
|
+
</div>
|
|
32
82
|
</th>
|
|
33
83
|
);
|
|
34
84
|
|
|
35
|
-
/** One client's HIV test: the result, with the kit, lot and expiry used beneath it */
|
|
36
|
-
const TestCell: React.FC<{
|
|
37
|
-
|
|
38
|
-
kit,
|
|
39
|
-
lot,
|
|
40
|
-
expiry,
|
|
41
|
-
}) => {
|
|
42
|
-
const detail = [text(kit), text(lot), formDate(expiry)].filter(Boolean).join(' · ');
|
|
85
|
+
/** One client's HIV test: the result in full, with the kit, lot and expiry used beneath it */
|
|
86
|
+
const TestCell: React.FC<{ row: Moh362Row; test: (typeof TESTS)[number] }> = ({ row, test }) => {
|
|
87
|
+
const detail = [text(row[test.kit]), text(row[test.lot]), formDate(row[test.expiry])].filter(Boolean).join(' · ');
|
|
43
88
|
return (
|
|
44
|
-
<td rowSpan={2} className={styles.testCell}>
|
|
45
|
-
|
|
46
|
-
|
|
89
|
+
<td rowSpan={2} className={styles.testCell} title={detail || undefined}>
|
|
90
|
+
{/* a box of the row's own height, so wrapping never makes the row taller */}
|
|
91
|
+
<div className={styles.testBox}>
|
|
92
|
+
<strong>{text(row[test.field])}</strong>
|
|
93
|
+
{detail && <span className={styles.testDetail}>{detail}</span>}
|
|
94
|
+
</div>
|
|
47
95
|
</td>
|
|
48
96
|
);
|
|
49
97
|
};
|
|
50
98
|
|
|
99
|
+
/** A heading with a dotted entry line above its result codes (n, o) */
|
|
100
|
+
const CodedHeader: React.FC<{ label: React.ReactNode; codes: string }> = ({ label, codes }) => (
|
|
101
|
+
<th>
|
|
102
|
+
<div className={styles.codedStack}>
|
|
103
|
+
<span>{label}</span>
|
|
104
|
+
<span className={styles.codes}>{codes}</span>
|
|
105
|
+
</div>
|
|
106
|
+
</th>
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
/** A heading holding two labels: upper cell above lower cell */
|
|
110
|
+
const SplitHeader: React.FC<{ upper: string; lower: string }> = ({ upper, lower }) => (
|
|
111
|
+
<th className={styles.splitHeader}>
|
|
112
|
+
<div className={styles.splitStack}>
|
|
113
|
+
<span>{upper}</span>
|
|
114
|
+
<span>{lower}</span>
|
|
115
|
+
</div>
|
|
116
|
+
</th>
|
|
117
|
+
);
|
|
118
|
+
|
|
51
119
|
/** A Tests (n) / Positive (n) block: age bands by sex, with totals */
|
|
52
120
|
const AgeSexSummary: React.FC<{ title: string; rows: Moh362Row[] }> = ({ title, rows }) => {
|
|
53
|
-
const sexes
|
|
121
|
+
const sexes = [
|
|
54
122
|
{ label: 'Male', code: 'M' },
|
|
55
123
|
{ label: 'Female', code: 'F' },
|
|
56
124
|
];
|
|
@@ -58,7 +126,7 @@ const AgeSexSummary: React.FC<{ title: string; rows: Moh362Row[] }> = ({ title,
|
|
|
58
126
|
<table className={styles.summaryTable}>
|
|
59
127
|
<thead>
|
|
60
128
|
<tr>
|
|
61
|
-
<th className={styles.
|
|
129
|
+
<th className={styles.titleCell}>{title}</th>
|
|
62
130
|
{AGE_BANDS.map((band) => (
|
|
63
131
|
<th key={band.label}>{band.label}</th>
|
|
64
132
|
))}
|
|
@@ -70,7 +138,7 @@ const AgeSexSummary: React.FC<{ title: string; rows: Moh362Row[] }> = ({ title,
|
|
|
70
138
|
const ofSex = rows.filter((row) => row.sex === sex.code);
|
|
71
139
|
return (
|
|
72
140
|
<tr key={sex.code}>
|
|
73
|
-
<th className={styles.
|
|
141
|
+
<th className={styles.rowLabel}>{sex.label}</th>
|
|
74
142
|
{AGE_BANDS.map((band) => (
|
|
75
143
|
<td key={band.label}>{count(ofSex, (row) => inBand(row, band))}</td>
|
|
76
144
|
))}
|
|
@@ -79,7 +147,7 @@ const AgeSexSummary: React.FC<{ title: string; rows: Moh362Row[] }> = ({ title,
|
|
|
79
147
|
);
|
|
80
148
|
})}
|
|
81
149
|
<tr>
|
|
82
|
-
<th className={styles.
|
|
150
|
+
<th className={styles.rowLabel}>Total</th>
|
|
83
151
|
{AGE_BANDS.map((band) => (
|
|
84
152
|
<td key={band.label}>{count(rows, (row) => inBand(row, band))}</td>
|
|
85
153
|
))}
|
|
@@ -90,300 +158,391 @@ const AgeSexSummary: React.FC<{ title: string; rows: Moh362Row[] }> = ({ title,
|
|
|
90
158
|
);
|
|
91
159
|
};
|
|
92
160
|
|
|
93
|
-
/**
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
161
|
+
/** A labelled single figure: Tests Facility (j), Discordant (o) ... */
|
|
162
|
+
const FigureRows: React.FC<{ figures: Array<[React.ReactNode, number]> }> = ({ figures }) => (
|
|
163
|
+
<table className={styles.summaryTable}>
|
|
164
|
+
<tbody>
|
|
165
|
+
{figures.map(([label, value], index) => (
|
|
166
|
+
<tr key={index}>
|
|
167
|
+
<th className={styles.figureLabel}>{label}</th>
|
|
168
|
+
<td className={styles.figure}>{value}</td>
|
|
169
|
+
</tr>
|
|
170
|
+
))}
|
|
171
|
+
</tbody>
|
|
172
|
+
</table>
|
|
173
|
+
);
|
|
174
|
+
|
|
175
|
+
/** A titled box holding one figure: Total Negative (n), Inconclusive test (n) */
|
|
176
|
+
const FigureBox: React.FC<{ title: string; value: number }> = ({ title, value }) => (
|
|
177
|
+
<table className={`${styles.summaryTable} ${styles.figureBox}`}>
|
|
178
|
+
<thead>
|
|
179
|
+
<tr>
|
|
180
|
+
<th>{title}</th>
|
|
181
|
+
</tr>
|
|
182
|
+
</thead>
|
|
183
|
+
<tbody>
|
|
184
|
+
<tr>
|
|
185
|
+
<td>{value}</td>
|
|
186
|
+
</tr>
|
|
187
|
+
</tbody>
|
|
188
|
+
</table>
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
/** The summary blocks at the foot of the last page, counting every client in the register */
|
|
192
|
+
const PageSummary: React.FC<{ rows: Moh362Row[] }> = ({ rows }) => {
|
|
193
|
+
const positives = rows.filter((row) => row.final_result === 'Positive');
|
|
194
|
+
const isKvp = (row: Moh362Row) => row.population_type === 'KVP';
|
|
195
|
+
const tests = TESTS.map((test, index) => {
|
|
196
|
+
const by = (result: string) => count(rows, (row) => row[test.field] === result);
|
|
197
|
+
const n = by('Negative');
|
|
198
|
+
const p = by('Positive');
|
|
199
|
+
const i = by('Inconclusive');
|
|
200
|
+
return { label: `${test.label} (${'klm'[index]})`, n, p, i, total: n + p + i };
|
|
201
|
+
});
|
|
202
|
+
const lines: Array<[string, 'n' | 'p' | 'i' | 'total' | null]> = [
|
|
203
|
+
['N: Negative', 'n'],
|
|
204
|
+
['P: Positive', 'p'],
|
|
205
|
+
['I: Invalid', 'i'],
|
|
206
|
+
['Wastage:', null],
|
|
207
|
+
['Total:', 'total'],
|
|
111
208
|
];
|
|
112
209
|
|
|
113
210
|
return (
|
|
114
|
-
<div className={styles.
|
|
115
|
-
<div className={styles.
|
|
116
|
-
<
|
|
117
|
-
<
|
|
118
|
-
|
|
211
|
+
<div className={styles.summary}>
|
|
212
|
+
<div className={styles.summaryColumn}>
|
|
213
|
+
<AgeSexSummary title="Tests (n )" rows={rows} />
|
|
214
|
+
<AgeSexSummary title="Positive (n)" rows={positives} />
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<div className={styles.summaryColumn}>
|
|
218
|
+
<FigureBox title="Total Negative (n)" value={count(rows, (row) => row.final_result === 'Negative')} />
|
|
219
|
+
<FigureBox title="Inconclusive test (n)" value={count(rows, (row) => row.final_result === 'Inconclusive')} />
|
|
220
|
+
</div>
|
|
221
|
+
|
|
222
|
+
<div className={styles.summaryColumn}>
|
|
223
|
+
<FigureRows
|
|
224
|
+
figures={[
|
|
225
|
+
['Tests Facility (j)', count(rows, (row) => row.setting === 'Facility')],
|
|
226
|
+
['Tests Community (j)', count(rows, (row) => row.setting === 'Community')],
|
|
227
|
+
]}
|
|
228
|
+
/>
|
|
229
|
+
<FigureRows figures={[['Discordant (o)', count(rows, (row) => row.discordant_couple === 'Yes')]]} />
|
|
230
|
+
<FigureRows
|
|
231
|
+
figures={[
|
|
232
|
+
['Tests Key pops (i)', count(rows, isKvp)],
|
|
233
|
+
[
|
|
234
|
+
<>
|
|
235
|
+
Positive Key pops
|
|
236
|
+
<br />
|
|
237
|
+
(i) & (n)
|
|
238
|
+
</>,
|
|
239
|
+
count(positives, isKvp),
|
|
240
|
+
],
|
|
241
|
+
]}
|
|
242
|
+
/>
|
|
119
243
|
</div>
|
|
120
|
-
<h4 className={styles.sheetTitle}>MOH 362: HTS LAB REGISTER</h4>
|
|
121
244
|
|
|
122
|
-
<div className={styles.
|
|
123
|
-
<table className={styles.
|
|
245
|
+
<div className={styles.summaryColumn}>
|
|
246
|
+
<table className={styles.summaryTable}>
|
|
124
247
|
<thead>
|
|
125
248
|
<tr>
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
<th className={styles.splitHeader}>
|
|
132
|
-
<span>National Identification Number</span>
|
|
133
|
-
<span>National Unique Patient Identifier (NUPI) Number</span>
|
|
134
|
-
</th>
|
|
135
|
-
<th>
|
|
136
|
-
Date of visit
|
|
137
|
-
<br />
|
|
138
|
-
(DD/MM/YYYY)
|
|
139
|
-
</th>
|
|
140
|
-
<th className={styles.wide}>
|
|
141
|
-
Client Name
|
|
142
|
-
<br />
|
|
143
|
-
(First, Middle, Last)
|
|
144
|
-
</th>
|
|
145
|
-
<th>Age</th>
|
|
146
|
-
<th>
|
|
147
|
-
Sex
|
|
148
|
-
<br />
|
|
149
|
-
(M/F)
|
|
150
|
-
</th>
|
|
151
|
-
<th>Telephone Number</th>
|
|
152
|
-
<th className={styles.codes}>
|
|
153
|
-
Marital Status
|
|
154
|
-
<br />
|
|
155
|
-
1=Married
|
|
156
|
-
<br />
|
|
157
|
-
2=Widowed
|
|
158
|
-
<br />
|
|
159
|
-
3=Single
|
|
160
|
-
<br />
|
|
161
|
-
4=Divorced
|
|
162
|
-
<br />
|
|
163
|
-
5=Separated
|
|
164
|
-
</th>
|
|
165
|
-
<th className={styles.codes}>
|
|
166
|
-
Population Type
|
|
167
|
-
<br />
|
|
168
|
-
1. Gen Pop
|
|
169
|
-
<br />
|
|
170
|
-
2. Key and Vulnerable Pop.
|
|
171
|
-
<br />
|
|
172
|
-
Codes for KVP
|
|
173
|
-
<br />- DC
|
|
174
|
-
<br />- MSM/MSW
|
|
175
|
-
<br />- FSW
|
|
176
|
-
<br />- PWID/PWUD
|
|
177
|
-
<br />- VP
|
|
178
|
-
</th>
|
|
179
|
-
<th className={styles.codes}>
|
|
180
|
-
Setting
|
|
181
|
-
<br />
|
|
182
|
-
(1=Facility
|
|
183
|
-
<br />
|
|
184
|
-
2=Community)
|
|
185
|
-
</th>
|
|
186
|
-
<TestHeader test="HIV Test 1" codes="(N,P,I)" />
|
|
187
|
-
<TestHeader test="HIV Test 2" codes="(N,P,I,NA)" />
|
|
188
|
-
<TestHeader test="HIV Test 3" codes="(N,P,I,NA)" />
|
|
189
|
-
<th>
|
|
190
|
-
Final HIV
|
|
191
|
-
<br />
|
|
192
|
-
Result
|
|
193
|
-
<br />
|
|
194
|
-
<br />
|
|
195
|
-
(N,P,Ic,NA)
|
|
196
|
-
</th>
|
|
197
|
-
<th>
|
|
198
|
-
Discordant
|
|
199
|
-
<br />
|
|
200
|
-
Couple
|
|
201
|
-
<br />
|
|
202
|
-
<br />
|
|
203
|
-
(Y/N/NA)
|
|
204
|
-
</th>
|
|
205
|
-
<th className={styles.codes}>
|
|
206
|
-
Referred for Prevention Services
|
|
207
|
-
<br />
|
|
208
|
-
1. PrEP
|
|
209
|
-
<br />
|
|
210
|
-
2. PEP
|
|
211
|
-
<br />
|
|
212
|
-
3. VMMC
|
|
213
|
-
<br />
|
|
214
|
-
4. PMTCT
|
|
215
|
-
<br />
|
|
216
|
-
5.NA
|
|
217
|
-
</th>
|
|
218
|
-
<th className={styles.wide}>
|
|
219
|
-
HTS Provider
|
|
220
|
-
<br />
|
|
221
|
-
(Name)
|
|
222
|
-
</th>
|
|
223
|
-
<th className={styles.wide}>Remarks</th>
|
|
224
|
-
</tr>
|
|
225
|
-
<tr className={styles.columnIds}>
|
|
226
|
-
{COLUMN_IDS.map((id) => (
|
|
227
|
-
<th key={id}>({id})</th>
|
|
249
|
+
{tests.map((test) => (
|
|
250
|
+
<React.Fragment key={test.label}>
|
|
251
|
+
<th className={styles.titleCell}>{test.label}</th>
|
|
252
|
+
<th>Number</th>
|
|
253
|
+
</React.Fragment>
|
|
228
254
|
))}
|
|
229
255
|
</tr>
|
|
230
256
|
</thead>
|
|
231
257
|
<tbody>
|
|
232
|
-
{
|
|
233
|
-
<
|
|
234
|
-
<tr>
|
|
235
|
-
<td rowSpan={2}>{text(row.serial_number) || index + 1}</td>
|
|
236
|
-
<td className={styles.upperCell}>{text(row.id_number)}</td>
|
|
237
|
-
<td rowSpan={2}>{formDate(row.date_of_visit)}</td>
|
|
238
|
-
<td rowSpan={2} className={styles.left}>
|
|
239
|
-
{clientName(row)}
|
|
240
|
-
</td>
|
|
241
|
-
<td rowSpan={2}>{text(row.age)}</td>
|
|
242
|
-
<td rowSpan={2}>{text(row.sex)}</td>
|
|
243
|
-
<td rowSpan={2}>{text(row.telephone)}</td>
|
|
244
|
-
<td rowSpan={2}>{text(row.marital_status)}</td>
|
|
245
|
-
<td className={styles.upperCell}>{text(row.population_type)}</td>
|
|
246
|
-
<td rowSpan={2}>{text(row.setting)}</td>
|
|
247
|
-
<TestCell result={row.hiv_test1} kit={row.kit1_name} lot={row.lot1} expiry={row.expiry1} />
|
|
248
|
-
<TestCell result={row.hiv_test2} kit={row.kit2_name} lot={row.lot2} expiry={row.expiry2} />
|
|
249
|
-
<TestCell result={row.hiv_test3} kit={row.kit3_name} lot={row.lot3} expiry={row.expiry3} />
|
|
250
|
-
<td rowSpan={2}>{finalResult(row.final_result)}</td>
|
|
251
|
-
<td rowSpan={2}>{text(row.discordant_couple)}</td>
|
|
252
|
-
<td rowSpan={2}>{text(row.referral_services)}</td>
|
|
253
|
-
<td rowSpan={2} className={styles.left}>
|
|
254
|
-
{text(row.hts_provider)}
|
|
255
|
-
</td>
|
|
256
|
-
<td rowSpan={2} className={styles.left}>
|
|
257
|
-
{text(row.initial_remarks)}
|
|
258
|
-
</td>
|
|
259
|
-
</tr>
|
|
260
|
-
<tr>
|
|
261
|
-
<td className={styles.lowerCell}>{text(row.nupi)}</td>
|
|
262
|
-
<td className={styles.lowerCell}>{text(row.sub_population)}</td>
|
|
263
|
-
</tr>
|
|
264
|
-
</React.Fragment>
|
|
265
|
-
))}
|
|
266
|
-
</tbody>
|
|
267
|
-
</table>
|
|
268
|
-
</div>
|
|
269
|
-
|
|
270
|
-
{/* The summary blocks at the foot of the sheet, laid out as on the form */}
|
|
271
|
-
<div className={styles.summary}>
|
|
272
|
-
<div className={styles.summaryColumn}>
|
|
273
|
-
<AgeSexSummary title="Tests (n )" rows={rows} />
|
|
274
|
-
<AgeSexSummary title="Positive (n)" rows={positives} />
|
|
275
|
-
</div>
|
|
276
|
-
|
|
277
|
-
<div className={styles.summaryColumn}>
|
|
278
|
-
<table className={styles.summaryTable}>
|
|
279
|
-
<thead>
|
|
280
|
-
<tr>
|
|
281
|
-
<th>Total Negative (n)</th>
|
|
282
|
-
</tr>
|
|
283
|
-
</thead>
|
|
284
|
-
<tbody>
|
|
285
|
-
<tr>
|
|
286
|
-
<td className={styles.bigFigure}>{count(rows, (row) => row.final_result === 'N')}</td>
|
|
287
|
-
</tr>
|
|
288
|
-
</tbody>
|
|
289
|
-
</table>
|
|
290
|
-
<table className={styles.summaryTable}>
|
|
291
|
-
<thead>
|
|
292
|
-
<tr>
|
|
293
|
-
<th>Inconclusive test (n)</th>
|
|
294
|
-
</tr>
|
|
295
|
-
</thead>
|
|
296
|
-
<tbody>
|
|
297
|
-
<tr>
|
|
298
|
-
<td className={styles.bigFigure}>{count(rows, (row) => finalResult(row.final_result) === 'Ic')}</td>
|
|
299
|
-
</tr>
|
|
300
|
-
</tbody>
|
|
301
|
-
</table>
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
<div className={styles.summaryColumn}>
|
|
305
|
-
<table className={styles.summaryTable}>
|
|
306
|
-
<tbody>
|
|
307
|
-
<tr>
|
|
308
|
-
<th className={styles.summaryLabel}>Tests Facility (j)</th>
|
|
309
|
-
<td>{count(rows, (row) => String(row.setting ?? '') === '1')}</td>
|
|
310
|
-
</tr>
|
|
311
|
-
<tr>
|
|
312
|
-
<th className={styles.summaryLabel}>Tests Community (j)</th>
|
|
313
|
-
<td>{count(rows, (row) => String(row.setting ?? '') === '2')}</td>
|
|
314
|
-
</tr>
|
|
315
|
-
</tbody>
|
|
316
|
-
</table>
|
|
317
|
-
<table className={styles.summaryTable}>
|
|
318
|
-
<tbody>
|
|
319
|
-
<tr>
|
|
320
|
-
<th className={styles.summaryLabel}>Discordant (o)</th>
|
|
321
|
-
<td>{count(rows, (row) => row.discordant_couple === 'Y')}</td>
|
|
322
|
-
</tr>
|
|
323
|
-
</tbody>
|
|
324
|
-
</table>
|
|
325
|
-
<table className={styles.summaryTable}>
|
|
326
|
-
<tbody>
|
|
327
|
-
<tr>
|
|
328
|
-
<th className={styles.summaryLabel}>Tests Key pops (i)</th>
|
|
329
|
-
<td>{count(rows, isKvp)}</td>
|
|
330
|
-
</tr>
|
|
331
|
-
<tr>
|
|
332
|
-
<th className={styles.summaryLabel}>
|
|
333
|
-
Positive Key pops
|
|
334
|
-
<br />
|
|
335
|
-
(i) & (n)
|
|
336
|
-
</th>
|
|
337
|
-
<td>{count(positives, isKvp)}</td>
|
|
338
|
-
</tr>
|
|
339
|
-
</tbody>
|
|
340
|
-
</table>
|
|
341
|
-
</div>
|
|
342
|
-
|
|
343
|
-
<div className={styles.summaryColumn}>
|
|
344
|
-
<table className={styles.summaryTable}>
|
|
345
|
-
<thead>
|
|
346
|
-
<tr>
|
|
258
|
+
{lines.map(([label, key]) => (
|
|
259
|
+
<tr key={label}>
|
|
347
260
|
{tests.map((test) => (
|
|
348
261
|
<React.Fragment key={test.label}>
|
|
349
|
-
<th className={styles.
|
|
350
|
-
|
|
262
|
+
<th className={styles.rowLabel}>{label}</th>
|
|
263
|
+
{/* wastage is counted on the kits, not recorded per client */}
|
|
264
|
+
<td>{key ? test[key] : ''}</td>
|
|
351
265
|
</React.Fragment>
|
|
352
266
|
))}
|
|
353
267
|
</tr>
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
[
|
|
358
|
-
['N: Negative', 'n'],
|
|
359
|
-
['P: Positive', 'p'],
|
|
360
|
-
['I: Invalid', 'i'],
|
|
361
|
-
['Wastage:', null],
|
|
362
|
-
['Total:', 'total'],
|
|
363
|
-
] as Array<[string, 'n' | 'p' | 'i' | 'total' | null]>
|
|
364
|
-
).map(([label, key]) => (
|
|
365
|
-
<tr key={label}>
|
|
366
|
-
{tests.map((test) => (
|
|
367
|
-
<React.Fragment key={test.label}>
|
|
368
|
-
<th className={styles.summaryLabel}>{label}</th>
|
|
369
|
-
{/* wastage is counted on the kits, not recorded per client */}
|
|
370
|
-
<td>{key ? test[key] : ''}</td>
|
|
371
|
-
</React.Fragment>
|
|
372
|
-
))}
|
|
373
|
-
</tr>
|
|
374
|
-
))}
|
|
375
|
-
</tbody>
|
|
376
|
-
</table>
|
|
377
|
-
</div>
|
|
268
|
+
))}
|
|
269
|
+
</tbody>
|
|
270
|
+
</table>
|
|
378
271
|
</div>
|
|
272
|
+
</div>
|
|
273
|
+
);
|
|
274
|
+
};
|
|
379
275
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
276
|
+
/** One client: two ruled lines, columns (b) and (i) split into upper and lower cells */
|
|
277
|
+
const ClientRows: React.FC<{ row?: Moh362Row; serial?: number }> = ({ row, serial }) => {
|
|
278
|
+
if (!row) {
|
|
279
|
+
return (
|
|
280
|
+
<>
|
|
281
|
+
<tr className={styles.upperLine}>
|
|
282
|
+
{COLUMN_IDS.map((id) => (
|
|
283
|
+
<td
|
|
284
|
+
key={id}
|
|
285
|
+
rowSpan={id === 'b' || id === 'i' ? 1 : 2}
|
|
286
|
+
className={id === 'b' || id === 'i' ? styles.upperCell : undefined}
|
|
287
|
+
/>
|
|
288
|
+
))}
|
|
289
|
+
</tr>
|
|
290
|
+
<tr className={styles.lowerLine}>
|
|
291
|
+
<td className={styles.lowerCell} />
|
|
292
|
+
<td className={styles.lowerCell} />
|
|
293
|
+
</tr>
|
|
294
|
+
</>
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return (
|
|
299
|
+
<>
|
|
300
|
+
<tr className={styles.upperLine}>
|
|
301
|
+
<td rowSpan={2}>{text(row.serial_number) || serial}</td>
|
|
302
|
+
<td className={`${styles.upperCell} ${styles.idCell}`}>{text(row.id_number)}</td>
|
|
303
|
+
<td rowSpan={2}>{formDate(row.date_of_visit)}</td>
|
|
304
|
+
<td rowSpan={2} className={styles.left}>
|
|
305
|
+
<span className={styles.clamp}>{clientName(row)}</span>
|
|
306
|
+
</td>
|
|
307
|
+
<td rowSpan={2}>{text(row.age)}</td>
|
|
308
|
+
<td rowSpan={2}>{text(row.sex)}</td>
|
|
309
|
+
<td rowSpan={2}>{text(row.telephone)}</td>
|
|
310
|
+
<td rowSpan={2}>{text(row.marital_status)}</td>
|
|
311
|
+
<td className={styles.upperCell}>{text(row.population_type)}</td>
|
|
312
|
+
<td rowSpan={2}>{text(row.setting)}</td>
|
|
313
|
+
{TESTS.map((test) => (
|
|
314
|
+
<TestCell key={test.field} row={row} test={test} />
|
|
315
|
+
))}
|
|
316
|
+
<td rowSpan={2} className={styles.finalResult}>
|
|
317
|
+
{text(row.final_result)}
|
|
318
|
+
</td>
|
|
319
|
+
<td rowSpan={2}>{text(row.discordant_couple)}</td>
|
|
320
|
+
<td rowSpan={2}>
|
|
321
|
+
<span className={styles.clamp}>{text(row.referral_services)}</span>
|
|
322
|
+
</td>
|
|
323
|
+
<td rowSpan={2} className={styles.left}>
|
|
324
|
+
<span className={styles.clamp}>{text(row.hts_provider)}</span>
|
|
325
|
+
</td>
|
|
326
|
+
<td rowSpan={2} className={styles.left}>
|
|
327
|
+
<span className={styles.clamp}>{text(row.initial_remarks)}</span>
|
|
328
|
+
</td>
|
|
329
|
+
</tr>
|
|
330
|
+
<tr className={styles.lowerLine}>
|
|
331
|
+
<td className={`${styles.lowerCell} ${styles.idCell}`}>{text(row.nupi)}</td>
|
|
332
|
+
<td className={styles.lowerCell}>{text(row.sub_population)}</td>
|
|
333
|
+
</tr>
|
|
334
|
+
</>
|
|
335
|
+
);
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* One A4 landscape page of the register. Pages before the last are ruled with
|
|
340
|
+
* client rows to the foot; the last has 15 rows with the summary of the whole
|
|
341
|
+
* register beneath them, in one frame.
|
|
342
|
+
*/
|
|
343
|
+
const LabRegisterPage: React.FC<{
|
|
344
|
+
rows: Moh362Row[];
|
|
345
|
+
firstSerial: number;
|
|
346
|
+
page: number;
|
|
347
|
+
pages: number;
|
|
348
|
+
scale: number;
|
|
349
|
+
/** every client in the register, given only to the last page */
|
|
350
|
+
summaryRows?: Moh362Row[];
|
|
351
|
+
}> = ({ rows, firstSerial, page, pages, scale, summaryRows }) => (
|
|
352
|
+
<section className={styles.page} data-register-page style={{ '--page-zoom': scale } as React.CSSProperties}>
|
|
353
|
+
<div className={styles.runningHead}>
|
|
354
|
+
<span>Ministry Of Health</span>
|
|
355
|
+
<span>MOH 362 HTS LAB, REFERRAL AND LINKAGE REGISTER</span>
|
|
356
|
+
<span>Ver. July 2023</span>
|
|
357
|
+
</div>
|
|
358
|
+
<h4 className={styles.title}>MOH 362: HTS LAB REGISTER</h4>
|
|
359
|
+
|
|
360
|
+
<div className={summaryRows ? `${styles.frame} ${styles.lastFrame}` : styles.frame}>
|
|
361
|
+
<table className={styles.register}>
|
|
362
|
+
<colgroup>
|
|
363
|
+
{COLUMN_WIDTHS.map((width, index) => (
|
|
364
|
+
<col key={index} style={{ width: `${(width / COLUMN_TOTAL) * 100}%` }} />
|
|
365
|
+
))}
|
|
366
|
+
</colgroup>
|
|
367
|
+
<thead>
|
|
368
|
+
<tr className={styles.headings}>
|
|
369
|
+
<th>
|
|
370
|
+
Serial
|
|
371
|
+
<br />
|
|
372
|
+
Number
|
|
373
|
+
</th>
|
|
374
|
+
<SplitHeader
|
|
375
|
+
upper="National Identification Number"
|
|
376
|
+
lower="National Unique Patient Identifier (NUPI) Number"
|
|
377
|
+
/>
|
|
378
|
+
<th>
|
|
379
|
+
Date of visit
|
|
380
|
+
<br />
|
|
381
|
+
(DD/MM/YYYY)
|
|
382
|
+
</th>
|
|
383
|
+
<th>
|
|
384
|
+
Client Name
|
|
385
|
+
<br />
|
|
386
|
+
(First, Middle, Last)
|
|
387
|
+
</th>
|
|
388
|
+
<th>Age</th>
|
|
389
|
+
<th>
|
|
390
|
+
Sex
|
|
391
|
+
<br />
|
|
392
|
+
(M/F)
|
|
393
|
+
</th>
|
|
394
|
+
<th>Telephone Number</th>
|
|
395
|
+
<th className={styles.codeList}>
|
|
396
|
+
Marital
|
|
397
|
+
<br />
|
|
398
|
+
Status
|
|
399
|
+
<br />
|
|
400
|
+
1=Married
|
|
401
|
+
<br />
|
|
402
|
+
2=Widowed
|
|
403
|
+
<br />
|
|
404
|
+
3=Single
|
|
405
|
+
<br />
|
|
406
|
+
4=Divorced
|
|
407
|
+
<br />
|
|
408
|
+
5=Separated
|
|
409
|
+
</th>
|
|
410
|
+
<th className={styles.codeList}>
|
|
411
|
+
Population Type
|
|
412
|
+
<br />
|
|
413
|
+
1. Gen Pop
|
|
414
|
+
<br />
|
|
415
|
+
2. Key and Vulnerable Pop.
|
|
416
|
+
<br />
|
|
417
|
+
<br />
|
|
418
|
+
Codes for KVP
|
|
419
|
+
<br />- DC
|
|
420
|
+
<br />- MSM/MSW
|
|
421
|
+
<br />- FSW
|
|
422
|
+
<br />- PWID/PWUD
|
|
423
|
+
<br />- VP
|
|
424
|
+
</th>
|
|
425
|
+
<th className={styles.codeList}>
|
|
426
|
+
Setting
|
|
427
|
+
<br />
|
|
428
|
+
(1=Facility
|
|
429
|
+
<br />
|
|
430
|
+
2=Community)
|
|
431
|
+
</th>
|
|
432
|
+
{TESTS.map((test) => (
|
|
433
|
+
<TestHeader key={test.field} test={test} />
|
|
434
|
+
))}
|
|
435
|
+
<CodedHeader
|
|
436
|
+
label={
|
|
437
|
+
<>
|
|
438
|
+
Final HIV
|
|
439
|
+
<br />
|
|
440
|
+
Result
|
|
441
|
+
</>
|
|
442
|
+
}
|
|
443
|
+
codes="(N,P,Ic,NA)"
|
|
444
|
+
/>
|
|
445
|
+
<CodedHeader
|
|
446
|
+
label={
|
|
447
|
+
<>
|
|
448
|
+
Discordant
|
|
449
|
+
<br />
|
|
450
|
+
Couple
|
|
451
|
+
</>
|
|
452
|
+
}
|
|
453
|
+
codes="(Y/N/NA)"
|
|
454
|
+
/>
|
|
455
|
+
<th>
|
|
456
|
+
<div className={styles.referralStack}>
|
|
457
|
+
<span>
|
|
458
|
+
Referred for
|
|
459
|
+
<br />
|
|
460
|
+
Prevention
|
|
461
|
+
<br />
|
|
462
|
+
Services
|
|
463
|
+
</span>
|
|
464
|
+
<span>
|
|
465
|
+
1. PrEP
|
|
466
|
+
<br />
|
|
467
|
+
2. PEP
|
|
468
|
+
<br />
|
|
469
|
+
3. VMMC
|
|
470
|
+
<br />
|
|
471
|
+
4. PMTCT
|
|
472
|
+
<br />
|
|
473
|
+
5.NA
|
|
474
|
+
</span>
|
|
475
|
+
</div>
|
|
476
|
+
</th>
|
|
477
|
+
<th>
|
|
478
|
+
HTS Provider
|
|
479
|
+
<br />
|
|
480
|
+
(Name)
|
|
481
|
+
</th>
|
|
482
|
+
<th>Remarks</th>
|
|
483
|
+
</tr>
|
|
484
|
+
<tr className={styles.columnIds}>
|
|
485
|
+
{COLUMN_IDS.map((id) => (
|
|
486
|
+
<th key={id}>({id})</th>
|
|
487
|
+
))}
|
|
488
|
+
</tr>
|
|
489
|
+
</thead>
|
|
490
|
+
<tbody>
|
|
491
|
+
{Array.from({ length: summaryRows ? ROWS_ON_LAST_PAGE : ROWS_PER_FULL_PAGE }, (_, index) => (
|
|
492
|
+
<ClientRows key={index} row={rows[index]} serial={firstSerial + index} />
|
|
493
|
+
))}
|
|
494
|
+
</tbody>
|
|
495
|
+
</table>
|
|
496
|
+
|
|
497
|
+
{summaryRows && (
|
|
498
|
+
<>
|
|
499
|
+
<PageSummary rows={summaryRows} />
|
|
500
|
+
|
|
501
|
+
<div className={styles.signOff}>
|
|
502
|
+
<span>
|
|
503
|
+
Supervisor's Name: <span className={styles.rule} />
|
|
504
|
+
</span>
|
|
505
|
+
<span>
|
|
506
|
+
Signature: <span className={styles.rule} />
|
|
507
|
+
</span>
|
|
508
|
+
</div>
|
|
509
|
+
</>
|
|
510
|
+
)}
|
|
511
|
+
</div>
|
|
512
|
+
|
|
513
|
+
<div className={styles.pageFoot}>
|
|
514
|
+
<span>
|
|
515
|
+
Page {page} of {pages}
|
|
516
|
+
</span>
|
|
517
|
+
<span>Ver. July 2023</span>
|
|
518
|
+
</div>
|
|
519
|
+
</section>
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* MOH 362: HTS LAB REGISTER (Ver. July 2023), page 5 of the MOH 362 form, as
|
|
524
|
+
* A4 landscape pages of clients, columns (a) to (r). The summary blocks sum
|
|
525
|
+
* the whole register, so they appear once, at the foot of the last page.
|
|
526
|
+
*/
|
|
527
|
+
const Moh362LabRegister: React.FC<Moh362LabRegisterProps> = ({ rows }) => {
|
|
528
|
+
const { ref, scale } = usePageScale<HTMLDivElement>(PAGE_WIDTH_MM);
|
|
529
|
+
const pages = paginate(rows);
|
|
530
|
+
const serials = pages.map((_, index) => pages.slice(0, index).reduce((sum, page) => sum + page.length, 0) + 1);
|
|
531
|
+
|
|
532
|
+
return (
|
|
533
|
+
<div className={styles.reportFrame}>
|
|
534
|
+
<div className={styles.viewport} ref={ref}>
|
|
535
|
+
{pages.map((pageRows, index) => (
|
|
536
|
+
<LabRegisterPage
|
|
537
|
+
key={index}
|
|
538
|
+
rows={pageRows}
|
|
539
|
+
firstSerial={serials[index]}
|
|
540
|
+
page={index + 1}
|
|
541
|
+
pages={pages.length}
|
|
542
|
+
scale={scale}
|
|
543
|
+
summaryRows={index === pages.length - 1 ? rows : undefined}
|
|
544
|
+
/>
|
|
545
|
+
))}
|
|
387
546
|
</div>
|
|
388
547
|
</div>
|
|
389
548
|
);
|