Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
118 changes: 118 additions & 0 deletions packages/esm-audit-app/locales/en/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"searchBy": "Search by",
"patients": "Patients",
"users": "Users",
"providers": "Providers",
"auditTrail": "Audit trail",
"encounterAudit": "Encounter audit",
"encounterNotFound": "That encounter could not be found.",
"patient": "Patient",
"identifier": "Identifier",
"encounterDate": "Encounter date",
"location": "Location",
"visit": "Visit",
"encounterType": "Encounter type",
"form": "Form",
"createdBy": "Created by",
"changedBy": "Changed by",
"deletedBy": "Deleted by",
"deletionReason": "Reason for deletion",
"encounter": "Encounter",
"deleted": "Deleted",
"encounterDeletedSubtitle": "This encounter has been deleted. Its observations are shown as they were left.",
"encounterDeleted": "Deleted encounter",
"encounterSummary": "Encounter summary",
"noProviders": "No providers are recorded for this encounter.",
"role": "Role",
"providerName": "Provider name",
"backToEncounters": "Back to encounters",
"allEncounterTypes": "All encounter types",
"encounterDateRange": "Encounter date range",
"clearFilters": "Clear filters",
"edited": "Edited",
"editedLegend": "the value was changed after it was first recorded",
"addedLater": "Added later",
"addedLaterLegend": "recorded after the encounter itself was created",
"deletedLegend": "removed from the encounter",
"previousValue": "Previous value",
"previousValueLegend": "the value that an edit replaced",
"observations": "Observations",
"showDeletedObs": "Show deleted observations",
"showConceptDescriptions": "Show concept descriptions",
"noObservations": "This encounter has no observations.",
"questionConcept": "Question concept",
"value": "Value",
"change": "Change",
"recordedBy": "Recorded by",
"wasPreviously": "was {{value}}",
"changedByUser": "Changed by {{user}}",
"encounterCreatedAction": "Created encounter",
"encounterChangedAction": "Changed encounter",
"encounterDeletedAction": "Deleted encounter",
"obsRecordedAction": "Recorded observation",
"obsEditedAction": "Edited observation",
"obsDeletedAction": "Deleted observation",
"recordActivity": "Patient activity",
"readingObservations": "Reading observations from {{read}} of {{total}} encounters…",
"noActivity": "There is no recorded activity to show.",
"whoTouchedThisRecord": "Who modified this patient record",
"user": "User",
"encountersCreated": "Encounters created",
"obsRecordedCount": "Observations recorded",
"obsEditedCount": "Observations edited",
"obsDeletedCount": "Observations deleted",
"firstActivity": "First activity",
"lastActivity": "Last activity",
"showOnlyThisUser": "Show only this user's activity",
"unknownUser": "Unknown user",
"activityLog": "Activity log",
"showEveryone": "Show everyone",
"when": "When",
"action": "Action",
"detail": "Detail",
"encounters": "Encounters",
"noEncountersMatchFilters": "No encounters match these filters.",
"noEncountersFound": "This patient has no encounters to audit.",
"provider": "Provider",
"status": "Status",
"backToPatientSearch": "Back to patient search",
"includeDeletedEncounters": "Include deleted encounters",
"cannotIncludeDeletedEncounters": "Deleted encounters can only be listed for a patient who has an identifier.",
"deletedEncountersUnavailable": "Deleted encounters unavailable",
"auditViews": "Audit views",
"searchForPatient": "Search for a patient by name or identifier",
"searchToBegin": "Enter a patient name or identifier to begin auditing their records.",
"patientSearchResults": "Patient search results",
"noPatientsFound": "No patients match \"{{searchTerm}}\".",
"patientName": "Patient name",
"gender": "Gender",
"age": "Age",
"birthdate": "Date of birth",
"backToProviderSearch": "Back to provider search",
"providerEncountersScope": "These are the encounters this provider is recorded on, which is not the same as the ones they entered or changed — the last column names who did that, and the Users tab searches by it.",
"recordedAsProvider": "Recorded as provider",
"noProviderEncountersMatch": "No encounters for this provider match these filters.",
"noProviderEncounters": "This provider is not recorded on any encounters.",
"searchForProvider": "Search for a provider by name or identifier",
"searchProviderToBegin": "Enter a provider name or identifier to see the encounters they are recorded on.",
"providerSearchResults": "Provider search results",
"noProvidersFound": "No providers match \"{{searchTerm}}\".",
"backToUserSearch": "Back to user search",
"changedBetween": "Changed between",
"readingUserObservations": "Read {{obsRead}} observations so far…",
"noUserEncountersMatch": "This user did not change any observations matching these filters.",
"noUserEncounters": "This user has not recorded or deleted any observations.",
"userScanStoppedEarly": "Stopped after reading {{obsRead}} observation changes without filling the page. Narrow the date range to search further back.",
"partialUserScan": "Part of the trail",
"lastChanged": "Last changed",
"lookingForMoreEncounters": "Looking for more encounters…",
"encounterRange": "Encounters {{from}}–{{to}}",
"searchForUser": "Search for a user by name or username",
"searchUserToBegin": "Enter a name or username to see the encounters that user has modified.",
"userSearchResults": "User search results",
"noUsersFound": "No users match \"{{searchTerm}}\".",
"name": "Name",
"username": "Username",
"retired": "Retired",
"audit": ""
}
118 changes: 118 additions & 0 deletions packages/esm-audit-app/locales/fr/translation.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
{
"searchBy": "Search by",
"patients": "Patients",
"users": "Users",
"providers": "Providers",
"auditTrail": "Audit trail",
"encounterAudit": "Encounter audit",
"encounterNotFound": "That encounter could not be found.",
"patient": "Patient",
"identifier": "Identifier",
"encounterDate": "Encounter date",
"location": "Location",
"visit": "Visit",
"encounterType": "Encounter type",
"form": "Form",
"createdBy": "Created by",
"changedBy": "Changed by",
"deletedBy": "Deleted by",
"deletionReason": "Reason for deletion",
"encounter": "Encounter",
"deleted": "Deleted",
"encounterDeletedSubtitle": "This encounter has been deleted. Its observations are shown as they were left.",
"encounterDeleted": "Deleted encounter",
"encounterSummary": "Encounter summary",
"noProviders": "No providers are recorded for this encounter.",
"role": "Role",
"providerName": "Provider name",
"backToEncounters": "Back to encounters",
"allEncounterTypes": "All encounter types",
"encounterDateRange": "Encounter date range",
"clearFilters": "Clear filters",
"edited": "Edited",
"editedLegend": "the value was changed after it was first recorded",
"addedLater": "Added later",
"addedLaterLegend": "recorded after the encounter itself was created",
"deletedLegend": "removed from the encounter",
"previousValue": "Previous value",
"previousValueLegend": "the value that an edit replaced",
"observations": "Observations",
"showDeletedObs": "Show deleted observations",
"showConceptDescriptions": "Show concept descriptions",
"noObservations": "This encounter has no observations.",
"questionConcept": "Question concept",
"value": "Value",
"change": "Change",
"recordedBy": "Recorded by",
"wasPreviously": "was {{value}}",
"changedByUser": "Changed by {{user}}",
"encounterCreatedAction": "Created encounter",
"encounterChangedAction": "Changed encounter",
"encounterDeletedAction": "Deleted encounter",
"obsRecordedAction": "Recorded observation",
"obsEditedAction": "Edited observation",
"obsDeletedAction": "Deleted observation",
"recordActivity": "Patient activity",
"readingObservations": "Reading observations from {{read}} of {{total}} encounters…",
"noActivity": "There is no recorded activity to show.",
"whoTouchedThisRecord": "Who modified this patient record",
"user": "User",
"encountersCreated": "Encounters created",
"obsRecordedCount": "Observations recorded",
"obsEditedCount": "Observations edited",
"obsDeletedCount": "Observations deleted",
"firstActivity": "First activity",
"lastActivity": "Last activity",
"showOnlyThisUser": "Show only this user's activity",
"unknownUser": "Unknown user",
"activityLog": "Activity log",
"showEveryone": "Show everyone",
"when": "When",
"action": "Action",
"detail": "Detail",
"encounters": "Encounters",
"noEncountersMatchFilters": "No encounters match these filters.",
"noEncountersFound": "This patient has no encounters to audit.",
"provider": "Provider",
"status": "Status",
"backToPatientSearch": "Back to patient search",
"includeDeletedEncounters": "Include deleted encounters",
"cannotIncludeDeletedEncounters": "Deleted encounters can only be listed for a patient who has an identifier.",
"deletedEncountersUnavailable": "Deleted encounters unavailable",
"auditViews": "Audit views",
"searchForPatient": "Search for a patient by name or identifier",
"searchToBegin": "Enter a patient name or identifier to begin auditing their records.",
"patientSearchResults": "Patient search results",
"noPatientsFound": "No patients match \"{{searchTerm}}\".",
"patientName": "Patient name",
"gender": "Gender",
"age": "Age",
"birthdate": "Date of birth",
"backToProviderSearch": "Back to provider search",
"providerEncountersScope": "These are the encounters this provider is recorded on, which is not the same as the ones they entered or changed — the last column names who did that, and the Users tab searches by it.",
"recordedAsProvider": "Recorded as provider",
"noProviderEncountersMatch": "No encounters for this provider match these filters.",
"noProviderEncounters": "This provider is not recorded on any encounters.",
"searchForProvider": "Search for a provider by name or identifier",
"searchProviderToBegin": "Enter a provider name or identifier to see the encounters they are recorded on.",
"providerSearchResults": "Provider search results",
"noProvidersFound": "No providers match \"{{searchTerm}}\".",
"backToUserSearch": "Back to user search",
"changedBetween": "Changed between",
"readingUserObservations": "Read {{obsRead}} observations so far…",
"noUserEncountersMatch": "This user did not change any observations matching these filters.",
"noUserEncounters": "This user has not recorded or deleted any observations.",
"userScanStoppedEarly": "Stopped after reading {{obsRead}} observation changes without filling the page. Narrow the date range to search further back.",
"partialUserScan": "Part of the trail",
"lastChanged": "Last changed",
"lookingForMoreEncounters": "Looking for more encounters…",
"encounterRange": "Encounters {{from}}–{{to}}",
"searchForUser": "Search for a user by name or username",
"searchUserToBegin": "Enter a name or username to see the encounters that user has modified.",
"userSearchResults": "User search results",
"noUsersFound": "No users match \"{{searchTerm}}\".",
"name": "Name",
"username": "Username",
"retired": "Retired",
"audit": ""
}
19 changes: 2 additions & 17 deletions packages/esm-audit-app/src/audit/audit.scss
Original file line number Diff line number Diff line change
Expand Up @@ -170,23 +170,8 @@
background-color: $color-blue-10;
}

