@pih/esm-audit-app 2.1.1-pre.148 → 2.1.1-pre.151

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.
@@ -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('/encountertype')) {
86
- // deliberately unsorted, and including a type the trail does not use
87
- return Promise.resolve({
88
- data: { results: [vitalsType, unusedType, consultationType] },
89
- }) as ReturnType<typeof openmrsFetch>;
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 { useAllEncounterTypes, useAuditUser, useUserEncounters } from './audit.resource';
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 } = useAllEncounterTypes();
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 the system rather than only those this account has touched: encounters are
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
- A range and a next button say exactly what is known.
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
- <div className={styles.lazyPagination}>
206
- <span className={styles.paginationRange}>
207
- {t('encounterRange', 'Encounters {{from}}–{{to}}', {
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
- </span>
212
- {/*
213
- A filter is applied after the trail has been read, so matching rows arrive as the
214
- scan goes on. Without this the list looks finished while it is still filling.
215
- */}
216
- {isDiscovering ? (
217
- <InlineLoading
218
- className={styles.discovering}
219
- description={t('lookingForMoreEncounters', 'Looking for more encounters…')}
220
- />
221
- ) : null}
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>
@@ -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",
@@ -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",
@@ -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",