@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.
Files changed (69) hide show
  1. package/dist/0.css +1 -0
  2. package/dist/0.css.map +1 -0
  3. package/dist/0.js +1 -0
  4. package/dist/0.js.map +1 -0
  5. package/dist/410.js +1 -1
  6. package/dist/410.js.map +1 -1
  7. package/dist/600.js +1 -1
  8. package/dist/600.js.map +1 -1
  9. package/dist/757.js +1 -0
  10. package/dist/757.js.map +1 -0
  11. package/dist/ampath-esm-reports-app.js +1 -1
  12. package/dist/ampath-esm-reports-app.js.map +1 -1
  13. package/dist/routes.json +1 -1
  14. package/package.json +1 -1
  15. package/src/common/report-filters/report-filters.scss +15 -6
  16. package/src/common/report-page/download-sheet-pdf.ts +193 -27
  17. package/src/common/report-register/report-parents.ts +2 -0
  18. package/src/dashboard/report-catalog.ts +18 -0
  19. package/src/registers/moh-267/moh-267-register.component.tsx +407 -0
  20. package/src/registers/moh-267/moh-267-register.scss +298 -0
  21. package/src/registers/moh-267/moh-267-summary.ts +102 -0
  22. package/src/registers/moh-267/moh-267.component.tsx +94 -0
  23. package/src/registers/moh-267/moh-267.types.ts +47 -0
  24. package/src/registers/moh-362/moh-362-lab-register.component.tsx +469 -310
  25. package/src/registers/moh-362/moh-362-lab-register.scss +412 -0
  26. package/src/registers/moh-362/moh-362-linkage-register.component.tsx +188 -93
  27. package/src/registers/moh-362/moh-362-linkage-register.scss +91 -0
  28. package/src/registers/moh-362/moh-362.component.tsx +33 -15
  29. package/src/registers/moh-362/moh-362.types.ts +15 -25
  30. package/src/registers/presumptive-tb/presumptive-tb-register.component.tsx +391 -0
  31. package/src/registers/presumptive-tb/presumptive-tb-register.scss +262 -0
  32. package/src/registers/presumptive-tb/presumptive-tb.component.tsx +92 -0
  33. package/src/registers/presumptive-tb/presumptive-tb.types.ts +88 -0
  34. package/src/registers/register-catalog.ts +37 -3
  35. package/src/registers/register-file-name.ts +18 -0
  36. package/src/registers/tb4/tb4-parts.tsx +164 -0
  37. package/src/registers/tb4/tb4-patient-sheet.component.tsx +418 -0
  38. package/src/registers/tb4/tb4-register.scss +282 -0
  39. package/src/registers/tb4/tb4-treatment-sheet.component.tsx +349 -0
  40. package/src/registers/tb4/tb4.component.tsx +120 -0
  41. package/src/registers/tb4/tb4.types.ts +129 -0
  42. package/src/reports/acf-summary/acf-summary-form.component.tsx +176 -0
  43. package/src/reports/acf-summary/acf-summary-linelist.component.tsx +86 -0
  44. package/src/reports/acf-summary/acf-summary.component.tsx +97 -0
  45. package/src/reports/acf-summary/acf-summary.counts.ts +37 -0
  46. package/src/reports/acf-summary/acf-summary.scss +162 -0
  47. package/src/reports/moh-729b/moh-729b-form.component.tsx +385 -0
  48. package/src/reports/moh-729b/moh-729b-linelist.component.tsx +205 -0
  49. package/src/reports/moh-729b/moh-729b-summary.component.tsx +258 -0
  50. package/src/reports/moh-729b/moh-729b-summary.config.ts +229 -0
  51. package/src/reports/moh-729b/moh-729b.component.tsx +157 -0
  52. package/src/reports/moh-729b/moh-729b.config.ts +235 -0
  53. package/src/reports/moh-729b/moh-729b.scss +276 -0
  54. package/src/reports/moh-729b/moh-729b.types.ts +111 -0
  55. package/src/reports/moh-731/moh-731.component.tsx +10 -7
  56. package/src/reports/moh-731/moh-731.scss +3 -6
  57. package/src/reports/moh-731/summary/moh-731-summary.config.ts +2 -4
  58. package/src/reports/moh-731/summary/moh-731-summary.scss +5 -0
  59. package/src/resources/moh-267.resource.ts +27 -0
  60. package/src/resources/moh-729b.resource.ts +25 -0
  61. package/src/resources/presumptive-tb.resource.ts +27 -0
  62. package/src/root.component.tsx +14 -0
  63. package/dist/177.css +0 -1
  64. package/dist/177.css.map +0 -1
  65. package/dist/177.js +0 -1
  66. package/dist/177.js.map +0 -1
  67. package/dist/580.js +0 -1
  68. package/dist/580.js.map +0 -1
  69. 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