/* Paging a list whose length is not known: a range and two buttons, no page count. */
.lazyPagination {
display: flex;
align-items: center;
gap: layout.$spacing-03;
padding: layout.$spacing-03 0;
border-top: 1px solid $ui-03;
}

.paginationRange {
@include type.type-style('label-01');
color: $text-02;
margin-right: auto;
}

/* Sits inline with the paging controls, so the row does not jump as it appears and goes. */
/* Sits above the pager while the trail is still being read, so the list does not look finished. */
.discovering {
min-height: auto;
margin-right: layout.$spacing-03;
margin-bottom: layout.$spacing-03;
}
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ describe('<UserEncounters />', () => {
await screen.findByText('Encounters 1–10');
const beforePaging = auditRequests().length;

await userEvent.click(screen.getByRole('button', { name: 'Next' }));
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));

expect(await screen.findByText('Encounters 11–20')).toBeInTheDocument();
expect(auditRequests().length).toBeGreaterThan(beforePaging);
Expand All @@ -198,11 +198,11 @@ describe('<UserEncounters />', () => {
mockRestApi({ totalCreated: 5000, obsPerEncounter: 20 });
renderUserEncounters();
await screen.findByText('Encounters 1–10');
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
await screen.findByText('Encounters 11–20');
const afterPaging = auditRequests().length;

await userEvent.click(screen.getByRole('button', { name: 'Previous' }));
await userEvent.click(screen.getByRole('button', { name: 'Previous page' }));

expect(await screen.findByText('Encounters 1–10')).toBeInTheDocument();
expect(auditRequests()).toHaveLength(afterPaging);
Expand All @@ -214,10 +214,10 @@ describe('<UserEncounters />', () => {
renderUserEncounters();

await screen.findByText('Encounters 1–10');
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));

expect(await screen.findByText('Encounters 11–12')).toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Next' })).toBeDisabled();
expect(screen.getByRole('button', { name: 'Next page' })).toBeDisabled();
});

