@pih/esm-audit-app 2.1.1-pre.148 → 2.1.1-pre.154
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/.turbo/turbo-build.log +1 -1
- package/dist/117.js +1 -1
- package/dist/117.js.map +1 -1
- package/dist/508.js +1 -1
- package/dist/508.js.map +1 -1
- package/dist/514.js +1 -1
- package/dist/514.js.map +1 -1
- package/dist/912.js +1 -1
- package/dist/912.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/pih-esm-audit-app.js +1 -1
- package/dist/pih-esm-audit-app.js.buildmanifest.json +14 -14
- package/dist/routes.json +1 -1
- package/locales/en/translation.json +118 -0
- package/locales/fr/translation.json +118 -0
- package/package.json +1 -1
- package/src/audit/audit.resource.ts +24 -8
- package/src/audit/audit.scss +2 -17
- package/src/audit/provider-encounters.component.test.tsx +33 -6
- package/src/audit/provider-encounters.component.tsx +3 -3
- package/src/audit/user-encounters.component.test.tsx +22 -13
- package/src/audit/user-encounters.component.tsx +35 -29
- package/translations/en.json +0 -2
- package/translations/es.json +0 -2
- package/translations/fr.json +0 -2
|
@@ -30,9 +30,12 @@ function dayFromIndex(index: number): string {
|
|
|
30
30
|
const consultationType = { uuid: 'type-consultation', display: 'Consultation' };
|
|
31
31
|
const vitalsType = { uuid: 'type-vitals', display: 'Vitals' };
|
|
32
32
|
|
|
33
|
-
/** A type nothing in the trail uses, since the dropdown offers every type in the system. */
|
|
33
|
+
/** A type nothing in the trail uses, since the dropdown offers every type in use in the system. */
|
|
34
34
|
const unusedType = { uuid: 'type-unused', display: 'Zibra Admission' };
|
|
35
35
|
|
|
36
|
+
/** A type nothing in the system has been recorded against, which the dropdown leaves out. */
|
|
37
|
+
const neverRecordedType = { uuid: 'type-never', display: 'Never Recorded' };
|
|
38
|
+
|
|
36
39
|
function encounter(index: number) {
|
|
37
40
|
return {
|
|
38
41
|
uuid: `enc-${index}`,
|
|
@@ -82,11 +85,17 @@ function mockRestApi({ totalCreated = 250, obsPerEncounter = 20 } = {}) {
|
|
|
82
85
|
if (url.includes('/pihapps/obs?') && url.includes('voidedBy=')) {
|
|
83
86
|
return Promise.resolve({ data: { results: [] } }) as ReturnType<typeof openmrsFetch>;
|
|
84
87
|
}
|
|
85
|
-
if (url.includes('/
|
|
86
|
-
// deliberately unsorted, and including a type the trail does not use
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
88
|
+
if (url.includes('/pihapps/encounterTypeCount')) {
|
|
89
|
+
// deliberately unsorted, and including a type the trail does not use and one nothing uses
|
|
90
|
+
const counts = [
|
|
91
|
+
{ encounterType: vitalsType, count: 40 },
|
|
92
|
+
{ encounterType: neverRecordedType, count: 0 },
|
|
93
|
+
{ encounterType: unusedType, count: 3 },
|
|
94
|
+
{ encounterType: consultationType, count: 40 },
|
|
95
|
+
];
|
|
96
|
+
// the endpoint leaves out unused types only when asked, so the mock does too
|
|
97
|
+
const results = url.includes('onlyUsed=true') ? counts.filter((entry) => entry.count > 0) : counts;
|
|
98
|
+
return Promise.resolve({ data: { results } }) as ReturnType<typeof openmrsFetch>;
|
|
90
99
|
}
|
|
91
100
|
if (url.includes('/obs?encounter=')) {
|
|
92
101
|
return Promise.resolve({
|
|
@@ -187,7 +196,7 @@ describe('<UserEncounters />', () => {
|
|
|
187
196
|
await screen.findByText('Encounters 1–10');
|
|
188
197
|
const beforePaging = auditRequests().length;
|
|
189
198
|
|
|
190
|
-
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
|
|
199
|
+
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
|
191
200
|
|
|
192
201
|
expect(await screen.findByText('Encounters 11–20')).toBeInTheDocument();
|
|
193
202
|
expect(auditRequests().length).toBeGreaterThan(beforePaging);
|
|
@@ -198,11 +207,11 @@ describe('<UserEncounters />', () => {
|
|
|
198
207
|
mockRestApi({ totalCreated: 5000, obsPerEncounter: 20 });
|
|
199
208
|
renderUserEncounters();
|
|
200
209
|
await screen.findByText('Encounters 1–10');
|
|
201
|
-
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
|
|
210
|
+
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
|
202
211
|
await screen.findByText('Encounters 11–20');
|
|
203
212
|
const afterPaging = auditRequests().length;
|
|
204
213
|
|
|
205
|
-
await userEvent.click(screen.getByRole('button', { name: 'Previous' }));
|
|
214
|
+
await userEvent.click(screen.getByRole('button', { name: 'Previous page' }));
|
|
206
215
|
|
|
207
216
|
expect(await screen.findByText('Encounters 1–10')).toBeInTheDocument();
|
|
208
217
|
expect(auditRequests()).toHaveLength(afterPaging);
|
|
@@ -214,10 +223,10 @@ describe('<UserEncounters />', () => {
|
|
|
214
223
|
renderUserEncounters();
|
|
215
224
|
|
|
216
225
|
await screen.findByText('Encounters 1–10');
|
|
217
|
-
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
|
|
226
|
+
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
|
218
227
|
|
|
219
228
|
expect(await screen.findByText('Encounters 11–12')).toBeInTheDocument();
|
|
220
|
-
expect(screen.getByRole('button', { name: 'Next' })).toBeDisabled();
|
|
229
|
+
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
|
|
221
230
|
});
|
|
222
231
|
|
|
223
232
|
it('counts each row’s observations exactly, from the encounter itself', async () => {
|
|
@@ -270,7 +279,7 @@ describe('<UserEncounters />', () => {
|
|
|
270
279
|
mockRestApi({ totalCreated: 5000, obsPerEncounter: 20 });
|
|
271
280
|
renderUserEncounters();
|
|
272
281
|
await screen.findByText('Encounters 1–10');
|
|
273
|
-
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
|
|
282
|
+
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
|
|
274
283
|
await screen.findByText('Encounters 11–20');
|
|
275
284
|
|
|
276
285
|
await userEvent.clear(screen.getByLabelText('Changed between'));
|
|
@@ -300,7 +309,7 @@ describe('<UserEncounters />', () => {
|
|
|
300
309
|
expect(screen.queryByRole('button', { name: /clear filters/i })).not.toBeInTheDocument();
|
|
301
310
|
});
|
|
302
311
|
|
|
303
|
-
it('offers every encounter type in the system, not just the ones in the trail', async () => {
|
|
312
|
+
it('offers every encounter type in use in the system, not just the ones in the trail', async () => {
|
|
304
313
|
renderUserEncounters();
|
|
305
314
|
await screen.findByText('Encounters 1–10');
|
|
306
315
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useState } from 'react';
|
|
1
|
+
import React, { useMemo, useState } from 'react';
|
|
2
2
|
import { useTranslation } from 'react-i18next';
|
|
3
3
|
import {
|
|
4
4
|
Button,
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
DataTableSkeleton,
|
|
7
7
|
InlineLoading,
|
|
8
8
|
InlineNotification,
|
|
9
|
+
Pagination,
|
|
9
10
|
Table,
|
|
10
11
|
TableBody,
|
|
11
12
|
TableCell,
|
|
@@ -20,7 +21,7 @@ import { type Config } from '../config-schema';
|
|
|
20
21
|
import { type OpenmrsResourceRef } from '../types';
|
|
21
22
|
import { formatAuditDatetime } from './audit-format';
|
|
22
23
|
import { type DateRange, fromPickerRange, hasDateRange, toPickerDefault } from './date-range';
|
|
23
|
-
import {
|
|
24
|
+
import { useAuditUser, useUsedEncounterTypes, useUserEncounters } from './audit.resource';
|
|
24
25
|
import styles from './audit.scss';
|
|
25
26
|
|
|
26
27
|
interface UserEncountersProps {
|
|
@@ -37,7 +38,8 @@ interface UserEncountersProps {
|
|
|
37
38
|
export default function UserEncounters({ userUuid, onSelectEncounter, onBackToSearch }: UserEncountersProps) {
|
|
38
39
|
const { t } = useTranslation();
|
|
39
40
|
const config = useConfig<Config>();
|
|
40
|
-
const pageSize = config.encountersPageSize ?? 10;
|
|
41
|
+
const [pageSize, setPageSize] = useState(config.encountersPageSize ?? 10);
|
|
42
|
+
const pageSizes = useMemo(() => Array.from(new Set([pageSize, 10, 20, 50])).sort((a, b) => a - b), [pageSize]);
|
|
41
43
|
const [dateRange, setDateRange] = useState<DateRange>({});
|
|
42
44
|
// bumped to clear the range picker, which has to be left to hold its own value
|
|
43
45
|
const [datePickerKey, setDatePickerKey] = useState(0);
|
|
@@ -45,7 +47,7 @@ export default function UserEncounters({ userUuid, onSelectEncounter, onBackToSe
|
|
|
45
47
|
const today = new Date();
|
|
46
48
|
|
|
47
49
|
const { user, error: userError, isLoading: isLoadingUser } = useAuditUser(userUuid);
|
|
48
|
-
const { encounterTypes, isLoading: isLoadingTypes } =
|
|
50
|
+
const { encounterTypes, isLoading: isLoadingTypes } = useUsedEncounterTypes();
|
|
49
51
|
const {
|
|
50
52
|
activity,
|
|
51
53
|
currentPage,
|
|
@@ -78,7 +80,7 @@ export default function UserEncounters({ userUuid, onSelectEncounter, onBackToSe
|
|
|
78
80
|
*/}
|
|
79
81
|
<div className={styles.filters}>
|
|
80
82
|
{/*
|
|
81
|
-
Every type in
|
|
83
|
+
Every type in use rather than only those this account has touched: encounters are
|
|
82
84
|
discovered lazily, so which types the trail holds is not known until all of it is read.
|
|
83
85
|
*/}
|
|
84
86
|
<ComboBox
|
|
@@ -197,35 +199,39 @@ export default function UserEncounters({ userUuid, onSelectEncounter, onBackToSe
|
|
|
197
199
|
</TableBody>
|
|
198
200
|
</Table>
|
|
199
201
|
</TableContainer>
|
|
202
|
+
{/*
|
|
203
|
+
A filter is applied after the trail has been read, so matching rows arrive as the scan
|
|
204
|
+
goes on. Without this the list looks finished while it is still filling.
|
|
205
|
+
*/}
|
|
206
|
+
{isDiscovering ? (
|
|
207
|
+
<InlineLoading
|
|
208
|
+
className={styles.discovering}
|
|
209
|
+
description={t('lookingForMoreEncounters', 'Looking for more encounters…')}
|
|
210
|
+
/>
|
|
211
|
+
) : null}
|
|
200
212
|
{/*
|
|
201
213
|
How many encounters this account has touched cannot be known without reading its whole
|
|
202
|
-
trail, which is the very thing being avoided, so there is no total to page against
|
|
203
|
-
|
|
214
|
+
trail, which is the very thing being avoided, so there is no total to page against: the
|
|
215
|
+
pager runs with its page count unknown and says only the range on screen.
|
|
204
216
|
*/}
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
217
|
+
<Pagination
|
|
218
|
+
isLastPage={!hasNextPage}
|
|
219
|
+
itemText={() =>
|
|
220
|
+
t('encounterRange', 'Encounters {{from}}–{{to}}', {
|
|
208
221
|
from: firstRowOnPage + 1,
|
|
209
222
|
to: firstRowOnPage + activity.length,
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
{
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
{
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
<Button disabled={currentPage === 1} kind="ghost" onClick={() => goTo(currentPage - 1)} size="sm">
|
|
223
|
-
{t('previousPage', 'Previous')}
|
|
224
|
-
</Button>
|
|
225
|
-
<Button disabled={!hasNextPage} kind="ghost" onClick={() => goTo(currentPage + 1)} size="sm">
|
|
226
|
-
{t('nextPage', 'Next')}
|
|
227
|
-
</Button>
|
|
228
|
-
</div>
|
|
223
|
+
})
|
|
224
|
+
}
|
|
225
|
+
onChange={({ page: nextPage, pageSize: nextPageSize }) => {
|
|
226
|
+
setPageSize(nextPageSize);
|
|
227
|
+
goTo(nextPage);
|
|
228
|
+
}}
|
|
229
|
+
page={currentPage}
|
|
230
|
+
pageSize={pageSize}
|
|
231
|
+
pageSizes={pageSizes}
|
|
232
|
+
pagesUnknown
|
|
233
|
+
size="sm"
|
|
234
|
+
/>
|
|
229
235
|
</>
|
|
230
236
|
)}
|
|
231
237
|
</div>
|
package/translations/en.json
CHANGED
|
@@ -53,7 +53,6 @@
|
|
|
53
53
|
"location": "Location",
|
|
54
54
|
"lookingForMoreEncounters": "Looking for more encounters…",
|
|
55
55
|
"name": "Name",
|
|
56
|
-
"nextPage": "Next",
|
|
57
56
|
"noActivity": "There is no recorded activity to show.",
|
|
58
57
|
"noEncountersFound": "This patient has no encounters to audit.",
|
|
59
58
|
"noEncountersMatchFilters": "No encounters match these filters.",
|
|
@@ -78,7 +77,6 @@
|
|
|
78
77
|
"patientName": "Patient name",
|
|
79
78
|
"patients": "Patients",
|
|
80
79
|
"patientSearchResults": "Patient search results",
|
|
81
|
-
"previousPage": "Previous",
|
|
82
80
|
"previousValue": "Previous value",
|
|
83
81
|
"previousValueLegend": "the value that an edit replaced",
|
|
84
82
|
"provider": "Provider",
|
package/translations/es.json
CHANGED
|
@@ -53,7 +53,6 @@
|
|
|
53
53
|
"location": "Localidad",
|
|
54
54
|
"lookingForMoreEncounters": "Buscando más encuentros…",
|
|
55
55
|
"name": "Nombre",
|
|
56
|
-
"nextPage": "Siguiente",
|
|
57
56
|
"noActivity": "No hay actividad registrada que mostrar.",
|
|
58
57
|
"noEncountersFound": "Este paciente no tiene encuentros que auditar.",
|
|
59
58
|
"noEncountersMatchFilters": "Ningún encuentro coincide con estos filtros.",
|
|
@@ -78,7 +77,6 @@
|
|
|
78
77
|
"patientName": "Nombre del paciente",
|
|
79
78
|
"patients": "Pacientes",
|
|
80
79
|
"patientSearchResults": "Resultados de la búsqueda de pacientes",
|
|
81
|
-
"previousPage": "Anterior",
|
|
82
80
|
"previousValue": "Valor anterior",
|
|
83
81
|
"previousValueLegend": "el valor sustituido por una edición",
|
|
84
82
|
"provider": "Proveedor",
|
package/translations/fr.json
CHANGED
|
@@ -53,7 +53,6 @@
|
|
|
53
53
|
"location": "Lieu",
|
|
54
54
|
"lookingForMoreEncounters": "Recherche d'autres rencontres…",
|
|
55
55
|
"name": "Nom",
|
|
56
|
-
"nextPage": "Suivant",
|
|
57
56
|
"noActivity": "Aucune activité enregistrée à afficher.",
|
|
58
57
|
"noEncountersFound": "Ce patient n'a aucune rencontre à auditer.",
|
|
59
58
|
"noEncountersMatchFilters": "Aucune rencontre ne correspond à ces filtres.",
|
|
@@ -78,7 +77,6 @@
|
|
|
78
77
|
"patientName": "Nom du patient",
|
|
79
78
|
"patients": "Patients",
|
|
80
79
|
"patientSearchResults": "Résultats de la recherche de patients",
|
|
81
|
-
"previousPage": "Précédent",
|
|
82
80
|
"previousValue": "Valeur précédente",
|
|
83
81
|
"previousValueLegend": "la valeur remplacée par une modification",
|
|
84
82
|
"provider": "Prestataire de service",
|