diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/api/model/ApiTestOrder.java b/backend/src/main/java/gov/cdc/usds/simplereport/api/model/ApiTestOrder.java index 13a12839dc..c8783c85ff 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/api/model/ApiTestOrder.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/api/model/ApiTestOrder.java @@ -30,4 +30,8 @@ public TestCorrectionStatus getCorrectionStatus() { public String getReasonForCorrection() { return wrapped.getReasonForCorrection(); } + + public String getTimerStartedAt() { + return wrapped.getTimerStartedAt(); + } } diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/api/queue/QueueMutationResolver.java b/backend/src/main/java/gov/cdc/usds/simplereport/api/queue/QueueMutationResolver.java index 4901ceaf7b..07d5fd7d10 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/api/queue/QueueMutationResolver.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/api/queue/QueueMutationResolver.java @@ -114,4 +114,10 @@ public void updateAoeQuestions( personService.updateTestResultDeliveryPreference(patientId, testResultDelivery); } } + + @MutationMapping + public void updateTestOrderTimerStartedAt( + @Argument UUID testOrderId, @Argument String startedAt) { + testOrderService.updateTimerStartedAt(testOrderId, startedAt); + } } diff --git a/backend/src/main/java/gov/cdc/usds/simplereport/service/TestOrderService.java b/backend/src/main/java/gov/cdc/usds/simplereport/service/TestOrderService.java index c95345a83c..598b58c615 100644 --- a/backend/src/main/java/gov/cdc/usds/simplereport/service/TestOrderService.java +++ b/backend/src/main/java/gov/cdc/usds/simplereport/service/TestOrderService.java @@ -484,6 +484,18 @@ public void removePatientFromQueue(UUID patientId) { _testOrderRepo.save(order); } + @AuthorizationConfiguration.RequirePermissionUpdateTestForPatient + public void updateTimerStartedAt(UUID id, String startedAt) { + Optional optionalTestOrder = _testOrderRepo.findById(id); + if (optionalTestOrder.isPresent()) { + TestOrder order = optionalTestOrder.get(); + order.setTimerStartedAt(startedAt); + _testOrderRepo.save(order); + } else { + throw new IllegalGraphqlArgumentException("Cannot find TestOrder"); + } + } + private TestOrder retrieveTestOrder(UUID patientId) { Organization org = _organizationService.getCurrentOrganization(); Person patient = _personService.getPatientNoPermissionsCheck(patientId, org); diff --git a/backend/src/main/resources/graphql/main.graphqls b/backend/src/main/resources/graphql/main.graphqls index 81f9d43f16..a8ab20987a 100644 --- a/backend/src/main/resources/graphql/main.graphqls +++ b/backend/src/main/resources/graphql/main.graphqls @@ -279,6 +279,7 @@ type TestOrder { dateTested: DateTime correctionStatus: String reasonForCorrection: String + timerStartedAt: String } type TestResult { @@ -694,6 +695,10 @@ type Mutation { genderOfSexualPartners: [String] testResultDelivery: TestResultDeliveryPreference ): String @requiredPermissions(allOf: ["UPDATE_TEST"]) + updateTestOrderTimerStartedAt( + testOrderId: ID! + startedAt: String + ): String @requiredPermissions(allOf: ["UPDATE_TEST"]) sendPatientLinkSms(internalId: ID!): Boolean @requiredPermissions(allOf: ["UPDATE_TEST"]) sendPatientLinkSmsByTestEventId(testEventId: ID!): Boolean diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/api/queue/QueueMutationResolverTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/api/queue/QueueMutationResolverTest.java new file mode 100644 index 0000000000..99a940c815 --- /dev/null +++ b/backend/src/test/java/gov/cdc/usds/simplereport/api/queue/QueueMutationResolverTest.java @@ -0,0 +1,33 @@ +package gov.cdc.usds.simplereport.api.queue; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +import gov.cdc.usds.simplereport.service.PersonService; +import gov.cdc.usds.simplereport.service.TestOrderService; +import java.util.UUID; +import org.junit.jupiter.api.Test; +import org.springframework.stereotype.Component; + +@Component +class QueueMutationResolverTest { + @Test + void updateTestTimer() { + // GIVEN + TestOrderService testOrderService = mock(TestOrderService.class); + PersonService personService = mock(PersonService.class); + + long currentTime = System.currentTimeMillis(); + String currentTimeString = Long.toString(currentTime); + + var sut = new QueueMutationResolver(testOrderService, personService); + + // WHEN + sut.updateTestOrderTimerStartedAt(UUID.randomUUID(), currentTimeString); + + // THEN + verify(testOrderService).updateTimerStartedAt(any(UUID.class), eq(currentTimeString)); + } +} diff --git a/backend/src/test/java/gov/cdc/usds/simplereport/service/TestOrderServiceTest.java b/backend/src/test/java/gov/cdc/usds/simplereport/service/TestOrderServiceTest.java index 06f1a423b5..200a84b6c3 100644 --- a/backend/src/test/java/gov/cdc/usds/simplereport/service/TestOrderServiceTest.java +++ b/backend/src/test/java/gov/cdc/usds/simplereport/service/TestOrderServiceTest.java @@ -2372,6 +2372,40 @@ void removeTest_sentToReportingService() { verify(testEventReportingService, times(2)).report(any()); } + @Test + @WithSimpleReportOrgAdminUser + void updateTimer_savesCorrectly() { + TestOrder timerOrder = addTestToQueue(); + long currentTime = System.currentTimeMillis(); + String currentTimeString = Long.toString(currentTime); + + // saves a time value + _service.updateTimerStartedAt(timerOrder.getInternalId(), currentTimeString); + TestOrder modifiedTimerOrder = _service.getTestOrder(timerOrder.getInternalId()); + assertEquals(modifiedTimerOrder.getTimerStartedAt(), currentTimeString); + + // saves a null time value + _service.updateTimerStartedAt(timerOrder.getInternalId(), null); + TestOrder modifiedNoTimerOrder = _service.getTestOrder(timerOrder.getInternalId()); + assertNull(modifiedNoTimerOrder.getTimerStartedAt()); + } + + @Test + @WithSimpleReportOrgAdminUser + void updateTimer_testOrderNotFound_throwsException() { + long currentTime = System.currentTimeMillis(); + String currentTimeString = Long.toString(currentTime); + UUID testOrderId = UUID.randomUUID(); + + IllegalGraphqlArgumentException caught = + assertThrows( + IllegalGraphqlArgumentException.class, + () -> { + _service.updateTimerStartedAt(testOrderId, currentTimeString); + }); + assertEquals("Cannot find TestOrder", caught.getMessage()); + } + private List makeAdminData() { var org = _organizationService.createOrganization("Da Org", "airport", "da-org-airport"); _organizationService.setIdentityVerified("da-org-airport", true); diff --git a/frontend/src/app/testQueue/TestCard/TestCard.test.tsx b/frontend/src/app/testQueue/TestCard/TestCard.test.tsx index 9cd50cd29c..a26eed3946 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.test.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.test.tsx @@ -70,6 +70,7 @@ import mockSupportedDiseaseMultiplex, { } from "../mocks/mockSupportedDiseaseMultiplex"; import mockSupportedDiseaseTestPerformedHIV from "../../supportAdmin/DeviceType/mocks/mockSupportedDiseaseTestPerformedHIV"; import mockSupportedDiseaseTestPerformedSyphilis from "../../supportAdmin/DeviceType/mocks/mockSupportedDiseaseTestPerformedSyphilis"; +import { UpdateTestOrderTimerStartedAtDocument } from "../../../generated/graphql"; import { TestCard, TestCardProps } from "./TestCard"; @@ -414,6 +415,69 @@ describe("TestCard", () => { expect(await screen.findByTestId("timer")).toHaveTextContent("Start timer"); }); + it("updates the test order timer started at value when the timer is clicked", async () => { + const currentTime = Date.now(); + const props = { ...testProps }; + props.testOrder.timerStartedAt = currentTime.toString(); + const { user } = await renderQueueItem({ + props: props, + mocks: [ + { + request: { + query: UpdateTestOrderTimerStartedAtDocument, + variables: { testOrderId: props.testOrder.internalId }, + }, + result: { data: { updateTestOrderTimerStartedAt: null } }, + }, + ], + }); + + const timerButton = await screen.findByTestId("timer"); + expect(timerButton).toHaveTextContent("15:00"); + + await user.click(timerButton); + expect(timerButton).toHaveTextContent("Start timer"); + }); + + it("handles a null timer started at value", async () => { + const currentTime = Date.now(); + global.Date.now = jest.fn(() => new Date(currentTime).getTime()); + const props = { ...testProps }; + props.testOrder.timerStartedAt = null; + const { user } = await renderQueueItem({ + props: props, + mocks: [ + { + request: { + query: UpdateTestOrderTimerStartedAtDocument, + variables: { + testOrderId: props.testOrder.internalId, + startedAt: currentTime.toString(), + }, + }, + result: { data: { updateTestOrderTimerStartedAt: null } }, + }, + { + request: { + query: UpdateTestOrderTimerStartedAtDocument, + variables: { testOrderId: props.testOrder.internalId }, + }, + result: { data: { updateTestOrderTimerStartedAt: null } }, + }, + ], + }); + + const timerButton = await screen.findByTestId("timer"); + expect(timerButton).toHaveTextContent("Start timer"); + + await user.click(timerButton); + expect(timerButton).toHaveTextContent("15:00"); + + await user.click(timerButton); + expect(timerButton).toHaveTextContent("Start timer"); + global.Date.now = Date.now; + }); + it("renders dropdown of device types", async () => { const { user } = await renderQueueItem({ mocks: [blankUpdateAoeEventMock], diff --git a/frontend/src/app/testQueue/TestCard/TestCard.tsx b/frontend/src/app/testQueue/TestCard/TestCard.tsx index d8546314d3..e0b7aa5a1d 100644 --- a/frontend/src/app/testQueue/TestCard/TestCard.tsx +++ b/frontend/src/app/testQueue/TestCard/TestCard.tsx @@ -14,6 +14,8 @@ import { QueriedFacility, QueriedTestOrder, } from "../TestCardForm/types"; +import { useUpdateTestOrderTimerStartedAtMutation } from "../../../generated/graphql"; +import { getAppInsights } from "../../TelemetryService"; import { CloseTestCardModal } from "./CloseTestCardModal"; @@ -39,11 +41,16 @@ export const TestCard = ({ const navigate = useNavigate(); const timer = useTestTimer( testOrder.internalId, - testOrder.deviceType.testLength + testOrder.deviceType.testLength, + testOrder.timerStartedAt === null + ? undefined + : Number(testOrder.timerStartedAt) ); const organization = useSelector( (state: any) => state.organization as Organization ); + const [updateTestOrderTimerStartedAt] = + useUpdateTestOrderTimerStartedAtMutation(); const [isCloseModalOpen, setIsCloseModalOpen] = useState(false); @@ -59,6 +66,8 @@ export const TestCard = ({ // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const appInsights = getAppInsights(); + const timerContext = { organizationName: organization.name, facilityName: facility!.name, @@ -66,6 +75,12 @@ export const TestCard = ({ testOrderId: testOrder.internalId, }; + const trackTimerReset = () => { + if (appInsights) { + appInsights.trackEvent({ name: "Test timer reset" }, timerContext); + } + }; + const { patientFullName, patientDateOfBirth } = useTestOrderPatient(testOrder); @@ -76,6 +91,24 @@ export const TestCard = ({ removeTimer(testOrder.internalId); }; + const saveStartedAtCallback = async (startedAt: number | undefined) => { + const response = await updateTestOrderTimerStartedAt({ + variables: { + testOrderId: testOrder.internalId, + startedAt: startedAt?.toString(), + }, + }); + if (!response.data) { + throw Error("updateTestOrderTimerStartedAt null response data"); + } + if (startedAt === undefined) { + timer.reset(trackTimerReset); + testOrder.timerStartedAt = "0"; + } else { + testOrder.timerStartedAt = startedAt?.toString(); + } + }; + return ( <>
- +