it('counts each row’s observations exactly, from the encounter itself', async () => {
Expand Down Expand Up @@ -270,7 +270,7 @@ describe('<UserEncounters />', () => {
mockRestApi({ totalCreated: 5000, obsPerEncounter: 20 });
renderUserEncounters();
await screen.findByText('Encounters 1–10');
await userEvent.click(screen.getByRole('button', { name: 'Next' }));
await userEvent.click(screen.getByRole('button', { name: 'Next page' }));
await screen.findByText('Encounters 11–20');

await userEvent.clear(screen.getByLabelText('Changed between'));
Expand Down
58 changes: 32 additions & 26 deletions packages/esm-audit-app/src/audit/user-encounters.component.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import React, { useState } from 'react';
import React, { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import {
Button,
ComboBox,
DataTableSkeleton,
InlineLoading,
InlineNotification,
Pagination,
Table,
TableBody,
TableCell,
Expand Down Expand Up @@ -37,7 +38,8 @@ interface UserEncountersProps {
export default function UserEncounters({ userUuid, onSelectEncounter, onBackToSearch }: UserEncountersProps) {
const { t } = useTranslation();
const config = useConfig<Config>();
const pageSize = config.encountersPageSize ?? 10;
const [pageSize, setPageSize] = useState(config.encountersPageSize ?? 10);
const pageSizes = useMemo(() => Array.from(new Set([pageSize, 10, 20, 50])).sort((a, b) => a - b), [pageSize]);
const [dateRange, setDateRange] = useState<DateRange>({});
// bumped to clear the range picker, which has to be left to hold its own value
const [datePickerKey, setDatePickerKey] = useState(0);
Expand Down Expand Up @@ -197,35 +199,39 @@ export default function UserEncounters({ userUuid, onSelectEncounter, onBackToSe
</TableBody>
</Table>
</TableContainer>
{/*
A filter is applied after the trail has been read, so matching rows arrive as the scan
goes on. Without this the list looks finished while it is still filling.
*/}
{isDiscovering ? (
<InlineLoading
className={styles.discovering}
description={t('lookingForMoreEncounters', 'Looking for more encounters…')}
/>
) : null}
{/*
How many encounters this account has touched cannot be known without reading its whole
trail, which is the very thing being avoided, so there is no total to page against.
A range and a next button say exactly what is known.
trail, which is the very thing being avoided, so there is no total to page against: the
pager runs with its page count unknown and says only the range on screen.
*/}
<div className={styles.lazyPagination}>
<span className={styles.paginationRange}>
{t('encounterRange', 'Encounters {{from}}–{{to}}', {
<Pagination
isLastPage={!hasNextPage}
itemText={() =>
t('encounterRange', 'Encounters {{from}}–{{to}}', {
from: firstRowOnPage + 1,
to: firstRowOnPage + activity.length,
})}
</span>
{/*
A filter is applied after the trail has been read, so matching rows arrive as the
scan goes on. Without this the list looks finished while it is still filling.
*/}
{isDiscovering ? (
<InlineLoading
className={styles.discovering}
description={t('lookingForMoreEncounters', 'Looking for more encounters…')}
/>
) : null}
<Button disabled={currentPage === 1} kind="ghost" onClick={() => goTo(currentPage - 1)} size="sm">
{t('previousPage', 'Previous')}
</Button>
<Button disabled={!hasNextPage} kind="ghost" onClick={() => goTo(currentPage + 1)} size="sm">
{t('nextPage', 'Next')}
</Button>
</div>
})
}
onChange={({ page: nextPage, pageSize: nextPageSize }) => {
setPageSize(nextPageSize);
goTo(nextPage);
}}
page={currentPage}
pageSize={pageSize}
pageSizes={pageSizes}
pagesUnknown
size="sm"
/>
</>
)}
</div>
Expand Down
Loading
Loading