@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.
- package/dist/299.js +2 -0
- package/dist/299.js.LICENSE.txt +3 -0
- package/dist/299.js.map +1 -0
- package/dist/354.js +2 -0
- package/dist/354.js.LICENSE.txt +20 -0
- package/dist/354.js.map +1 -0
- package/dist/410.js +1 -1
- package/dist/410.js.map +1 -1
- package/dist/457.js +2 -0
- package/dist/457.js.LICENSE.txt +261 -0
- package/dist/457.js.map +1 -0
- package/dist/480.js +1 -0
- package/dist/781.js +1 -0
- package/dist/781.js.map +1 -0
- package/dist/907.js +1 -1
- package/dist/907.js.map +1 -1
- package/dist/953.css +1 -0
- package/dist/953.css.map +1 -0
- package/dist/953.js +1 -0
- package/dist/953.js.map +1 -0
- package/dist/99.js +1 -1
- package/dist/99.js.map +1 -0
- package/dist/ampath-esm-reports-app.js +1 -1
- package/dist/routes.json +1 -1
- package/package.json +3 -1
- package/src/common/linelist-table/linelist-table.component.tsx +1 -1
- package/src/common/report-filters/report-filters.component.tsx +25 -4
- package/src/common/report-filters/types.ts +13 -0
- package/src/common/report-page/download-sheet-pdf.ts +59 -0
- package/src/common/report-page/index.ts +1 -0
- package/src/dashboard/reports-dasboard.tsx +156 -69
- package/src/dashboard/reports-dashboard.module.scss +144 -59
- package/src/registers/moh-362/moh-362-lab-register.component.tsx +392 -0
- package/src/registers/moh-362/moh-362-linkage-register.component.tsx +120 -0
- package/src/registers/moh-362/moh-362-register.scss +222 -0
- package/src/registers/moh-362/moh-362.component.tsx +99 -0
- package/src/registers/moh-362/moh-362.scss +11 -0
- package/src/registers/moh-362/moh-362.types.ts +97 -0
- package/src/registers/register-catalog.ts +30 -0
- package/src/reports/moh-731/linelist/moh-731-indicators.ts +82 -0
- package/src/reports/moh-731/linelist/moh-731-linelist-sections.ts +51 -2
- package/src/reports/moh-731/moh-731.component.tsx +18 -3
- package/src/reports/moh-731/moh-731.scss +6 -0
- package/src/reports/moh-731/sections/hiv-and-tb-treatment.component.tsx +49 -52
- package/src/reports/moh-731/summary/moh-731-summary.config.ts +25 -16
- package/src/resources/moh-362.resource.ts +27 -0
- package/src/root.component.tsx +2 -0
- package/dist/876.css +0 -1
- package/dist/876.css.map +0 -1
- package/dist/876.js +0 -1
- package/dist/876.js.map +0 -1
|
@@ -25,78 +25,93 @@ $tones: (
|
|
|
25
25
|
);
|
|
26
26
|
|
|
27
27
|
.dashboard {
|
|
28
|
-
/*
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
68
|
-
|
|
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
|
-
|
|
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-
|
|
87
|
+
gap: layout.$spacing-04;
|
|
90
88
|
margin-bottom: layout.$spacing-05;
|
|
91
89
|
}
|
|
92
90
|
|
|
93
91
|
.searchLayer {
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
247
|
-
}
|
|
310
|
+
--page-gutter: #{layout.$spacing-05};
|
|
248
311
|
|
|
249
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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) & (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'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;
|