@ampath/esm-reports-app 1.0.0-next.63 → 1.0.0-next.64

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 (51) hide show
  1. package/dist/299.js +2 -0
  2. package/dist/299.js.LICENSE.txt +3 -0
  3. package/dist/299.js.map +1 -0
  4. package/dist/354.js +2 -0
  5. package/dist/354.js.LICENSE.txt +20 -0
  6. package/dist/354.js.map +1 -0
  7. package/dist/410.js +1 -1
  8. package/dist/410.js.map +1 -1
  9. package/dist/457.js +2 -0
  10. package/dist/457.js.LICENSE.txt +261 -0
  11. package/dist/457.js.map +1 -0
  12. package/dist/480.js +1 -0
  13. package/dist/781.js +1 -0
  14. package/dist/781.js.map +1 -0
  15. package/dist/907.js +1 -1
  16. package/dist/907.js.map +1 -1
  17. package/dist/953.css +1 -0
  18. package/dist/953.css.map +1 -0
  19. package/dist/953.js +1 -0
  20. package/dist/953.js.map +1 -0
  21. package/dist/99.js +1 -1
  22. package/dist/99.js.map +1 -0
  23. package/dist/ampath-esm-reports-app.js +1 -1
  24. package/dist/routes.json +1 -1
  25. package/package.json +3 -1
  26. package/src/common/linelist-table/linelist-table.component.tsx +1 -1
  27. package/src/common/report-filters/report-filters.component.tsx +25 -4
  28. package/src/common/report-filters/types.ts +13 -0
  29. package/src/common/report-page/download-sheet-pdf.ts +59 -0
  30. package/src/common/report-page/index.ts +1 -0
  31. package/src/dashboard/reports-dasboard.tsx +156 -69
  32. package/src/dashboard/reports-dashboard.module.scss +144 -59
  33. package/src/registers/moh-362/moh-362-lab-register.component.tsx +392 -0
  34. package/src/registers/moh-362/moh-362-linkage-register.component.tsx +120 -0
  35. package/src/registers/moh-362/moh-362-register.scss +222 -0
  36. package/src/registers/moh-362/moh-362.component.tsx +99 -0
  37. package/src/registers/moh-362/moh-362.scss +11 -0
  38. package/src/registers/moh-362/moh-362.types.ts +97 -0
  39. package/src/registers/register-catalog.ts +30 -0
  40. package/src/reports/moh-731/linelist/moh-731-indicators.ts +82 -0
  41. package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +51 -2
  42. package/src/reports/moh-731/moh-731.component.tsx +18 -3
  43. package/src/reports/moh-731/moh-731.scss +6 -0
  44. package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +49 -52
  45. package/src/reports/moh-731/summary/moh-731-summary.config.ts +25 -16
  46. package/src/resources/moh-362.resource.ts +27 -0
  47. package/src/root.component.tsx +2 -0
  48. package/dist/876.css +0 -1
  49. package/dist/876.css.map +0 -1
  50. package/dist/876.js +0 -1
  51. package/dist/876.js.map +0 -1
@@ -25,78 +25,93 @@ $tones: (
25
25
  );
26
26
 
27
27
  .dashboard {
28
- /* Horizontal gutter comes from `.page` in root.scss */
29
- padding: layout.$spacing-05 0 layout.$spacing-07;
28
+ /* `.page` in root.scss gives every route this side gutter. The dashboard
29
+ reaches over it so its grey runs edge to edge, then pads it back. */
30
+ --page-gutter: #{layout.$spacing-06};
31
+
32
+ box-sizing: border-box;
33
+ margin: 0 calc(-1 * var(--page-gutter));
34
+ padding: 0 var(--page-gutter) layout.$spacing-07;
35
+ /* the original grey behind the reports; the tab row above it is white */
30
36
  background-color: $openmrs-background-grey;
31
- min-height: 100%;
37
+ /* grey down to the foot of the window, below the 3rem top bar, however few
38
+ reports are listed */
39
+ min-height: calc(100vh - 3rem);
32
40
  }
33
41
 
34
42
  /* Header */
35
43
 
