@ampath/esm-reports-app 1.0.0-next.79 → 1.0.0-next.80
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/0.css +1 -0
- package/dist/0.css.map +1 -0
- package/dist/0.js +1 -0
- package/dist/0.js.map +1 -0
- package/dist/410.js +1 -1
- package/dist/410.js.map +1 -1
- package/dist/600.js +1 -1
- package/dist/600.js.map +1 -1
- package/dist/757.js +1 -0
- package/dist/757.js.map +1 -0
- package/dist/ampath-esm-reports-app.js +1 -1
- package/dist/ampath-esm-reports-app.js.map +1 -1
- package/dist/routes.json +1 -1
- package/package.json +1 -1
- package/src/common/report-filters/report-filters.scss +15 -6
- package/src/common/report-page/download-sheet-pdf.ts +193 -27
- package/src/common/report-register/report-parents.ts +2 -0
- package/src/dashboard/report-catalog.ts +18 -0
- package/src/registers/moh-267/moh-267-register.component.tsx +407 -0
- package/src/registers/moh-267/moh-267-register.scss +298 -0
- package/src/registers/moh-267/moh-267-summary.ts +102 -0
- package/src/registers/moh-267/moh-267.component.tsx +94 -0
- package/src/registers/moh-267/moh-267.types.ts +47 -0
- package/src/registers/moh-362/moh-362-lab-register.component.tsx +469 -310
- package/src/registers/moh-362/moh-362-lab-register.scss +412 -0
- package/src/registers/moh-362/moh-362-linkage-register.component.tsx +188 -93
- package/src/registers/moh-362/moh-362-linkage-register.scss +91 -0
- package/src/registers/moh-362/moh-362.component.tsx +33 -15
- package/src/registers/moh-362/moh-362.types.ts +15 -25
- package/src/registers/presumptive-tb/presumptive-tb-register.component.tsx +391 -0
- package/src/registers/presumptive-tb/presumptive-tb-register.scss +262 -0
- package/src/registers/presumptive-tb/presumptive-tb.component.tsx +92 -0
- package/src/registers/presumptive-tb/presumptive-tb.types.ts +88 -0
- package/src/registers/register-catalog.ts +37 -3
- package/src/registers/register-file-name.ts +18 -0
- package/src/registers/tb4/tb4-parts.tsx +164 -0
- package/src/registers/tb4/tb4-patient-sheet.component.tsx +418 -0
- package/src/registers/tb4/tb4-register.scss +282 -0
- package/src/registers/tb4/tb4-treatment-sheet.component.tsx +349 -0
- package/src/registers/tb4/tb4.component.tsx +120 -0
- package/src/registers/tb4/tb4.types.ts +129 -0
- package/src/reports/acf-summary/acf-summary-form.component.tsx +176 -0
- package/src/reports/acf-summary/acf-summary-linelist.component.tsx +86 -0
- package/src/reports/acf-summary/acf-summary.component.tsx +97 -0
- package/src/reports/acf-summary/acf-summary.counts.ts +37 -0
- package/src/reports/acf-summary/acf-summary.scss +162 -0
- package/src/reports/moh-729b/moh-729b-form.component.tsx +385 -0
- package/src/reports/moh-729b/moh-729b-linelist.component.tsx +205 -0
- package/src/reports/moh-729b/moh-729b-summary.component.tsx +258 -0
- package/src/reports/moh-729b/moh-729b-summary.config.ts +229 -0
- package/src/reports/moh-729b/moh-729b.component.tsx +157 -0
- package/src/reports/moh-729b/moh-729b.config.ts +235 -0
- package/src/reports/moh-729b/moh-729b.scss +276 -0
- package/src/reports/moh-729b/moh-729b.types.ts +111 -0
- package/src/reports/moh-731/moh-731.component.tsx +10 -7
- package/src/reports/moh-731/moh-731.scss +3 -6
- package/src/reports/moh-731/summary/moh-731-summary.config.ts +2 -4
- package/src/reports/moh-731/summary/moh-731-summary.scss +5 -0
- package/src/resources/moh-267.resource.ts +27 -0
- package/src/resources/moh-729b.resource.ts +25 -0
- package/src/resources/presumptive-tb.resource.ts +27 -0
- package/src/root.component.tsx +14 -0
- package/dist/177.css +0 -1
- package/dist/177.css.map +0 -1
- package/dist/177.js +0 -1
- package/dist/177.js.map +0 -1
- package/dist/580.js +0 -1
- package/dist/580.js.map +0 -1
- package/src/registers/moh-362/moh-362-register.scss +0 -222
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
import React, { createContext, useContext } from 'react';
|
|
2
|
+
import dayjs from 'dayjs';
|
|
3
|
+
|
|
4
|
+
import { usePageScale } from '../../common/report-page';
|
|
5
|
+
import {
|
|
6
|
+
ADULT_ART,
|
|
7
|
+
HEPATITIS_B,
|
|
8
|
+
INFANT_AGES,
|
|
9
|
+
OPPORTUNISTIC_INFECTIONS,
|
|
10
|
+
PAEDIATRIC_ART,
|
|
11
|
+
PEP,
|
|
12
|
+
PMTCT_INFANTS,
|
|
13
|
+
PMTCT_MOTHERS,
|
|
14
|
+
PREP,
|
|
15
|
+
WEIGHT_BANDS,
|
|
16
|
+
type Regimen,
|
|
17
|
+
type TotalledGroup,
|
|
18
|
+
TPT_HIV_POSITIVE,
|
|
19
|
+
} from './moh-729b.config';
|
|
20
|
+
import { type Moh729bCell } from './moh-729b.types';
|
|
21
|
+
import styles from './moh-729b.scss';
|
|
22
|
+
|
|
23
|
+
/** A3 portrait less its 8mm margins: the page as drawn on screen, inside the report card */
|
|
24
|
+
const PAGE_WIDTH_MM = 297 - 2 * 8;
|
|
25
|
+
|
|
26
|
+
/** The form's measuring unit (row heights in the config) in millimetres */
|
|
27
|
+
const MM_PER_UNIT = 0.144;
|
|
28
|
+
const height = (units: number): React.CSSProperties => ({ height: `${units * MM_PER_UNIT}mm` });
|
|
29
|
+
|
|
30
|
+
/** Column widths, measured off the form, as fractions of each half */
|
|
31
|
+
const LEFT_COLUMNS = [79, 212, 77, 77, 76, 77, 77, 77, 80, 75];
|
|
32
|
+
const RIGHT_COLUMNS = [83, 342, 84, 84, 84, 84, 84, 84];
|
|
33
|
+
|
|
34
|
+
const ColumnWidths: React.FC<{ widths: number[] }> = ({ widths }) => {
|
|
35
|
+
const total = widths.reduce((sum, width) => sum + width, 0);
|
|
36
|
+
return (
|
|
37
|
+
<colgroup>
|
|
38
|
+
{widths.map((width, index) => (
|
|
39
|
+
<col key={index} style={{ width: `${(width / total) * 100}%` }} />
|
|
40
|
+
))}
|
|
41
|
+
</colgroup>
|
|
42
|
+
);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/** The column headings both halves start with, and the half's first band */
|
|
46
|
+
const HalfHead: React.FC<{ span: number; band: string }> = ({ span, band }) => (
|
|
47
|
+
<thead>
|
|
48
|
+
<tr className={styles.columnHead} style={height(40)}>
|
|
49
|
+
<th>Regimen Code</th>
|
|
50
|
+
<th>ARV or OI Treatment Regimen</th>
|
|
51
|
+
<th colSpan={span}>
|
|
52
|
+
Number of Current Active Patients/Clients on this regimen at the end of this Reporting period
|
|
53
|
+
</th>
|
|
54
|
+
</tr>
|
|
55
|
+
<tr className={styles.band} style={height(30)}>
|
|
56
|
+
<th colSpan={span + 2}>{band}</th>
|
|
57
|
+
</tr>
|
|
58
|
+
</thead>
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
const RegimenCells: React.FC<{ regimen: Regimen }> = ({ regimen: [code, name] }) => (
|
|
62
|
+
<>
|
|
63
|
+
<td className={styles.code}>{code}</td>
|
|
64
|
+
<td>{name}</td>
|
|
65
|
+
</>
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
/** The form's counts, and what a count opens; absent while the form is blank */
|
|
69
|
+
export interface Moh729bCounts {
|
|
70
|
+
count: (cell: Moh729bCell) => number;
|
|
71
|
+
open: (cell: Moh729bCell) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const CountsContext = createContext<Moh729bCounts | null>(null);
|
|
75
|
+
|
|
76
|
+
/** A count on the form, opening the linelist of its clients when clicked */
|
|
77
|
+
const Count: React.FC<{ cell: Moh729bCell; colSpan?: number }> = ({ cell, colSpan }) => {
|
|
78
|
+
const counts = useContext(CountsContext);
|
|
79
|
+
if (!counts) {
|
|
80
|
+
return <td colSpan={colSpan} />;
|
|
81
|
+
}
|
|
82
|
+
return (
|
|
83
|
+
<td
|
|
84
|
+
colSpan={colSpan}
|
|
85
|
+
className={styles.count}
|
|
86
|
+
title="Click to open linelist"
|
|
87
|
+
role="button"
|
|
88
|
+
tabIndex={0}
|
|
89
|
+
onClick={() => counts.open(cell)}
|
|
90
|
+
onKeyDown={(event) => (event.key === 'Enter' || event.key === ' ') && counts.open(cell)}
|
|
91
|
+
>
|
|
92
|
+
{counts.count(cell)}
|
|
93
|
+
</td>
|
|
94
|
+
);
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const blanks = (count: number, className?: string) =>
|
|
98
|
+
Array.from({ length: count }, (_, index) => <td key={index} className={className} />);
|
|
99
|
+
|
|
100
|
+
/** ART: adults by line with a single Total, children by weight band, then PrEP */
|
|
101
|
+
const ArtHalf: React.FC = () => {
|
|
102
|
+
/* the adults' shaded block runs unbroken beside all three lines */
|
|
103
|
+
const adultRows = ADULT_ART.reduce((count, section) => count + section.rows.length + 1, 0);
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
<table className={styles.grid}>
|
|
107
|
+
<ColumnWidths widths={LEFT_COLUMNS} />
|
|
108
|
+
<HalfHead span={8} band="ADULT ART" />
|
|
109
|
+
<tbody>
|
|
110
|
+
{ADULT_ART.map((section, sectionIndex) => (
|
|
111
|
+
<React.Fragment key={section.title}>
|
|
112
|
+
<tr className={styles.heading} style={height(section.h)}>
|
|
113
|
+
<th colSpan={2}>{section.title}</th>
|
|
114
|
+
<th colSpan={3}>Total</th>
|
|
115
|
+
{sectionIndex === 0 && <td colSpan={5} rowSpan={adultRows} className={styles.shade} />}
|
|
116
|
+
</tr>
|
|
117
|
+
{section.rows.map((regimen) => (
|
|
118
|
+
<tr key={regimen[0]} style={height(regimen[2] ?? 39.6)}>
|
|
119
|
+
<RegimenCells regimen={regimen} />
|
|
120
|
+
<Count colSpan={3} cell={{ code: regimen[0] }} />
|
|
121
|
+
</tr>
|
|
122
|
+
))}
|
|
123
|
+
</React.Fragment>
|
|
124
|
+
))}
|
|
125
|
+
|
|
126
|
+
<tr className={styles.band} style={height(30)}>
|
|
127
|
+
<th colSpan={10}>PAEDIATRIC ART</th>
|
|
128
|
+
</tr>
|
|
129
|
+
{PAEDIATRIC_ART.map((section) => (
|
|
130
|
+
<React.Fragment key={section.title}>
|
|
131
|
+
<tr className={styles.heading} style={height(section.h)}>
|
|
132
|
+
<th colSpan={2}>{section.title}</th>
|
|
133
|
+
{[...WEIGHT_BANDS, 'Total'].map((band) => (
|
|
134
|
+
<th key={band} className={styles.small}>
|
|
135
|
+
{band}
|
|
136
|
+
</th>
|
|
137
|
+
))}
|
|
138
|
+
</tr>
|
|
139
|
+
{section.rows.map((regimen) => (
|
|
140
|
+
<tr key={regimen[0]} style={height(regimen[2] ?? 41)}>
|
|
141
|
+
<RegimenCells regimen={regimen} />
|
|
142
|
+
{WEIGHT_BANDS.map((band) => (
|
|
143
|
+
<Count key={band} cell={{ code: regimen[0], band }} />
|
|
144
|
+
))}
|
|
145
|
+
<Count cell={{ code: regimen[0] }} />
|
|
146
|
+
</tr>
|
|
147
|
+
))}
|
|
148
|
+
</React.Fragment>
|
|
149
|
+
))}
|
|
150
|
+
|
|
151
|
+
<tr className={styles.heading} style={height(PREP.h)}>
|
|
152
|
+
<th colSpan={2} className={styles.bandCell}>
|
|
153
|
+
{PREP.title}
|
|
154
|
+
</th>
|
|
155
|
+
<th className={styles.leftText}>Total</th>
|
|
156
|
+
{blanks(7, styles.shade)}
|
|
157
|
+
</tr>
|
|
158
|
+
{PREP.rows.map((regimen) => (
|
|
159
|
+
<tr key={regimen[0]} style={height(regimen[2] ?? 35.4)}>
|
|
160
|
+
<RegimenCells regimen={regimen} />
|
|
161
|
+
<Count cell={{ code: regimen[0] }} />
|
|
162
|
+
{blanks(7, styles.shade)}
|
|
163
|
+
</tr>
|
|
164
|
+
))}
|
|
165
|
+
</tbody>
|
|
166
|
+
</table>
|
|
167
|
+
);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* A numbered group of the right half: its heading with Total, then a count and
|
|
172
|
+
* a Total per regimen, the columns between shaded as one block. `extraRows`
|
|
173
|
+
* lengthens the block over rows that follow the group (Hepatitis B's).
|
|
174
|
+
* `isCounted` fills each regimen's Total from the data (PEP, Hepatitis B and TPT HIV Positive).
|
|
175
|
+
*/
|
|
176
|
+
const TotalledRows: React.FC<{ group: TotalledGroup; extraRows?: number; isCounted?: boolean }> = ({
|
|
177
|
+
group,
|
|
178
|
+
extraRows = 0,
|
|
179
|
+
isCounted = false,
|
|
180
|
+
}) => (
|
|
181
|
+
<>
|
|
182
|
+
<tr className={styles.heading} style={height(group.h)}>
|
|
183
|
+
<td className={styles.code}>{group.number}</td>
|
|
184
|
+
<th colSpan={5} className={styles.leftText}>
|
|
185
|
+
{group.title}
|
|
186
|
+
</th>
|
|
187
|
+
{group.noTotal ? <td colSpan={2} className={styles.shade} /> : <th colSpan={2}>Total</th>}
|
|
188
|
+
</tr>
|
|
189
|
+
{group.rows.map((regimen, index) => (
|
|
190
|
+
<tr key={regimen[0]} style={height(regimen[2] ?? 40)}>
|
|
191
|
+
<RegimenCells regimen={regimen} />
|
|
192
|
+
<td />
|
|
193
|
+
{index === 0 && <td colSpan={3} rowSpan={group.rows.length + extraRows} className={styles.shade} />}
|
|
194
|
+
{isCounted ? <Count colSpan={2} cell={{ code: regimen[0] }} /> : <td colSpan={2} />}
|
|
195
|
+
</tr>
|
|
196
|
+
))}
|
|
197
|
+
</>
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
/** PMTCT for mothers and infants, PEP, Hepatitis B and the OIs */
|
|
201
|
+
const PmtctHalf: React.FC = () => (
|
|
202
|
+
<table className={styles.grid}>
|
|
203
|
+
<ColumnWidths widths={RIGHT_COLUMNS} />
|
|
204
|
+
<HalfHead span={6} band="PMTCT" />
|
|
205
|
+
<tbody>
|
|
206
|
+
<tr className={styles.heading} style={height(PMTCT_MOTHERS.h)}>
|
|
207
|
+
<td className={styles.code}>1</td>
|
|
208
|
+
<th className={styles.italicHeading}>{PMTCT_MOTHERS.title}</th>
|
|
209
|
+
<th>1st Line</th>
|
|
210
|
+
<th>2nd Line</th>
|
|
211
|
+
<th>3rd Line</th>
|
|
212
|
+
<th colSpan={3}>Total</th>
|
|
213
|
+
</tr>
|
|
214
|
+
{PMTCT_MOTHERS.rows.map((regimen) => (
|
|
215
|
+
<tr key={regimen[0]} style={height(39.6)}>
|
|
216
|
+
<RegimenCells regimen={regimen} />
|
|
217
|
+
{[1, 2, 3].map((line) => (
|
|
218
|
+
<Count key={line} cell={{ code: regimen[0], line }} />
|
|
219
|
+
))}
|
|
220
|
+
<td colSpan={3} className={styles.shade} />
|
|
221
|
+
</tr>
|
|
222
|
+
))}
|
|
223
|
+
<tr style={height(39)}>
|
|
224
|
+
{blanks(5, styles.lightShade)}
|
|
225
|
+
<td colSpan={3} className={styles.shade} />
|
|
226
|
+
</tr>
|
|
227
|
+
|
|
228
|
+
<tr className={styles.heading} style={height(PMTCT_INFANTS.h)}>
|
|
229
|
+
<td className={styles.code}>2</td>
|
|
230
|
+
<th className={styles.leftText}>{PMTCT_INFANTS.title}</th>
|
|
231
|
+
{[...INFANT_AGES, 'Total'].map((age) => (
|
|
232
|
+
<th key={age} className={styles.small}>
|
|
233
|
+
{age}
|
|
234
|
+
</th>
|
|
235
|
+
))}
|
|
236
|
+
</tr>
|
|
237
|
+
{PMTCT_INFANTS.rows.map((regimen) => (
|
|
238
|
+
<tr key={regimen[0]} style={height(regimen[2] ?? 38)}>
|
|
239
|
+
<RegimenCells regimen={regimen} />
|
|
240
|
+
{blanks(5)}
|
|
241
|
+
<td className={styles.shade} />
|
|
242
|
+
</tr>
|
|
243
|
+
))}
|
|
244
|
+
|
|
245
|
+
<tr className={styles.band} style={height(38)}>
|
|
246
|
+
<td className={styles.blankCell} />
|
|
247
|
+
<th colSpan={7}>PEP</th>
|
|
248
|
+
</tr>
|
|
249
|
+
{PEP.map((group) => (
|
|
250
|
+
<TotalledRows key={group.title} group={group} isCounted />
|
|
251
|
+
))}
|
|
252
|
+
|
|
253
|
+
<TotalledRows group={HEPATITIS_B} extraRows={1} isCounted />
|
|
254
|
+
<tr style={height(30)}>
|
|
255
|
+
<td colSpan={3} className={styles.shade} />
|
|
256
|
+
<td colSpan={2} />
|
|
257
|
+
</tr>
|
|
258
|
+
|
|
259
|
+
<tr className={styles.band} style={height(40)}>
|
|
260
|
+
<td className={styles.blankCell} />
|
|
261
|
+
<th colSpan={7}>Management of Opportunistic infections (OIs)</th>
|
|
262
|
+
</tr>
|
|
263
|
+
{OPPORTUNISTIC_INFECTIONS.map((group) => (
|
|
264
|
+
<TotalledRows key={group.title} group={group} isCounted={group.title === TPT_HIV_POSITIVE} />
|
|
265
|
+
))}
|
|
266
|
+
</tbody>
|
|
267
|
+
</table>
|
|
268
|
+
);
|
|
269
|
+
|
|
270
|
+
/** A label and the ruled line it is written on */
|
|
271
|
+
const Field: React.FC<{ label: React.ReactNode; value?: string; caption?: string }> = ({ label, value, caption }) => (
|
|
272
|
+
<>
|
|
273
|
+
<span className={styles.fieldLabel}>{label}</span>
|
|
274
|
+
<span className={caption ? `${styles.fieldLine} ${styles.captioned}` : styles.fieldLine}>
|
|
275
|
+
{value}
|
|
276
|
+
{caption && <span className={styles.caption}>{caption}</span>}
|
|
277
|
+
</span>
|
|
278
|
+
</>
|
|
279
|
+
);
|
|
280
|
+
|
|
281
|
+
const SIGN_OFF = ['Signature:', 'Date:', 'Designation:', 'Contact Telephone:'];
|
|
282
|
+
|
|
283
|
+
interface Moh729bFormProps {
|
|
284
|
+
facility: string;
|
|
285
|
+
startDate: string;
|
|
286
|
+
endDate: string;
|
|
287
|
+
/** The counts of the ART and PMTCT regimens; without them the form is drawn blank */
|
|
288
|
+
counts?: Moh729bCounts;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* MoH 729B Facility Monthly ARV Patient Summary (F-MAPS), Ver. July 2023: the
|
|
293
|
+
* report page of the form as one A3 portrait sheet, ART on the left and PMTCT,
|
|
294
|
+
* PEP, Hepatitis B and the OIs on the right, with the shaded cells as printed.
|
|
295
|
+
* The ART regimens (adult, paediatric and PMTCT mothers) are counted from the
|
|
296
|
+
* clients active on ART in the month, and PrEP from the clients on PrEP in the
|
|
297
|
+
* month by MOH 731's PrEP section, and PEP from the clients given PEP in the
|
|
298
|
+
* month by its PEP section, and Hepatitis B from the HIV-negative clients its
|
|
299
|
+
* HBV screening at ANC finds positive, and TPT HIV Positive from its 3.4
|
|
300
|
+
* Starting TPT by the drug started; infants, universal prophylaxis, TPT HIV
|
|
301
|
+
* Negative and Cryptococcal Meningitis stay blank until their queries are added.
|
|
302
|
+
*/
|
|
303
|
+
const Moh729bForm: React.FC<Moh729bFormProps> = ({ facility, startDate, endDate, counts }) => {
|
|
304
|
+
const { ref, scale } = usePageScale<HTMLDivElement>(PAGE_WIDTH_MM);
|
|
305
|
+
const formDate = (value: string) => (value ? dayjs(value).format('DD/MM/YYYY') : '');
|
|
306
|
+
|
|
307
|
+
return (
|
|
308
|
+
<div className={styles.viewport} ref={ref}>
|
|
309
|
+
<section className={styles.page} data-register-page style={{ '--page-zoom': scale } as React.CSSProperties}>
|
|
310
|
+
<div className={styles.titleLine}>
|
|
311
|
+
<span>Ministry of Health</span>
|
|
312
|
+
<span>FACILITY MONTHLY ARV PATIENT SUMMARY (F-MAPS) Report (MoH 729B)</span>
|
|
313
|
+
<span>Ver. July 2023</span>
|
|
314
|
+
</div>
|
|
315
|
+
<div className={styles.formCode}>MoH 729B</div>
|
|
316
|
+
|
|
317
|
+
<div className={styles.fields}>
|
|
318
|
+
<div className={styles.fieldGrid}>
|
|
319
|
+
<Field label="Facility Name:" value={facility} />
|
|
320
|
+
<Field label="County:" />
|
|
321
|
+
</div>
|
|
322
|
+
<div className={styles.fieldGrid}>
|
|
323
|
+
<Field label="Facility (MFL) code:" />
|
|
324
|
+
<Field label="Sub-County:" />
|
|
325
|
+
</div>
|
|
326
|
+
</div>
|
|
327
|
+
<div className={styles.period}>
|
|
328
|
+
<Field
|
|
329
|
+
label={
|
|
330
|
+
<>
|
|
331
|
+
Period of Reporting:
|
|
332
|
+
<br />
|
|
333
|
+
Beginning:
|
|
334
|
+
</>
|
|
335
|
+
}
|
|
336
|
+
value={formDate(startDate)}
|
|
337
|
+
caption="(Day/Month/Year)"
|
|
338
|
+
/>
|
|
339
|
+
<Field label="Ending:" value={formDate(endDate)} caption="(Day/Month/Year)" />
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<CountsContext.Provider value={counts ?? null}>
|
|
343
|
+
<div className={styles.halves}>
|
|
344
|
+
<div className={styles.half} style={{ flexGrow: LEFT_COLUMNS.reduce((a, b) => a + b, 0) }}>
|
|
345
|
+
<ArtHalf />
|
|
346
|
+
</div>
|
|
347
|
+
<div className={styles.half} style={{ flexGrow: RIGHT_COLUMNS.reduce((a, b) => a + b, 0) }}>
|
|
348
|
+
<PmtctHalf />
|
|
349
|
+
<div className={styles.filler} />
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</CountsContext.Provider>
|
|
353
|
+
|
|
354
|
+
<p className={styles.instruction}>
|
|
355
|
+
1. For each Regimen, sum the Number of Patients/Clients from the MoH367A (DAR for ARV and OI medicines) placed
|
|
356
|
+
at ALL the ART Dispensing points in your Health Facility to get the Total numbers by Regimen for the Facility
|
|
357
|
+
<br />
|
|
358
|
+
This information is important for purposes of planning your stock orders and forecasting your requirements.
|
|
359
|
+
</p>
|
|
360
|
+
|
|
361
|
+
<div className={styles.signOff}>
|
|
362
|
+
{[
|
|
363
|
+
['Report prepared by:', 'Name of Reporting Officer'],
|
|
364
|
+
['Report approved by:', 'Name of Approving officer'],
|
|
365
|
+
].map(([label, caption]) => (
|
|
366
|
+
<div key={label} className={styles.fieldGrid}>
|
|
367
|
+
<Field label={label} caption={caption} />
|
|
368
|
+
{SIGN_OFF.map((line) => (
|
|
369
|
+
<Field key={line} label={line} />
|
|
370
|
+
))}
|
|
371
|
+
</div>
|
|
372
|
+
))}
|
|
373
|
+
</div>
|
|
374
|
+
<p className={styles.notes}>Notes:</p>
|
|
375
|
+
|
|
376
|
+
<div className={styles.pageFoot}>
|
|
377
|
+
<span>For Satellite sites Central site Dispensing points</span>
|
|
378
|
+
<span>Ver. July 2023</span>
|
|
379
|
+
</div>
|
|
380
|
+
</section>
|
|
381
|
+
</div>
|
|
382
|
+
);
|
|
383
|
+
};
|
|
384
|
+
|
|
385
|
+
export default Moh729bForm;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useSearchParams } from 'react-router-dom';
|
|
3
|
+
|
|
4
|
+
import { type LinelistColumn, LinelistTable, useAllRows } from '../../common/linelist-table';
|
|
5
|
+
import { RegisterLayout, useLocationName } from '../../common/report-register';
|
|
6
|
+
import { getMoh729b } from '../../resources/moh-729b.resource';
|
|
7
|
+
import { decodeCell, describeCell, inCells, PEP_CODES, type Moh729bCell, type Moh729bRow } from './moh-729b.types';
|
|
8
|
+
|
|
9
|
+
const formatDate = (value: unknown) => (value ? new Date(String(value)).toLocaleDateString('en-GB') : '');
|
|
10
|
+
|
|
11
|
+
/** Clients on ART */
|
|
12
|
+
const ART_COLUMNS: LinelistColumn[] = [
|
|
13
|
+
{ header: 'Name', field: 'full_name' },
|
|
14
|
+
{ header: 'CCC Number', field: 'ccc_number', text: true },
|
|
15
|
+
{ header: 'AMRS Number', field: 'amrs_number', text: true },
|
|
16
|
+
{ header: 'Sex', field: 'gender' },
|
|
17
|
+
{ header: 'Age', field: 'age' },
|
|
18
|
+
{ header: 'Weight (kg)', field: 'weight' },
|
|
19
|
+
{ header: 'Regimen', field: 'regimen', wrap: true },
|
|
20
|
+
{ header: 'Regimen Code', field: 'regimen_code' },
|
|
21
|
+
{ header: 'ARV Line', field: 'arv_line' },
|
|
22
|
+
{ header: 'RTC Date', value: (row) => formatDate(row.rtc_date), sortValue: (row) => row.rtc_date },
|
|
23
|
+
/* Available from the Columns menu */
|
|
24
|
+
{ header: 'Pregnant', field: 'pregnant', hidden: true },
|
|
25
|
+
{ header: 'Weight Band', field: 'weight_band', hidden: true },
|
|
26
|
+
{ header: 'ARV Concepts', field: 'cur_arv_meds', text: true, hidden: true },
|
|
27
|
+
{
|
|
28
|
+
header: 'Date of Birth',
|
|
29
|
+
value: (row) => formatDate(row.birthdate),
|
|
30
|
+
sortValue: (row) => row.birthdate,
|
|
31
|
+
hidden: true,
|
|
32
|
+
},
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
/** Clients on PrEP */
|
|
36
|
+
const PREP_COLUMNS: LinelistColumn[] = [
|
|
37
|
+
{ header: 'Name', field: 'full_name' },
|
|
38
|
+
{ header: 'PrEP Number', field: 'prep_number', text: true },
|
|
39
|
+
{ header: 'AMRS Number', field: 'amrs_number', text: true },
|
|
40
|
+
{ header: 'Sex', field: 'gender' },
|
|
41
|
+
{ header: 'Age', field: 'age' },
|
|
42
|
+
{ header: 'PrEP Regimen', field: 'regimen', wrap: true },
|
|
43
|
+
{ header: 'Regimen Code', field: 'regimen_code' },
|
|
44
|
+
{ header: 'PrEP Status', field: 'prep_status' },
|
|
45
|
+
{
|
|
46
|
+
header: 'Last Visit',
|
|
47
|
+
value: (row) => formatDate(row.last_visit_date),
|
|
48
|
+
sortValue: (row) => row.last_visit_date,
|
|
49
|
+
},
|
|
50
|
+
/* Available from the Columns menu */
|
|
51
|
+
{
|
|
52
|
+
header: 'Date of Birth',
|
|
53
|
+
value: (row) => formatDate(row.birthdate),
|
|
54
|
+
sortValue: (row) => row.birthdate,
|
|
55
|
+
hidden: true,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
/** Clients given PEP */
|
|
60
|
+
const PEP_COLUMNS: LinelistColumn[] = [
|
|
61
|
+
{ header: 'Name', field: 'full_name' },
|
|
62
|
+
{ header: 'CCC Number', field: 'ccc_number', text: true },
|
|
63
|
+
{ header: 'AMRS Number', field: 'amrs_number', text: true },
|
|
64
|
+
{ header: 'Sex', field: 'gender' },
|
|
65
|
+
{ header: 'Age', field: 'age' },
|
|
66
|
+
{
|
|
67
|
+
header: 'Exposure Date',
|
|
68
|
+
value: (row) => formatDate(row.exposure_date),
|
|
69
|
+
sortValue: (row) => row.exposure_date,
|
|
70
|
+
},
|
|
71
|
+
{ header: 'Exposure Type', field: 'exposure_type', wrap: true },
|
|
72
|
+
{ header: 'PEP Regimen', field: 'regimen', wrap: true },
|
|
73
|
+
{ header: 'Regimen Code', field: 'regimen_code' },
|
|
74
|
+
/* Available from the Columns menu */
|
|
75
|
+
{ header: 'ARV Concepts', field: 'cur_arv_meds', text: true, hidden: true },
|
|
76
|
+
];
|
|
77
|
+
|
|
78
|
+
/** Hepatitis B patients who are HIV-ve */
|
|
79
|
+
const HBV_COLUMNS: LinelistColumn[] = [
|
|
80
|
+
{ header: 'Name', field: 'full_name' },
|
|
81
|
+
{ header: 'AMRS Number', field: 'amrs_number', text: true },
|
|
82
|
+
{ header: 'Sex', field: 'gender' },
|
|
83
|
+
{ header: 'Age', field: 'age' },
|
|
84
|
+
{ header: 'ANC Visit', value: (row) => formatDate(row.anc_date), sortValue: (row) => row.anc_date },
|
|
85
|
+
{ header: 'HIV Result', field: 'hiv_result' },
|
|
86
|
+
{ header: 'HBV Result', field: 'hbv_result' },
|
|
87
|
+
{
|
|
88
|
+
header: 'HBV Test Date',
|
|
89
|
+
value: (row) => formatDate(row.hbv_test_date),
|
|
90
|
+
sortValue: (row) => row.hbv_test_date,
|
|
91
|
+
},
|
|
92
|
+
{ header: 'Hep B Regimen', field: 'regimen' },
|
|
93
|
+
{ header: 'Regimen Code', field: 'regimen_code' },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
/** Clients on ART who started TPT */
|
|
97
|
+
const TPT_COLUMNS: LinelistColumn[] = [
|
|
98
|
+
{ header: 'Name', field: 'full_name' },
|
|
99
|
+
{ header: 'CCC Number', field: 'ccc_number', text: true },
|
|
100
|
+
{ header: 'AMRS Number', field: 'amrs_number', text: true },
|
|
101
|
+
{ header: 'Sex', field: 'gender' },
|
|
102
|
+
{ header: 'Age', field: 'age' },
|
|
103
|
+
{
|
|
104
|
+
header: 'ART Start Date',
|
|
105
|
+
value: (row) => formatDate(row.arv_first_regimen_start_date),
|
|
106
|
+
sortValue: (row) => row.arv_first_regimen_start_date,
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
header: 'TB Screening Date',
|
|
110
|
+
value: (row) => formatDate(row.tb_screening_date),
|
|
111
|
+
sortValue: (row) => row.tb_screening_date,
|
|
112
|
+
},
|
|
113
|
+
{ header: 'TB Screening Result', field: 'tb_screening_result' },
|
|
114
|
+
{
|
|
115
|
+
header: 'TPT Start Date',
|
|
116
|
+
value: (row) => formatDate(row.ipt_start_date),
|
|
117
|
+
sortValue: (row) => row.ipt_start_date,
|
|
118
|
+
},
|
|
119
|
+
{ header: 'TPT Regimen', field: 'regimen' },
|
|
120
|
+
{ header: 'Regimen Code', field: 'regimen_code' },
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
/** Each group's columns, its saved column layout and what its clients are */
|
|
124
|
+
const GROUPS = {
|
|
125
|
+
art: { columns: ART_COLUMNS, layoutKey: 'moh-729b', title: 'Current active clients on ART' },
|
|
126
|
+
prep: { columns: PREP_COLUMNS, layoutKey: 'moh-729b:prep', title: 'Current active clients on PrEP' },
|
|
127
|
+
pep: { columns: PEP_COLUMNS, layoutKey: 'moh-729b:pep', title: 'Clients given PEP this month' },
|
|
128
|
+
tpt: { columns: TPT_COLUMNS, layoutKey: 'moh-729b:tpt', title: 'HIV positive clients started on TPT this month' },
|
|
129
|
+
hbv: { columns: HBV_COLUMNS, layoutKey: 'moh-729b:hbv', title: 'Hepatitis B patients who are HIV-ve' },
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The clients behind a count on the MOH 729B form or its Summary: the month's
|
|
134
|
+
* clients on ART, PrEP, PEP, Hep B or TPT (the same rows the form is counted
|
|
135
|
+
* from), narrowed to the count's regimen code and, where the form splits it,
|
|
136
|
+
* weight band or line. A total of several counts (`f`, one per count, with a
|
|
137
|
+
* `label`) lists the clients of all of them; its counts never overlap.
|
|
138
|
+
*/
|
|
139
|
+
const Moh729bLinelist: React.FC = () => {
|
|
140
|
+
const [searchParams] = useSearchParams();
|
|
141
|
+
const endDate = searchParams.get('endDate') ?? '';
|
|
142
|
+
const locationUuids = searchParams.get('locationUuids') ?? '';
|
|
143
|
+
const figures = searchParams.getAll('f');
|
|
144
|
+
const cells: Moh729bCell[] = figures.length
|
|
145
|
+
? figures.map(decodeCell)
|
|
146
|
+
: [
|
|
147
|
+
{
|
|
148
|
+
code: searchParams.get('code') ?? '',
|
|
149
|
+
band: searchParams.get('band') ?? undefined,
|
|
150
|
+
line: Number(searchParams.get('line')) || undefined,
|
|
151
|
+
},
|
|
152
|
+
];
|
|
153
|
+
const label = searchParams.get('label');
|
|
154
|
+
const heading = `${label ?? describeCell(cells[0])} linelist`;
|
|
155
|
+
/* the counts of a total are all of one kind, so the first decides the columns */
|
|
156
|
+
const cell = cells[0];
|
|
157
|
+
const group = cell.code.startsWith('PRP')
|
|
158
|
+
? GROUPS.prep
|
|
159
|
+
: cell.code.startsWith('HPB')
|
|
160
|
+
? GROUPS.hbv
|
|
161
|
+
: /^[AC]TPT1[AB]$/.test(cell.code)
|
|
162
|
+
? GROUPS.tpt
|
|
163
|
+
: PEP_CODES.has(cell.code)
|
|
164
|
+
? GROUPS.pep
|
|
165
|
+
: GROUPS.art;
|
|
166
|
+
|
|
167
|
+
const locationName = useLocationName(locationUuids);
|
|
168
|
+
/* the month's clients come back in one response; they are fetched once and
|
|
169
|
+
handed to the table in the batches it reads */
|
|
170
|
+
let clients: Promise<Moh729bRow[]> | undefined;
|
|
171
|
+
const { rows, isLoading } = useAllRows(
|
|
172
|
+
({ startIndex, limit }) => {
|
|
173
|
+
clients ??= getMoh729b({ locationUuids, endDate }).then((all) =>
|
|
174
|
+
(all as unknown as Moh729bRow[]).filter((row) => inCells(row, cells)),
|
|
175
|
+
);
|
|
176
|
+
return clients.then((matching) => ({
|
|
177
|
+
results: matching.slice(startIndex, startIndex + limit),
|
|
178
|
+
total: matching.length,
|
|
179
|
+
isTotalExact: true,
|
|
180
|
+
}));
|
|
181
|
+
},
|
|
182
|
+
[searchParams.toString()],
|
|
183
|
+
);
|
|
184
|
+
|
|
185
|
+
return (
|
|
186
|
+
<RegisterLayout
|
|
187
|
+
parentLabel="MOH 729B F-MAPS Report"
|
|
188
|
+
parentPath="/moh-729b"
|
|
189
|
+
title={`MOH 729B F-MAPS · ${group.title}`}
|
|
190
|
+
heading={heading}
|
|
191
|
+
location={locationName}
|
|
192
|
+
isLoading={isLoading}
|
|
193
|
+
isEmpty={rows.length === 0}
|
|
194
|
+
>
|
|
195
|
+
<LinelistTable
|
|
196
|
+
rows={rows}
|
|
197
|
+
columns={group.columns}
|
|
198
|
+
layoutKey={group.layoutKey}
|
|
199
|
+
exportName={`MOH 729B ${heading} ${endDate}`}
|
|
200
|
+
/>
|
|
201
|
+
</RegisterLayout>
|
|
202
|
+
);
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
export default Moh729bLinelist;
|