- AGE_BANDS,
5
- clientName,
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
- /** One HIV test's header cell: the test, then kit, lot and expiry lines, then its result codes */
24
- const TestHeader: React.FC<{ test: string; codes: string }> = ({ test, codes }) => (
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
- <span>{test}</span>
27
- <span>Kit Name:</span>
28
- <span>Lot No.</span>
29
- <span>Expiry Date</span>
30
- <span className={styles.dateRule}>____/____/____</span>
31
- <span>{codes}</span>
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<{ result: unknown; kit: unknown; lot: unknown; expiry: unknown }> = ({
37
- result,
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
- <strong>{testResult(result)}</strong>
46
- {detail && <span className={styles.testDetail}>{detail}</span>}
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: Array<{ label: string; code: string }> = [
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.summaryLabel}>{title}</th>
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.summaryLabel}>{sex.label}</th>
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.summaryLabel}>Total</th>
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
- * MOH 362: HTS LAB REGISTER (Ver. July 2023), page 5 of the MOH 362 form: one
95
- * row per client tested, columns (a) to (r), and the summary blocks beneath.
96
- */
97
- const Moh362LabRegister: React.FC<Moh362LabRegisterProps> = ({ rows }) => {
98
- const positives = rows.filter((row) => row.final_result === 'P');
99
- const isKvp = (row: Moh362Row) => String(row.population_type ?? '') === '2';
100
- const testTotals = (field: 'hiv_test1' | 'hiv_test2' | 'hiv_test3') => {
101
- const by = (code: string) => count(rows, (row) => testResult(row[field]) === code);
102
- const n = by('N');
103
- const p = by('P');
104
- const i = by('I');
105
- return { n, p, i, total: n + p + i };
106
- };
107
- const tests = [
108
- { label: 'HIV Test 1 (k)', ...testTotals('hiv_test1') },
109
- { label: 'HIV Test 2 (l)', ...testTotals('hiv_test2') },
110
- { label: 'HIV Test 3 (m)', ...testTotals('hiv_test3') },
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.sheet}>
115
- <div className={styles.runningHead}>
116
- <span>Ministry Of Health</span>
117
- <span>MOH 362 HTS LAB, REFERRAL AND LINKAGE REGISTER</span>
118
- <span>Ver. July 2023</span>
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) &amp; (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.tableScroll}>
123
- <table className={styles.register}>
245
+ <div className={styles.summaryColumn}>
246
+ <table className={styles.summaryTable}>
124
247
  <thead>
125
248
  <tr>
126
- <th>
127
- Serial
128
- <br />
129
- Number
130
- </th>
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
- {rows.map((row, index) => (
233
- <React.Fragment key={`${row.patient_id}-${row.serial_number}-${index}`}>
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) &amp; (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.summaryLabel}>{test.label}</th>
350
- <th>Number</th>
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
- </thead>
355
- <tbody>
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
- <div className={styles.signOff}>
381
- <span>
382
- Supervisor&apos;s Name: <span className={styles.rule} />
383
- </span>
384
- <span>
385
- Signature: <span className={styles.rule} />
386
- </span>
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&apos;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
  );