36
- .header {
37
- display: flex;
38
- align-items: center;
39
- flex-wrap: wrap;
40
- gap: layout.$spacing-03 layout.$spacing-04;
41
- padding: layout.$spacing-03 layout.$spacing-05;
42
- margin-bottom: layout.$spacing-04;
43
- background-color: $ui-02;
44
- border: 1px solid $ui-03;
45
- border-left: layout.$spacing-02 solid var(--brand-01);
46
- }
47
-
48
- .headerIcon {
49
- display: flex;
50
- align-items: center;
51
- justify-content: center;
52
- flex-shrink: 0;
53
- width: layout.$spacing-06;
54
- height: layout.$spacing-06;
55
- color: var(--brand-01);
56
- background-color: $brand-primary-10;
57
- }
58
-
59
- .headerTitle {
60
- @include type.type-style('productive-heading-03');
61
- color: $color-gray-100;
44
+ /* Tabs, with the facility and date at the right end of the tab row */
45
+ .tabsArea {
46
+ position: relative;
47
+ padding-top: layout.$spacing-05;
48
+
49
+ /* a white band behind the tab row, edge to edge over the page gutter, with
50
+ the tabs' 2px rule along its foot; the reports below sit on grey */
51
+ &::before {
52
+ content: '';
53
+ position: absolute;
54
+ top: 0;
55
+ left: calc(-1 * var(--page-gutter));
56
+ right: calc(-1 * var(--page-gutter));
57
+ height: calc(#{layout.$spacing-05} + #{layout.$spacing-08});
58
+ background-color: $ui-02;
59
+ box-shadow: inset 0 -2px 0 $ui-03;
60
+ }
62
61
  }
63
62
 
64
63
  .headerMeta {
64
+ @include type.type-style('body-compact-01');
65
+ position: absolute;
66
+ /* level with the tab row, below the white band's top padding, and drawn
67
+ over the band */
68
+ top: layout.$spacing-05;
69
+ right: 0;
70
+ z-index: 1;
65
71
  display: flex;
66
72
  align-items: center;
67
- gap: layout.$spacing-05;
68
- margin-left: auto;
69
- }
70
-
71
- .metaItem {
72
- @include type.type-style('label-01');
73
- display: inline-flex;
74
- align-items: center;
75
- gap: layout.$spacing-02;
73
+ /* the height of the tab row, so the line sits level with the tab labels */
74
+ height: layout.$spacing-08;
76
75
  color: $text-02;
77
76
  white-space: nowrap;
78
-
79
- svg {
80
- fill: var(--brand-01);
81
- }
82
77
  }
83
78
 
84
79
  /* Toolbar */
85
80
 
86
81
  .toolbar {
87
- display: flex;
82
+ /* the tiles' columns, so the search box is exactly one tile wide and the
83
+ count ends at the grid's right edge */
84
+ display: grid;
85
+ grid-template-columns: repeat(3, 1fr);
88
86
  align-items: center;
89
- gap: layout.$spacing-05;
87
+ gap: layout.$spacing-04;
90
88
  margin-bottom: layout.$spacing-05;
91
89
  }
92
90
 
93
91
  .searchLayer {
94
- flex: 1 1 auto;
95
- max-width: 24rem;
92
+ /* two tiles wide, edges lined up with the first two tiles */
93
+ grid-column: 1 / span 2;
94
+
95
+ /* a white field with a thin outline, and the brand colour on focus */
96
+ :global(.cds--search-input) {
97
+ background-color: $ui-02;
98
+ border: 1px solid $ui-03;
99
+
100
+ &:hover {
101
+ border-color: $ui-04;
102
+ }
103
+
104
+ &:focus {
105
+ outline: 2px solid var(--brand-01);
106
+ outline-offset: -2px;
107
+ }
108
+ }
96
109
  }
97
110
 
98
111
  .resultCount {
99
112
  @include type.type-style('label-01');
113
+ grid-column: -2 / -1;
114
+ justify-self: end;
100
115
  color: $text-02;
101
116
  white-space: nowrap;
102
117
  }
@@ -216,6 +231,54 @@ $tones: (
216
231
 
217
232
  /* Empty state */
218
233
 
234
+ /* Reports / Registers tabs, between the header and the panels */
235
+
236
+ .tabList {
237
+ /* above the white band drawn behind it (.tabsArea::before) */
238
+ position: relative;
239
+ z-index: 1;
240
+ margin-bottom: layout.$spacing-05;
241
+
242
+ /* the first tab's label lines up with the content's left edge */
243
+ :global(.cds--tabs__nav-item:first-child) {
244
+ padding-left: 0;
245
+ }
246
+
247
+ /* keyboard / click focus in the brand colour too */
248
+ :global(.cds--tabs__nav-item:focus) {
249
+ outline-color: var(--brand-01);
250
+ }
251
+
252
+ /* the open tab in the AMPATH brand colour rather than Carbon blue */
253
+ :global(.cds--tabs__nav-item--selected) {
254
+ border-bottom-color: var(--brand-01);
255
+
256
+ .tabLabel svg {
257
+ fill: var(--brand-01);
258
+ }
259
+ }
260
+ }
261
+
262
+ /* Icon, name and count inside a tab */
263
+ .tabLabel {
264
+ display: inline-flex;
265
+ align-items: center;
266
+ gap: layout.$spacing-03;
267
+
268
+ svg {
269
+ fill: $text-02;
270
+ }
271
+ }
272
+
273
+ .tabCount {
274
+ margin: 0;
275
+ }
276
+
277
+ /* Each panel keeps the spacing its content already brings */
278
+ .tabPanel {
279
+ padding: 0;
280
+ }
281
+
219
282
  .emptyState {
220
283
  display: flex;
221
284
  flex-direction: column;
@@ -242,31 +305,53 @@ $tones: (
242
305
  }
243
306
 
244
307
  @media (max-width: $breakpoint-tablet-max) {
308
+ /* the gutter `.page` uses at this size */
245
309
  .dashboard {
246
- padding: layout.$spacing-05 0;
247
- }
310
+ --page-gutter: #{layout.$spacing-05};
248
311
 
249
- .headerMeta {
250
- align-items: flex-start;
251
- flex-wrap: wrap;
252
- margin-left: 0;
312
+ padding-bottom: layout.$spacing-05;
253
313
  }
254
314
 
255
- .toolbar {
256
- flex-wrap: wrap;
315
+ .toolbar,
316
+ .grid {
317
+ grid-template-columns: repeat(2, 1fr);
257
318
  }
258
319
 
320
+ /* two columns: one and a half tiles, leaving the count room beside it */
259
321
  .searchLayer {
260
- max-width: none;
261
- }
262
-
263
- .grid {
264
- grid-template-columns: repeat(2, 1fr);
322
+ grid-column: 1;
323
+ width: 150%;
265
324
  }
266
325
  }
267
326
 
268
327
  @media (max-width: $breakpoint-phone-max) {
328
+ /* no room beside the tabs: the facility and date sit above them, and the
329
+ white band grows to hold both */
330
+ .headerMeta {
331
+ /* in the flow, but still painted over the band */
332
+ position: relative;
333
+ top: auto;
334
+ right: auto;
335
+ height: 1.125rem;
336
+ margin-bottom: layout.$spacing-03;
337
+ }
338
+
339
+ .tabsArea::before {
340
+ height: calc(#{layout.$spacing-05} + 1.125rem + #{layout.$spacing-03} + #{layout.$spacing-08});
341
+ }
342
+
343
+ .toolbar,
269
344
  .grid {
270
345
  grid-template-columns: 1fr;
271
346
  }
347
+
348
+ /* one column: full width, with the count under the search box */
349
+ .searchLayer {
350
+ grid-column: 1;
351
+ width: 100%;
352
+ }
353
+
354
+ .resultCount {
355
+ justify-self: start;
356
+ }
272
357
  }
@@ -0,0 +1,392 @@
1
+ import React from 'react';
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';
14
+
15
+ interface Moh362LabRegisterProps {
16
+ rows: Moh362Row[];
17
+ }
18
+
19
+ const COLUMN_IDS = 'abcdefghijklmnopqr'.split('');
20
+
21
+ const count = (rows: Moh362Row[], test: (row: Moh362Row) => boolean) => rows.filter(test).length;
22
+
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 }) => (
25
+ <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>
32
+ </th>
33
+ );
34
+
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(' · ');
43
+ return (
44
+ <td rowSpan={2} className={styles.testCell}>
45
+ <strong>{testResult(result)}</strong>
46
+ {detail && <span className={styles.testDetail}>{detail}</span>}
47
+ </td>
48
+ );
49
+ };
50
+
51
+ /** A Tests (n) / Positive (n) block: age bands by sex, with totals */
52
+ const AgeSexSummary: React.FC<{ title: string; rows: Moh362Row[] }> = ({ title, rows }) => {
53
+ const sexes: Array<{ label: string; code: string }> = [
54
+ { label: 'Male', code: 'M' },
55
+ { label: 'Female', code: 'F' },
56
+ ];
57
+ return (
58
+ <table className={styles.summaryTable}>
59
+ <thead>
60
+ <tr>
61
+ <th className={styles.summaryLabel}>{title}</th>
62
+ {AGE_BANDS.map((band) => (
63
+ <th key={band.label}>{band.label}</th>
64
+ ))}
65
+ <th>Total</th>
66
+ </tr>
67
+ </thead>
68
+ <tbody>
69
+ {sexes.map((sex) => {
70
+ const ofSex = rows.filter((row) => row.sex === sex.code);
71
+ return (
72
+ <tr key={sex.code}>
73
+ <th className={styles.summaryLabel}>{sex.label}</th>
74
+ {AGE_BANDS.map((band) => (
75
+ <td key={band.label}>{count(ofSex, (row) => inBand(row, band))}</td>
76
+ ))}
77
+ <td>{ofSex.length}</td>
78
+ </tr>
79
+ );
80
+ })}
81
+ <tr>
82
+ <th className={styles.summaryLabel}>Total</th>
83
+ {AGE_BANDS.map((band) => (
84
+ <td key={band.label}>{count(rows, (row) => inBand(row, band))}</td>
85
+ ))}
86
+ <td>{rows.length}</td>
87
+ </tr>
88
+ </tbody>
89
+ </table>
90
+ );
91
+ };
92
+
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') },
111
+ ];
112
+
113
+ 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>
119
+ </div>
120
+ <h4 className={styles.sheetTitle}>MOH 362: HTS LAB REGISTER</h4>
121
+
122
+ <div className={styles.tableScroll}>
123
+ <table className={styles.register}>
124
+ <thead>
125
+ <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>
228
+ ))}
229
+ </tr>
230
+ </thead>
231
+ <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>
347
+ {tests.map((test) => (
348
+ <React.Fragment key={test.label}>
349
+ <th className={styles.summaryLabel}>{test.label}</th>
350
+ <th>Number</th>
351
+ </React.Fragment>
352
+ ))}
353
+ </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>
378
+ </div>
379
+
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>
387
+ </div>
388
+ </div>
389
+ );
390
+ };
391
+
392
+ export default Moh362LabRegister;