From 35c1e0003319c7799acd8062bfc0ed42c77f9a70 Mon Sep 17 00:00:00 2001 From: mogoodrich Date: Fri, 27 Sep 2024 16:58:08 -0400 Subject: [PATCH] O3-4043: Dispensing: Allow back dating dispense events for retrospective entry --- src/constants.ts | 2 + .../medication-dispense-review.component.tsx | 54 ++++++++++++++++- .../history-and-comments.component.tsx | 7 +-- .../medication-dispense.resource.test.tsx | 6 +- .../medication-dispense.resource.tsx | 34 +++++------ .../medication-request.resource.tsx | 10 ++-- src/types.ts | 11 ++++ src/utils.test.ts | 60 ++++++++++--------- src/utils.ts | 14 ++--- 9 files changed, 128 insertions(+), 70 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 2553717..4d35401 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -2,6 +2,8 @@ export const spaRoot = window['getOpenmrsSpaBase']; export const basePath = '/dispensing'; export const spaBasePath = `${window.spaBase}${basePath}`; +export const datePickerFormat = 'd/m/Y'; + // defined in FHIR 2 module export const OPENMRS_FHIR_PREFIX = 'http://fhir.openmrs.org'; export const OPENMRS_FHIR_EXT_PREFIX = OPENMRS_FHIR_PREFIX + '/ext'; diff --git a/src/forms/medication-dispense-review.component.tsx b/src/forms/medication-dispense-review.component.tsx index f0048db..659cfc2 100644 --- a/src/forms/medication-dispense-review.component.tsx +++ b/src/forms/medication-dispense-review.component.tsx @@ -1,20 +1,22 @@ import React, { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import { ComboBox, Dropdown, NumberInput, Stack, TextArea } from '@carbon/react'; +import { ComboBox, DatePicker, DatePickerInput, Dropdown, NumberInput, Stack, TextArea } from '@carbon/react'; import { useLayoutType, useConfig, useSession, userHasAccess } from '@openmrs/esm-framework'; import { getConceptCodingUuid, getMedicationReferenceOrCodeableConcept, getOpenMRSMedicineDrugName } from '../utils'; import MedicationCard from '../components/medication-card.component'; import { useMedicationCodeableConcept, useMedicationFormulations } from '../medication/medication.resource'; -import { useMedicationRequest } from '../medication-request/medication-request.resource'; +import { useMedicationRequest, usePrescriptionDetails } from '../medication-request/medication-request.resource'; import { useOrderConfig, + useProviders, useSubstitutionReasonValueSet, useSubstitutionTypeValueSet, } from '../medication-dispense/medication-dispense.resource'; -import { PRIVILEGE_CREATE_DISPENSE_MODIFY_DETAILS } from '../constants'; +import { datePickerFormat, PRIVILEGE_CREATE_DISPENSE_MODIFY_DETAILS } from '../constants'; import { type Medication, type MedicationDispense } from '../types'; import { type PharmacyConfig } from '../config-schema'; import styles from '../components/medication-dispense-review.scss'; +import dayjs from 'dayjs'; interface MedicationDispenseReviewProps { medicationDispense: MedicationDispense; @@ -51,6 +53,7 @@ const MedicationDispenseReview: React.FC = ({ const { orderConfigObject } = useOrderConfig(); const { substitutionTypeValueSet } = useSubstitutionTypeValueSet(config.valueSets.substitutionType.uuid); const { substitutionReasonValueSet } = useSubstitutionReasonValueSet(config.valueSets.substitutionReason.uuid); + const providers = useProviders(); const allowEditing = config.dispenseBehavior.allowModifyingPrescription; @@ -155,6 +158,9 @@ const MedicationDispenseReview: React.FC = ({ config.refreshInterval, ); + // we fetch this just to get the prescription date + const { prescriptionDate } = usePrescriptionDetails(medicationRequest ? medicationRequest.encounter.reference : null); + // check to see if the current dispense would be a substitution, and update accordingly useEffect(() => { if ( @@ -489,6 +495,48 @@ const MedicationDispenseReview: React.FC = ({ }); }} /> + + { + updateMedicationDispense({ + ...medicationDispense, + whenHandedOver: dayjs(date).format(), + }); + }} + value={dayjs(medicationDispense.whenHandedOver).toDate()}> + + + + provider.uuid === medicationDispense?.performer[0].actor.reference.split('/')[1], + ) + : null + } + onChange={({ selectedItem }) => { + updateMedicationDispense({ + ...medicationDispense, + performer: [ + { + actor: { + reference: `Practitioner/${selectedItem?.uuid}`, + }, + }, + ], + }); + }} + items={providers} + itemToString={(item) => item?.person?.display} + titleText={t('dispensedBy', 'Dispensed by')} + /> ); diff --git a/src/history/history-and-comments.component.tsx b/src/history/history-and-comments.component.tsx index 13c8d30..5dfb9e3 100644 --- a/src/history/history-and-comments.component.tsx +++ b/src/history/history-and-comments.component.tsx @@ -20,12 +20,11 @@ import { import { computeNewFulfillerStatusAfterDelete, computeQuantityRemaining, - getDateRecorded, getFulfillerStatus, getMedicationRequestBundleContainingMedicationDispense, getUuidFromReference, revalidate, - sortMedicationDispensesByDateRecorded, + sortMedicationDispensesByWhenHandedOver, } from '../utils'; import PauseDispenseForm from '../forms/pause-dispense-form.component'; import CloseDispenseForm from '../forms/close-dispense-form.component'; @@ -214,7 +213,7 @@ const HistoryAndComments: React.FC<{ {medicationRequestBundles && medicationRequestBundles .flatMap((medicationDispenseBundle) => medicationDispenseBundle.dispenses) - .sort(sortMedicationDispensesByDateRecorded) + .sort(sortMedicationDispensesByWhenHandedOver) .map((dispense) => { return (
@@ -225,7 +224,7 @@ const HistoryAndComments: React.FC<{ fontSize: '0.9rem', }}> {dispense.performer && dispense.performer[0]?.actor?.display} {generateDispenseVerbiage(dispense)} -{' '} - {formatDatetime(parseDate(getDateRecorded(dispense)))} + {formatDatetime(parseDate(dispense.whenHandedOver))} {generateMedicationDispenseActionMenu( diff --git a/src/medication-dispense/medication-dispense.resource.test.tsx b/src/medication-dispense/medication-dispense.resource.test.tsx index de8ec9f..39c38ed 100644 --- a/src/medication-dispense/medication-dispense.resource.test.tsx +++ b/src/medication-dispense/medication-dispense.resource.test.tsx @@ -14,6 +14,7 @@ import { MedicationDispenseStatus, MedicationRequestStatus, } from '../types'; +import dayjs from 'dayjs'; jest.mock('@openmrs/esm-framework', () => { const originalModule = jest.requireActual('@openmrs/esm-framework'); @@ -102,7 +103,6 @@ describe('Medication Dispense Resource tests', () => { // @ts-ignore useSWR.mockImplementation(() => ({ data: { data: 'mockedOrderConfig' } })); const orderConfig = useOrderConfig(); - expect(useSWR).toHaveBeenCalledWith('/ws/rest/v1/orderentryconfig', openmrsFetch); expect(orderConfig.orderConfigObject).toBe('mockedOrderConfig'); }); @@ -227,8 +227,8 @@ describe('Medication Dispense Resource tests', () => { expect(medicationDispense.quantity.system).toBe('http://snomed.info/sct'); expect(medicationDispense.quantity.unit).toBe('Tablet'); expect(medicationDispense.quantity.code).toBe('123456789'); - expect(medicationDispense.whenPrepared).toBeNull(); - expect(medicationDispense.whenHandedOver).toBeNull(); + expect(dayjs(medicationDispense.whenPrepared).isToday()).toBeTruthy(); + expect(dayjs(medicationDispense.whenHandedOver).isToday()).toBeTruthy(); expect(medicationDispense.dosageInstruction[0].text).toBe('Take with food'); expect(medicationDispense.dosageInstruction[0].timing.repeat.duration).toBe(30.0); expect(medicationDispense.dosageInstruction[0].timing.repeat.durationUnit).toBe('d'); diff --git a/src/medication-dispense/medication-dispense.resource.tsx b/src/medication-dispense/medication-dispense.resource.tsx index c703cb7..540266c 100644 --- a/src/medication-dispense/medication-dispense.resource.tsx +++ b/src/medication-dispense/medication-dispense.resource.tsx @@ -1,11 +1,12 @@ -import { fhirBaseUrl, openmrsFetch, type Session } from '@openmrs/esm-framework'; +import { fhirBaseUrl, restBaseUrl, openmrsFetch, type Session } from '@openmrs/esm-framework'; import dayjs from 'dayjs'; import useSWR from 'swr'; import { type MedicationDispense, - MedicationDispenseStatus, + type MedicationDispenseStatus, type MedicationRequest, type OrderConfig, + type ProviderRequestResponse, type ValueSet, } from '../types'; @@ -23,21 +24,9 @@ export function saveMedicationDispense( medicationDispense.status = medicationDispenseStatus; - // timestamp if needed - if (medicationDispenseStatus === MedicationDispenseStatus.completed) { - if (medicationDispense.whenHandedOver === null) { - medicationDispense.whenHandedOver = dayjs(); - } - } + // TODO for now we don't support a different prepared and handed over date, so just set the handed over to the prepared date + medicationDispense.whenPrepared = medicationDispense.whenHandedOver; - if ( - // medicationDispenseStatus === MedicationDispenseStatus.in_progress || NOT YET IMPLEMENTED - medicationDispenseStatus === MedicationDispenseStatus.completed - ) { - if (medicationDispense.whenPrepared === null) { - medicationDispense.whenPrepared = dayjs(); - } - } return openmrsFetch(url, { method: method, signal: abortController.signal, @@ -56,7 +45,7 @@ export function deleteMedicationDispense(medicationDispenseUuid: string) { export function useOrderConfig() { const { data, error, isValidating } = useSWR<{ data: OrderConfig }, Error>( - `/ws/rest/v1/orderentryconfig`, + `${restBaseUrl}/orderentryconfig`, openmrsFetch, ); return { @@ -67,6 +56,14 @@ export function useOrderConfig() { }; } +export function useProviders() { + const { data } = useSWR<{ data: ProviderRequestResponse }, Error>( + `${restBaseUrl}/provider?v=custom:(uuid,person:(display))`, + openmrsFetch, + ); + return data?.data?.results.sort((a, b) => a.person?.display.localeCompare(b.person?.display)); +} + export function useReasonForPauseValueSet(uuid: string) { const valueSet = useValueSet(uuid); return { reasonForPauseValueSet: valueSet }; @@ -120,6 +117,7 @@ export function initiateMedicationDispenseBody( location: { reference: session?.sessionLocation ? `Location/${session.sessionLocation.uuid}` : '', }, + whenHandedOver: dayjs().format(), }; if (populateDispenseInformation) { @@ -131,8 +129,6 @@ export function initiateMedicationDispenseBody( unit: medicationRequest.dispenseRequest?.quantity?.unit, system: medicationRequest.dispenseRequest?.quantity?.system, }, - whenPrepared: null, - whenHandedOver: null, dosageInstruction: [ { // see https://openmrs.atlassian.net/browse/O3-3791 for an explanation for the reason for the below diff --git a/src/medication-request/medication-request.resource.tsx b/src/medication-request/medication-request.resource.tsx index f6a16b7..b7939cc 100644 --- a/src/medication-request/medication-request.resource.tsx +++ b/src/medication-request/medication-request.resource.tsx @@ -17,7 +17,7 @@ import { getMedicationReferenceOrCodeableConcept, getPrescriptionTableActiveMedicationRequestsEndpoint, getPrescriptionTableAllMedicationRequestsEndpoint, - sortMedicationDispensesByDateRecorded, + sortMedicationDispensesByWhenHandedOver, computePrescriptionStatusMessageCode, getAssociatedMedicationDispenses, } from '../utils'; @@ -60,7 +60,7 @@ export function usePrescriptionsTable( const medicationDispenses = entries .filter((entry) => entry?.resource?.resourceType == 'MedicationDispense') .map((entry) => entry.resource as MedicationDispense) - .sort(sortMedicationDispensesByDateRecorded); + .sort(sortMedicationDispensesByWhenHandedOver); prescriptionsTableRows = encounters.map((encounter) => { const medicationRequestsForEncounter = medicationRequests.filter( (medicationRequest) => medicationRequest.encounter.reference == 'Encounter/' + encounter.id, @@ -123,7 +123,7 @@ function buildPrescriptionsTableRow( }; } -export function usePrescriptionDetails(encounterUuid: string, refreshInterval) { +export function usePrescriptionDetails(encounterUuid: string, refreshInterval = null) { const medicationRequestBundles: Array = []; let prescriptionDate: Date; let isLoading = true; @@ -152,13 +152,13 @@ export function usePrescriptionDetails(encounterUuid: string, refreshInterval) { const medicationDispenses = results ?.filter((entry) => entry?.resource?.resourceType == 'MedicationDispense') .map((entry) => entry.resource as MedicationDispense) - .sort(sortMedicationDispensesByDateRecorded); + .sort(sortMedicationDispensesByWhenHandedOver); medicationRequests.every((medicationRequest) => medicationRequestBundles.push({ request: medicationRequest, dispenses: getAssociatedMedicationDispenses(medicationRequest, medicationDispenses).sort( - sortMedicationDispensesByDateRecorded, + sortMedicationDispensesByWhenHandedOver, ), }), ); diff --git a/src/types.ts b/src/types.ts index a89079b..4a06dd3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -422,6 +422,17 @@ export interface Person { uuid: string; } +export interface Provider { + uuid: string; + person: { + display: string; + }; +} + +export interface ProviderRequestResponse { + results: Array; +} + // represents a row in the main table export interface PrescriptionsTableRow { id: string; diff --git a/src/utils.test.ts b/src/utils.test.ts index 22b7151..2ef32c6 100644 --- a/src/utils.test.ts +++ b/src/utils.test.ts @@ -593,8 +593,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T14:00:00-05:00', + whenPrepared: '2023-01-05T14:00:00-05:00', }; const medicationDispenseDeclined: MedicationDispense = { @@ -626,8 +626,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-04T14:00:00-05:00', + whenPrepared: '2023-01-04T14:00:00-05:00', }; const medicationDispenseOnHold: MedicationDispense = { @@ -659,8 +659,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-03T14:00:00-05:00', + whenPrepared: '2023-01-03T14:00:00-05:00', }; const medicationDispenseCompleteOldest: MedicationDispense = { @@ -692,8 +692,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-01T14:00:00-05:00', + whenPrepared: '2023-01-01T14:00:00-05:00', }; test('should return declined if deleting most recent medication dispense and next most recent status declined', () => { @@ -846,8 +846,10 @@ describe('Util Tests', () => { whenPrepared: '', }; - test('when adding new dispense should return null even if dispense meets or exceeds quantitiy if restrict total quantity dispensed config is false', () => { + test('when adding new dispense should return null even if dispense meets or exceeds quantity if restrict total quantity dispensed config is false', () => { newMedicationDispense.extension[0].valueDateTime = '2023-01-03T14:00:00-05:00'; + newMedicationDispense.whenHandedOver = '2023-01-03T14:00:00-05:00'; + newMedicationDispense.whenPrepared = '2023-01-03T14:00:00-05:00'; newMedicationDispense.status = MedicationDispenseStatus.completed; newMedicationDispense.quantity.value = 30; expect( @@ -2193,8 +2195,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T14:00:00-05:00', + whenPrepared: '2023-01-05T14:00:00-05:00', }, { dosageInstruction: undefined, @@ -2225,8 +2227,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T20:00:00-05:00', + whenPrepared: '2023-01-05T20:00:00-05:00', }, { dosageInstruction: undefined, @@ -2257,8 +2259,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T17:00:00-05:00', + whenPrepared: '2023-01-05T17:00:00-05:00', }, ]; @@ -2304,8 +2306,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T14:00:00-05:00', + whenPrepared: '2023-01-05T14:00:00-05:00', }, { dosageInstruction: undefined, @@ -2336,8 +2338,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T20:00:00-05:00', + whenPrepared: '2023-01-05T20:00:00-05:00', }, { dosageInstruction: undefined, @@ -2368,8 +2370,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T17:00:00-05:00', + whenPrepared: '2023-01-05T17:00:00-05:00', }, ]; @@ -2406,8 +2408,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T14:00:00-05:00', + whenPrepared: '2023-01-05T14:00:00-05:00', }, ]; expect(getNextMostRecentMedicationDispenseStatus(medicationDispenses)).toBeNull(); @@ -2755,8 +2757,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T14:00:00-05:00', + whenPrepared: '2023-01-05T14:00:00-05:00', }; const medicationDispense2: MedicationDispense = { @@ -2788,8 +2790,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T20:00:00-05:00', + whenPrepared: '2023-01-05T20:00:00-05:00', }; const medicationDispense3: MedicationDispense = { @@ -2821,8 +2823,8 @@ describe('Util Tests', () => { subject: { display: '', reference: '', type: '' }, substitution: { reason: [], type: undefined, wasSubstituted: false }, type: undefined, - whenHandedOver: '', - whenPrepared: '', + whenHandedOver: '2023-01-05T17:00:00-05:0', + whenPrepared: '2023-01-05T17:00:00-05:0', }; const medicationDispenses: Array = [ diff --git a/src/utils.ts b/src/utils.ts index dc8dfda..4460154 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -414,7 +414,7 @@ export function getMedicationReferenceOrCodeableConcept( export function getMostRecentMedicationDispenseStatus( medicationDispenses: Array, ): MedicationDispenseStatus { - const sorted = medicationDispenses?.sort(sortMedicationDispensesByDateRecorded); + const sorted = medicationDispenses?.sort(sortMedicationDispensesByWhenHandedOver); return sorted && sorted.length > 0 ? sorted[0].status : null; } @@ -425,7 +425,7 @@ export function getMostRecentMedicationDispenseStatus( export function getNextMostRecentMedicationDispenseStatus( medicationDispenses: Array, ): MedicationDispenseStatus { - const sorted = medicationDispenses?.sort(sortMedicationDispensesByDateRecorded); + const sorted = medicationDispenses?.sort(sortMedicationDispensesByWhenHandedOver); return sorted && sorted.length > 1 ? sorted[1].status : null; } @@ -554,7 +554,7 @@ export function isMostRecentMedicationDispense( medicationDispense: MedicationDispense, medicationDispenses: Array, ): boolean { - const sorted = medicationDispenses?.sort(sortMedicationDispensesByDateRecorded); + const sorted = medicationDispenses?.sort(sortMedicationDispensesByWhenHandedOver); // prettier-ignore return medicationDispense && @@ -577,13 +577,13 @@ export function revalidate(encounterUuid: string) { ); } -export function sortMedicationDispensesByDateRecorded(a: MedicationDispense, b: MedicationDispense): number { - if (getDateRecorded(b) === null) { +export function sortMedicationDispensesByWhenHandedOver(a: MedicationDispense, b: MedicationDispense): number { + if (b.whenHandedOver === null) { return 1; - } else if (getDateRecorded(a) === null) { + } else if (a.whenHandedOver === null) { return -1; } - const dateDiff = parseDate(getDateRecorded(b)).getTime() - parseDate(getDateRecorded(a)).getTime(); + const dateDiff = parseDate(b.whenHandedOver).getTime() - parseDate(a.whenHandedOver).getTime(); if (dateDiff !== 0) { return dateDiff; } else {