@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
@@ -0,0 +1,258 @@
1
+ import React, { useState } from 'react';
2
+ import classNames from 'classnames';
3
+ import { Button, Tab, TabList, TabPanel, TabPanels, Tabs, Toggle } from '@carbon/react';
4
+ import { Download } from '@carbon/react/icons';
5
+
6
+ import { columnsToCsv, downloadCsv } from '../../common/report-register';
7
+ import {
8
+ columnCells,
9
+ MOH_729B_SUMMARY,
10
+ NOT_YET_COUNTED,
11
+ rowCells,
12
+ type SummarySection,
13
+ type SummaryTable,
14
+ tableCells,
15
+ } from './moh-729b-summary.config';
16
+ import { countedCode, type Moh729bCell } from './moh-729b.types';
17
+ /* drawn exactly like the MOH 731 Summary */
18
+ import styles from '../moh-731/summary/moh-731-summary.scss';
19
+
20
+ interface Moh729bSummaryProps {
21
+ /** Clients in any of the counts */
22
+ count: (cells: Moh729bCell[]) => number;
23
+ /** Opens the linelist of the counts, under a heading */
24
+ openLinelist: (cells: Moh729bCell[], label: string) => void;
25
+ /** The section tab shown, by id */
26
+ sectionId: string;
27
+ onSectionChange: (sectionId: string) => void;
28
+ /** Used to name the export */
29
+ period: string;
30
+ }
31
+
32
+ const SHOW_CODES_KEY = 'ampath-moh729b-summary-show-codes';
33
+
34
+ const readShowCodes = () => {
35
+ try {
36
+ return localStorage.getItem(SHOW_CODES_KEY) === 'true';
37
+ } catch {
38
+ return false;
39
+ }
40
+ };
41
+
42
+ /** The form's code for a single count, e.g. `CF2G` or `PM12 · 2nd Line` */
43
+ const codeOf = (cells: Moh729bCell[]) =>
44
+ cells.length === 1 && !cells[0].band ? countedCode(cells[0].code) : undefined;
45
+
46
+ /**
47
+ * The MOH 729B report regrouped for reading, as the MOH 731 Summary: a tab per
48
+ * group of the form, each part as a compact table with a Total column and a
49
+ * Total row. Every number opens its linelist, and a total opens the clients of
50
+ * the counts it adds up.
51
+ */
52
+ const Moh729bSummary: React.FC<Moh729bSummaryProps> = ({ count, openLinelist, sectionId, onSectionChange, period }) => {
53
+ const [showCodes, setShowCodes] = useState(readShowCodes);
54
+ const selectedIndex = Math.max(
55
+ 0,
56
+ MOH_729B_SUMMARY.findIndex((section) => section.id === sectionId),
57
+ );
58
+
59
+ const toggleCodes = (checked: boolean) => {
60
+ setShowCodes(checked);
61
+ try {
62
+ localStorage.setItem(SHOW_CODES_KEY, String(checked));
63
+ } catch {
64
+ /* not remembered, still applied */
65
+ }
66
+ };
67
+
68
+ /** A count or total: its value, opening its linelist; the form's code on hover (or shown) */
69
+ const value = (cells: Moh729bCell[], label: string, options: { strong?: boolean; code?: boolean } = {}) => {
70
+ if (cells.length === 0) {
71
+ return <span className={styles.none}>–</span>;
72
+ }
73
+ const amount = count(cells);
74
+ const code = codeOf(cells);
75
+ return (
76
+ <button
77
+ type="button"
78
+ className={classNames(styles.value, { [styles.zero]: amount === 0, [styles.strong]: options.strong })}
79
+ onClick={() => openLinelist(cells, label)}
80
+ title={code ? `${code} · open the linelist` : `Open the ${label} linelist`}
81
+ >
82
+ {amount.toLocaleString()}
83
+ {showCodes && code && options.code !== false && <span className={styles.code}>{code}</span>}
84
+ </button>
85
+ );
86
+ };
87
+
88
+ const exportCsv = () => {
89
+ const lines: Array<Record<string, unknown>> = [];
90
+ MOH_729B_SUMMARY.forEach((section) =>
91
+ section.cards.forEach((card) =>
92
+ card.tables.forEach((table) => {
93
+ const part = [card.title, table.title].filter(Boolean).join(' · ');
94
+ table.rows.forEach((row) => {
95
+ row.cells.forEach((cells, index) => {
96
+ if (cells.length) {
97
+ lines.push({ section: section.tab, part, row: row.label, column: table.columns[index], cells });
98
+ }
99
+ });
100
+ lines.push({ section: section.tab, part, row: row.label, column: 'Total', cells: rowCells(row) });
101
+ });
102
+ if (table.totalLabel) {
103
+ lines.push({
104
+ section: section.tab,
105
+ part,
106
+ row: table.totalLabel,
107
+ column: 'Total',
108
+ cells: tableCells(table),
109
+ });
110
+ }
111
+ }),
112
+ ),
113
+ );
114
+ downloadCsv(
115
+ columnsToCsv(lines, [
116
+ { header: 'Section', value: (line) => line.section },
117
+ { header: 'Part', value: (line) => line.part },
118
+ { header: 'Row', value: (line) => line.row },
119
+ { header: 'Column', value: (line) => line.column },
120
+ { header: 'MOH 729B Code', value: (line) => codeOf(line.cells as Moh729bCell[]) ?? '', text: true },
121
+ { header: 'Value', value: (line) => count(line.cells as Moh729bCell[]) },
122
+ ]),
123
+ `moh-729b-summary-${period}.csv`.replace(/[^a-z0-9.-]+/gi, '-').toLowerCase(),
124
+ );
125
+ };
126
+
127
+ const renderTable = (table: SummaryTable, caption: string, key: number) => (
128
+ <div key={key} className={styles.tableScroll}>
129
+ {table.title && <p className={styles.tableTitle}>{table.title}</p>}
130
+ <table className={classNames(styles.table, { [styles.single]: table.columns.length === 0 })}>
131
+ <thead>
132
+ <tr>
133
+ <th scope="col" className={styles.labelCol} />
134
+ {table.columns.map((column) => (
135
+ <th scope="col" key={column}>
136
+ {column}
137
+ </th>
138
+ ))}
139
+ <th scope="col" className={styles.totalCol}>
140
+ {table.columns.length ? 'Total' : 'Number'}
141
+ </th>
142
+ </tr>
143
+ </thead>
144
+ <tbody>
145
+ {table.rows.map((row) => (
146
+ <tr key={row.label}>
147
+ <th scope="row" className={styles.labelCol}>
148
+ {row.label}
149
+ </th>
150
+ {table.columns.map((column, index) => (
151
+ <td key={column}>{value(row.cells[index] ?? [], `${caption} · ${row.label} · ${column}`)}</td>
152
+ ))}
153
+ <td className={styles.totalCol}>
154
+ {/* the code is beside the row's own counts already; only a count
155
+ shown in the Total column alone carries it here */}
156
+ {value(rowCells(row), `${caption} · ${row.label}`, { code: table.columns.length === 0 })}
157
+ </td>
158
+ </tr>
159
+ ))}
160
+ </tbody>
161
+ {table.totalLabel && (
162
+ <tfoot>
163
+ <tr>
164
+ <th scope="row" className={styles.labelCol}>
165
+ {table.totalLabel}
166
+ </th>
167
+ {table.columns.map((column, index) => (
168
+ <td key={column}>
169
+ {value(columnCells(table, index), `${caption} · ${table.totalLabel} · ${column}`, {
170
+ strong: true,
171
+ code: false,
172
+ })}
173
+ </td>
174
+ ))}
175
+ <td className={styles.totalCol}>
176
+ {value(tableCells(table), `${caption} · ${table.totalLabel}`, { strong: true, code: false })}
177
+ </td>
178
+ </tr>
179
+ </tfoot>
180
+ )}
181
+ </table>
182
+ </div>
183
+ );
184
+
185
+ const renderSection = (section: SummarySection) => (
186
+ <>
187
+ <div className={styles.strip}>
188
+ {section.headlines.map((headline) => (
189
+ <button
190
+ type="button"
191
+ key={headline.label}
192
+ className={styles.headline}
193
+ onClick={() => openLinelist(headline.cells, headline.label)}
194
+ >
195
+ <span className={styles.headlineLabel}>{headline.label}</span>
196
+ <span className={styles.headlineValue}>{count(headline.cells).toLocaleString()}</span>
197
+ </button>
198
+ ))}
199
+ </div>
200
+ <div className={styles.grid}>
201
+ {section.cards.map((card) => (
202
+ <div key={card.title} className={styles.card}>
203
+ <h5 className={styles.cardTitle}>{card.title}</h5>
204
+ {card.note && <p className={styles.note}>{card.note}</p>}
205
+ {card.tables.map((table, index) => renderTable(table, card.title, index))}
206
+ </div>
207
+ ))}
208
+ {section.id === 'ois' && (
209
+ <div className={styles.card}>
210
+ <h5 className={styles.cardTitle}>Not counted yet</h5>
211
+ <p className={styles.note}>{NOT_YET_COUNTED}</p>
212
+ </div>
213
+ )}
214
+ </div>
215
+ </>
216
+ );
217
+
218
+ return (
219
+ <div className={styles.summary}>
220
+ <Tabs
221
+ selectedIndex={selectedIndex}
222
+ onChange={({ selectedIndex: index }) => onSectionChange(MOH_729B_SUMMARY[index].id)}
223
+ >
224
+ <div className={styles.bar}>
225
+ <TabList aria-label="MOH 729B sections" className={styles.sectionTabs}>
226
+ {MOH_729B_SUMMARY.map((section) => (
227
+ <Tab key={section.id}>{section.tab}</Tab>
228
+ ))}
229
+ </TabList>
230
+ <div className={styles.tools}>
231
+ <Toggle
232
+ id="moh729b-summary-codes"
233
+ size="sm"
234
+ labelText="Show MOH codes"
235
+ hideLabel
236
+ labelA="MOH codes"
237
+ labelB="MOH codes"
238
+ toggled={showCodes}
239
+ onToggle={toggleCodes}
240
+ />
241
+ <Button kind="ghost" size="sm" renderIcon={Download} onClick={exportCsv}>
242
+ Export CSV
243
+ </Button>
244
+ </div>
245
+ </div>
246
+ <TabPanels>
247
+ {MOH_729B_SUMMARY.map((section) => (
248
+ <TabPanel key={section.id} className={styles.panel}>
249
+ {renderSection(section)}
250
+ </TabPanel>
251
+ ))}
252
+ </TabPanels>
253
+ </Tabs>
254
+ </div>
255
+ );
256
+ };
257
+
258
+ export default Moh729bSummary;
@@ -0,0 +1,229 @@
1
+ import { WEIGHT_BANDS } from './moh-729b.config';
2
+ import { type Moh729bCell } from './moh-729b.types';
3
+
4
+ /**
5
+ * The layout of the MOH 729B Summary tab: each section as a set of tables, each
6
+ * cell one or more counts of the form (a regimen code, narrowed to a weight
7
+ * band or line where the form splits it).
8
+ *
9
+ * The counts in a table never overlap -- a client has one regimen code, one
10
+ * weight band and one line -- so a Total column or row is the clients of its
11
+ * cells together, and opens their linelists as one.
12
+ */
13
+
14
+ export interface SummaryRow {
15
+ label: string;
16
+ /** The counts in each column (Total excluded); empty where the form has no box */
17
+ cells: Moh729bCell[][];
18
+ /** What the row's Total counts, where that is not its cells together (a child
19
+ without a weight is in no band but still in the regimen's Total) */
20
+ total?: Moh729bCell[];
21
+ }
22
+
23
+ export interface SummaryTable {
24
+ title?: string;
25
+ /** Column headings, Total excluded; none for a single "Number" column */
26
+ columns: string[];
27
+ rows: SummaryRow[];
28
+ /** Label of the Total row; omitted for no Total row */
29
+ totalLabel?: string;
30
+ }
31
+
32
+ export interface SummaryCard {
33
+ /** As on the form, e.g. `Adult ART`; also the linelist caption */
34
+ title: string;
35
+ note?: string;
36
+ tables: SummaryTable[];
37
+ }
38
+
39
+ export interface SummarySection {
40
+ id: string;
41
+ tab: string;
42
+ /** Totals shown in the strip above the section */
43
+ headlines: Array<{ label: string; cells: Moh729bCell[] }>;
44
+ cards: SummaryCard[];
45
+ }
46
+
47
+ const LINES = ['1st Line', '2nd Line', '3rd Line'];
48
+
49
+ /** A row of codes, one per column; null where the form has no such regimen */
50
+ const byCode = (label: string, codes: Array<string | null>): SummaryRow => ({
51
+ label,
52
+ cells: codes.map((code) => (code ? [{ code }] : [])),
53
+ });
54
+
55
+ /** A single figure, for a table with one "Number" column */
56
+ const single = (label: string, code: string): SummaryRow => ({ label, cells: [], total: [{ code }] });
57
+
58
+ /** Every count in a table's rows */
59
+ export const rowCells = (row: SummaryRow) => row.total ?? row.cells.flat();
60
+ export const tableCells = (table: SummaryTable) => table.rows.flatMap(rowCells);
61
+ export const columnCells = (table: SummaryTable, index: number) => table.rows.flatMap((row) => row.cells[index] ?? []);
62
+
63
+ const ADULT_ART: SummaryTable = {
64
+ columns: LINES,
65
+ rows: [
66
+ byCode('TDF + 3TC + DTG', ['AF2E', 'AS2B', 'AT2G']),
67
+ byCode('TAF + 3TC + DTG', ['AS2G', null, null]),
68
+ byCode('TDF + 3TC + EFV', ['AF2B', null, null]),
69
+ byCode('AZT + 3TC + DTG', ['AF1D', 'AS1C', 'AT2H']),
70
+ byCode('ABC + 3TC + DTG', ['AF4C', 'AS5C', null]),
71
+ byCode('TDF + 3TC + ATV/r', ['AF2D', 'AS2C', null]),
72
+ byCode('TDF + 3TC + LPV/r', ['AF2F', 'AS2A', null]),
73
+ byCode('AZT + 3TC + LPV/r', ['AF1E', 'AS1A', null]),
74
+ byCode('AZT + 3TC + ATV/r', ['AF1F', 'AS1B', null]),
75
+ byCode('ABC + 3TC + LPV/r', [null, 'AS5A', null]),
76
+ byCode('ABC + 3TC + ATV/r', [null, 'AS5B', null]),
77
+ byCode('TDF + 3TC + DTG + DRV + RTV', [null, null, 'AT2D']),
78
+ byCode('TDF + 3TC + DTG + ETV + DRV + RTV', [null, null, 'AT2F']),
79
+ byCode('AZT + 3TC + DTG + DRV + RTV', [null, null, 'AT2I']),
80
+ byCode('Any other regimen', ['AF5X', 'AS6X', 'AT2X']),
81
+ ],
82
+ totalLabel: 'Total adults',
83
+ };
84
+
85
+ const PAEDIATRIC_LINES: Array<[string, string[]]> = [
86
+ ['1st Line', ['CF2G', 'CF1D', 'CF4E', 'CF5X']],
87
+ ['2nd Line', ['CS1A', 'CS1B', 'CS2A', 'CS2B', 'CS4X']],
88
+ ['3rd Line', ['CT1H', 'CT2D', 'CT3X']],
89
+ ];
90
+
91
+ const PAEDIATRIC_ART: SummaryTable = {
92
+ columns: LINES,
93
+ rows: [
94
+ byCode('ABC + 3TC + DTG', ['CF2G', 'CS2B', null]),
95
+ byCode('AZT + 3TC + DTG', ['CF1D', 'CS1B', null]),
96
+ byCode('TDF + 3TC + DTG', ['CF4E', null, null]),
97
+ byCode('AZT + 3TC + LPV/r', [null, 'CS1A', null]),
98
+ byCode('ABC + 3TC + LPV/r', [null, 'CS2A', null]),
99
+ byCode('AZT + 3TC + DRV + RTV + RAL', [null, null, 'CT1H']),
100
+ byCode('ABC + 3TC + DRV + RTV + RAL', [null, null, 'CT2D']),
101
+ byCode('Any other regimen', ['CF5X', 'CS4X', 'CT3X']),
102
+ ],
103
+ totalLabel: 'Total children',
104
+ };
105
+
106
+ const PAEDIATRIC_WEIGHT: SummaryTable = {
107
+ title: 'By weight band',
108
+ columns: WEIGHT_BANDS,
109
+ rows: PAEDIATRIC_LINES.map(([label, codes]) => ({
110
+ label,
111
+ cells: WEIGHT_BANDS.map((band) => codes.map((code) => ({ code, band }))),
112
+ total: codes.map((code) => ({ code })),
113
+ })),
114
+ totalLabel: 'Total children',
115
+ };
116
+
117
+ const PMTCT_MOTHERS: SummaryTable = {
118
+ columns: LINES,
119
+ rows: (
120
+ [
121
+ ['TDF + 3TC + DTG', 'PM12'],
122
+ ['TDF + 3TC + EFV', 'PM9'],
123
+ ['AZT + 3TC + DTG', 'PM13'],
124
+ ['ABC + 3TC + DTG', 'PM14'],
125
+ ['TDF + 3TC + ATV/r', 'PM11'],
126
+ ['TDF + 3TC + LPV/r', 'PM7'],
127
+ ['AZT + 3TC + ATV/r', 'PM10'],
128
+ ['AZT + 3TC + LPV/r', 'PM5'],
129
+ ['Any other PMTCT regimen', 'PM1X'],
130
+ ] as Array<[string, string]>
131
+ ).map(([label, code]) => ({ label, cells: [1, 2, 3].map((line) => [{ code, line }]), total: [{ code }] })),
132
+ totalLabel: 'Total pregnant women',
133
+ };
134
+
135
+ const PREP: SummaryTable = {
136
+ columns: [],
137
+ rows: [
138
+ single('TDF + FTC', 'PRP1A'),
139
+ single('TDF + 3TC', 'PRP1B'),
140
+ single('Dapivirine ring', 'PRP1D'),
141
+ single('Cabotegravir', 'PRP1E'),
142
+ single('Lenacapavir', 'PRP1F'),
143
+ single('Others', 'PRP1X'),
144
+ ],
145
+ totalLabel: 'Total on PrEP',
146
+ };
147
+
148
+ const PEP: SummaryTable = {
149
+ columns: ['Adults', 'Children'],
150
+ rows: [
151
+ byCode('TDF + 3TC + DTG', ['PA3D', null]),
152
+ byCode('TDF + 3TC + ATV/r', ['PA3C', null]),
153
+ byCode('AZT + 3TC + ATV/r', ['PA1C', null]),
154
+ byCode('ABC + 3TC + LPV/r', [null, 'PC3A']),
155
+ byCode('ABC + 3TC + DTG', [null, 'PC3C']),
156
+ byCode('Any other PEP regimen', ['PA4X', 'PC4X']),
157
+ ],
158
+ totalLabel: 'Total given PEP',
159
+ };
160
+
161
+ const HEPATITIS_B: SummaryTable = {
162
+ columns: [],
163
+ rows: [single('TDF + 3TC', 'HPB1A'), single('TDF + FTC', 'HPB1B'), single('Any other Hep B regimen', 'HPB1C')],
164
+ totalLabel: 'Total Hep B (HIV-ve)',
165
+ };
166
+
167
+ const TPT: SummaryTable = {
168
+ columns: ['6H', '3HP'],
169
+ rows: [byCode('Adults (≥15 yrs)', ['ATPT1A', 'ATPT1B']), byCode('Children (<15 yrs)', ['CTPT1A', 'CTPT1B'])],
170
+ totalLabel: 'Total started TPT',
171
+ };
172
+
173
+ const all = (table: SummaryTable) => tableCells(table);
174
+
175
+ /** Not counted yet: shown on the form blank, listed here so the gap is visible */
176
+ export const NOT_YET_COUNTED =
177
+ 'Not counted yet: PMTCT infants (PC8, PC7, PC1X), universal prophylaxis (OI1A, OI1C, OI2A), TPT for HIV-negative clients (ATPT1C) and Cryptococcal Meningitis (OI6B, OI6X). These are blank on the form too, until their queries are added.';
178
+
179
+ export const MOH_729B_SUMMARY: SummarySection[] = [
180
+ {
181
+ id: 'art',
182
+ tab: 'ART',
183
+ headlines: [
184
+ { label: 'Adults on ART', cells: all(ADULT_ART) },
185
+ { label: 'Children on ART', cells: all(PAEDIATRIC_ART) },
186
+ { label: 'Pregnant women on ART', cells: all(PMTCT_MOTHERS) },
187
+ { label: 'Total on ART', cells: [...all(ADULT_ART), ...all(PAEDIATRIC_ART), ...all(PMTCT_MOTHERS)] },
188
+ ],
189
+ cards: [
190
+ { title: 'Adult ART', tables: [ADULT_ART] },
191
+ {
192
+ title: 'Paediatric ART',
193
+ note: 'Under 15 years. Weight is from the latest clinical visit.',
194
+ tables: [PAEDIATRIC_ART, PAEDIATRIC_WEIGHT],
195
+ },
196
+ { title: 'PMTCT · Pregnant women', note: 'Women pregnant this month.', tables: [PMTCT_MOTHERS] },
197
+ ],
198
+ },
199
+ {
200
+ id: 'prevention',
201
+ tab: 'PrEP, PEP & Hep B',
202
+ headlines: [
203
+ { label: 'On PrEP', cells: all(PREP) },
204
+ { label: 'Given PEP', cells: all(PEP) },
205
+ { label: 'Hep B (HIV-ve)', cells: all(HEPATITIS_B) },
206
+ ],
207
+ cards: [
208
+ { title: 'Pre-Exposure Prophylaxis (PrEP)', note: 'Seen for PrEP this month, not discontinued.', tables: [PREP] },
209
+ { title: 'Post Exposure Prophylaxis (PEP)', note: 'Given PEP this month; children are under 15.', tables: [PEP] },
210
+ {
211
+ title: 'Hepatitis B patients who are HIV-ve',
212
+ note: 'HBV positive at ANC this month (MOH 731 2.4). No Hep B regimen is recorded yet, so all are "any other".',
213
+ tables: [HEPATITIS_B],
214
+ },
215
+ ],
216
+ },
217
+ {
218
+ id: 'ois',
219
+ tab: 'OIs & TPT',
220
+ headlines: [{ label: 'Started TPT (HIV positive)', cells: all(TPT) }],
221
+ cards: [
222
+ {
223
+ title: 'TPT HIV Positive',
224
+ note: 'Started ART and TPT this month (MOH 731 3.4).',
225
+ tables: [TPT],
226
+ },
227
+ ],
228
+ },
229
+ ];
@@ -0,0 +1,157 @@
1
+ import React, { useMemo, useRef, useState } from 'react';
2
+ import { useNavigate, useSearchParams } from 'react-router-dom';
3
+ import { Tab, TabList, TabPanel, TabPanels, Tabs } from '@carbon/react';
4
+ import { useSession } from '@openmrs/esm-framework';
5
+
6
+ import { ReportFiltersComponent, type ReportPeriod, useRestorePeriod } from '../../common/report-filters';
7
+ import { downloadSheetsPdf } from '../../common/report-page';
8
+ import { showReportError } from '../../common/report-error';
9
+ import { ReportPlaceholder, useGeneratedLocation } from '../../common/report-placeholder';
10
+ import { ReportSkeleton } from '../../common/report-skeleton';
11
+ import { registerFileName } from '../../registers/register-file-name';
12
+ import { getMoh729b } from '../../resources/moh-729b.resource';
13
+ import Moh729bForm, { type Moh729bCounts } from './moh-729b-form.component';
14
+ import Moh729bSummary from './moh-729b-summary.component';
15
+ import { encodeCell, inCell, inCells, type Moh729bCell, type Moh729bRow } from './moh-729b.types';
16
+ import styles from './moh-729b.scss';
17
+ import reportStyles from '../moh-731/moh-731.scss';
18
+
19
+ /**
20
+ * MoH 729B Facility Monthly ARV Patient Summary (F-MAPS), opened from the
21
+ * landing page's Reports tab, with the reports' filter bar.
22
+ *
23
+ * The clients active on ART in the month come back one row each with their
24
+ * regimen code (/moh-729b), so every count on the form is a count of those
25
+ * rows and opens the linelist of exactly those clients. As MOH 731, the form
26
+ * has a Summary tab beside it that regroups the counts into tables with totals.
27
+ */
28
+ const Moh729bReport: React.FC = () => {
29
+ const [rows, setRows] = useState<Moh729bRow[] | null>(null);
30
+ const [isLoading, setIsLoading] = useState(false);
31
+ const [period, setPeriod] = useState<{ startDate: string; endDate: string }>({ startDate: '', endDate: '' });
32
+
33
+ const navigate = useNavigate();
34
+
35
+ /* The form tab is the default; the selected tab and the Summary's section
36
+ are kept in the URL, so a linelist opened from them comes back to them */
37
+ const [searchParams, setSearchParams] = useSearchParams();
38
+ const view = searchParams.get('view') === 'summary' ? 'summary' : undefined;
39
+ const section = searchParams.get('section') ?? '';
40
+ const setParam = (key: string, value?: string) => {
41
+ const next = new URLSearchParams(searchParams);
42
+ if (value) {
43
+ next.set(key, value);
44
+ } else {
45
+ next.delete(key);
46
+ }
47
+ setSearchParams(next, { replace: true });
48
+ };
49
+ const session = useSession();
50
+ const locationUuids = session?.sessionLocation?.uuid ?? '';
51
+ const facility = session?.sessionLocation?.display ?? '';
52
+ /* Only a report for the current location counts */
53
+ const { isForCurrentLocation, hasLocationChanged, markGenerated } = useGeneratedLocation('moh729bLocation');
54
+ const isReportGenerated = rows !== null && isForCurrentLocation;
55
+
56
+ const generate = async ({ startDate, endDate }: ReportPeriod) => {
57
+ setIsLoading(true);
58
+ setPeriod({ startDate, endDate });
59
+ try {
60
+ setRows((await getMoh729b({ locationUuids, endDate })) as unknown as Moh729bRow[]);
61
+ markGenerated(locationUuids);
62
+ } catch (error) {
63
+ showReportError('the MOH 729B report', error);
64
+ } finally {
65
+ setIsLoading(false);
66
+ }
67
+ };
68
+
69
+ useRestorePeriod(generate);
70
+
71
+ /** Opens the linelist of one count or, under a heading, of several */
72
+ const openLinelist = (cells: Moh729bCell[], label?: string) => {
73
+ const params = new URLSearchParams({ ...period, locationUuids, from: 'moh-729b' });
74
+ cells.forEach((cell) => params.append('f', encodeCell(cell)));
75
+ if (label) params.set('label', label);
76
+ if (view) params.set('view', view);
77
+ if (view && section) params.set('section', section);
78
+ navigate(`/moh-729b-linelist?${params}`);
79
+ };
80
+
81
+ const counts = useMemo<Moh729bCounts>(
82
+ () => ({
83
+ count: (cell) => (rows ?? []).filter((row) => inCell(row, cell)).length,
84
+ open: (cell) => openLinelist([cell]),
85
+ }),
86
+ // eslint-disable-next-line react-hooks/exhaustive-deps
87
+ [rows, period, locationUuids, view, section],
88
+ );
89
+ const countClients = (cells: Moh729bCell[]) => (rows ?? []).filter((row) => inCells(row, cells)).length;
90
+
91
+ /* Download PDF saves the form as one A3 portrait page, made from a copy, so
92
+ the page on screen is left as it is */
93
+ const sheetRef = useRef<HTMLDivElement>(null);
94
+ const downloadPdf = async () => {
95
+ const pages = Array.from(sheetRef.current?.querySelectorAll<HTMLElement>('[data-register-page]') ?? []);
96
+ await downloadSheetsPdf(pages, registerFileName('MOH-729B', facility, period), {
97
+ paperSize: 'a3',
98
+ orientation: 'portrait',
99
+ stylesheetsOnly: true,
100
+ });
101
+ };
102
+
103
+ return (
104
+ <>
105
+ <ReportFiltersComponent
106
+ reportName="MOH 729B F-MAPS Report"
107
+ mode="monthly"
108
+ onGenerate={generate}
109
+ isLoading={isLoading}
110
+ isReportGenerated={isReportGenerated}
111
+ generatedPeriod={period}
112
+ onDownloadPdf={downloadPdf}
113
+ />
114
+ {isLoading && <ReportSkeleton />}
115
+ {!isLoading && !isReportGenerated && (
116
+ <ReportPlaceholder reportName="MOH 729B" isLocationChanged={rows !== null && hasLocationChanged} />
117
+ )}
118
+ {!isLoading && isReportGenerated && (
119
+ <Tabs
120
+ selectedIndex={view === 'summary' ? 1 : 0}
121
+ onChange={({ selectedIndex }) => setParam('view', selectedIndex === 1 ? 'summary' : undefined)}
122
+ >
123
+ <TabList aria-label="MOH 729B views" className={reportStyles.tabList}>
124
+ <Tab>MOH 729B Form</Tab>
125
+ <Tab>Summary</Tab>
126
+ </TabList>
127
+ <TabPanels>
128
+ <TabPanel className={reportStyles.formPanel}>
129
+ {/* Download PDF saves the form whichever tab is open */}
130
+ <div className={styles.reportFrame} ref={sheetRef}>
131
+ <Moh729bForm
132
+ facility={facility}
133
+ startDate={period.startDate}
134
+ endDate={period.endDate}
135
+ counts={counts}
136
+ />
137
+ </div>
138
+ </TabPanel>
139
+ <TabPanel className={reportStyles.summaryPanel}>
140
+ <div className={reportStyles.reportFrame}>
141
+ <Moh729bSummary
142
+ count={countClients}
143
+ openLinelist={openLinelist}
144
+ sectionId={section}
145
+ onSectionChange={(id) => setParam('section', id)}
146
+ period={period.endDate.slice(0, 7)}
147
+ />
148
+ </div>
149
+ </TabPanel>
150
+ </TabPanels>
151
+ </Tabs>
152
+ )}
153
+ </>
154
+ );
155
+ };
156
+
157
+ export default Moh729bReport;