Skip to content

Commit

Permalink
(feat) O3-3259 Add ability to deduct stock items while performing dis…
Browse files Browse the repository at this point in the history
…pensing
  • Loading branch information
donaldkibet committed May 21, 2024
1 parent 1511795 commit cfb6d33
Show file tree
Hide file tree
Showing 9 changed files with 232 additions and 566 deletions.
6 changes: 6 additions & 0 deletions src/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ export const configSchema = {
},
},
},
enableStockDispense: {
_type: Type.Boolean,
_description: 'To enable or disable the deduction of stock during the dispensing process',
_default: true,
},
};

export interface PharmacyConfig {
Expand Down Expand Up @@ -162,4 +167,5 @@ export interface PharmacyConfig {
uuid: string;
};
};
enableStockDispense: boolean;
}
1 change: 1 addition & 0 deletions src/declarations.d.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ declare module '*.css';
declare module '*.scss';
declare module '*.svg';
declare module '@carbon/react';
declare type SideNavProps = {};
62 changes: 53 additions & 9 deletions src/forms/dispense-form.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import {
import { Button, FormLabel, InlineLoading } from '@carbon/react';
import styles from './forms.scss';
import { closeOverlay } from '../hooks/useOverlay';
import { type MedicationDispense, MedicationDispenseStatus, type MedicationRequestBundle } from '../types';
import {
type MedicationDispense,
MedicationDispenseStatus,
type MedicationRequestBundle,
type InventoryItem,
} from '../types';
import { saveMedicationDispense } from '../medication-dispense/medication-dispense.resource';
import MedicationDispenseReview from './medication-dispense-review.component';
import {
Expand All @@ -22,6 +27,8 @@ import {
} from '../utils';
import { updateMedicationRequestFulfillerStatus } from '../medication-request/medication-request.resource';
import { type PharmacyConfig } from '../config-schema';
import StockDispense from './stock-dispense/stock-dispense.component';
import { createStockDispenseRequestPayload, sendStockDispenseRequest } from './stock-dispense/useDispenseStock';

interface DispenseFormProps {
medicationDispense: MedicationDispense;
Expand All @@ -45,6 +52,9 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
const { patient, isLoading } = usePatient(patientUuid);
const config = useConfig<PharmacyConfig>();

// Keep track of inventory item
const [inventoryItem, setInventoryItem] = useState<InventoryItem>();

// Keep track of medication dispense payload
const [medicationDispensePayload, setMedicationDispensePayload] = useState<MedicationDispense>();

Expand All @@ -67,6 +77,30 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
medicationRequestBundle,
config.dispenseBehavior.restrictTotalQuantityDispensed,
);

if (config.enableStockDispense) {
const stockDispenseRequestPayload = createStockDispenseRequestPayload(
inventoryItem,
patientUuid,
encounterUuid,
medicationDispensePayload,
);
sendStockDispenseRequest(stockDispenseRequestPayload, abortController).then(
() => {
showToast({
title: t('stockDispensed', 'Stock dispensed'),
kind: 'success',
description: t(
'stockDispensedSuccessfully',
'Stock dispensed successfully and batch level updated.',
),
});
},
(error) => {
showToast({ title: 'Stock dispense error', kind: 'error', description: error?.message });
},
);
}
if (getFulfillerStatus(medicationRequestBundle.request) !== newFulfillerStatus) {
return updateMedicationRequestFulfillerStatus(
getUuidFromReference(
Expand Down Expand Up @@ -134,7 +168,9 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
useEffect(() => setMedicationDispensePayload(medicationDispense), [medicationDispense]);

// check is valid on any changes
useEffect(checkIsValid, [medicationDispensePayload, quantityRemaining]);
useEffect(checkIsValid, [medicationDispensePayload, quantityRemaining, inventoryItem]);

const isButtonDisabled = (config.enableStockDispense ? !inventoryItem : false) || !isValid || isSubmitting;

const bannerState = useMemo(() => {
if (patient) {
Expand All @@ -159,7 +195,6 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
)}
{patient && <ExtensionSlot name="patient-header-slot" state={bannerState} />}
<section className={styles.formGroup}>
{/* <span style={{ marginTop: "1rem" }}>1. {t("drug", "Drug")}</span>*/}
<FormLabel>
{t(
config.dispenseBehavior.allowModifyingPrescription ? 'drugHelpText' : 'drugHelpTextNoEdit',
Expand All @@ -169,18 +204,27 @@ const DispenseForm: React.FC<DispenseFormProps> = ({
)}
</FormLabel>
{medicationDispensePayload ? (
<MedicationDispenseReview
medicationDispense={medicationDispensePayload}
updateMedicationDispense={setMedicationDispensePayload}
quantityRemaining={quantityRemaining}
/>
<div>
<MedicationDispenseReview
medicationDispense={medicationDispensePayload}
updateMedicationDispense={setMedicationDispensePayload}
quantityRemaining={quantityRemaining}
/>
{config.enableStockDispense && (
<StockDispense
inventoryItem={inventoryItem}
medicationDispense={medicationDispense}
updateInventoryItem={setInventoryItem}
/>
)}
</div>
) : null}
</section>
<section className={styles.buttonGroup}>
<Button disabled={isSubmitting} onClick={() => closeOverlay()} kind="secondary">
{t('cancel', 'Cancel')}
</Button>
<Button disabled={!isValid || isSubmitting} onClick={handleSubmit}>
<Button disabled={isButtonDisabled} onClick={handleSubmit}>
{t(
mode === 'enter' ? 'dispensePrescription' : 'saveChanges',
mode === 'enter' ? 'Dispense prescription' : 'Save changes',
Expand Down
63 changes: 63 additions & 0 deletions src/forms/stock-dispense/stock-dispense.component.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from 'react';
import { ComboBox, InlineLoading, InlineNotification, Layer } from '@carbon/react';
import { type MedicationDispense, type InventoryItem } from '../../types';
import { useDispenseStock } from './useDispenseStock';
import { formatDate } from '@openmrs/esm-framework';
import { useTranslation } from 'react-i18next';

type StockDispenseProps = {
medicationDispense: MedicationDispense;
updateInventoryItem: (inventoryItem: InventoryItem) => void;
inventoryItem: InventoryItem;
};

const StockDispense: React.FC<StockDispenseProps> = ({ medicationDispense, updateInventoryItem }) => {
const { t } = useTranslation();
const drugUuid = medicationDispense?.medicationReference?.reference?.split('/')[1];
const { inventoryItems, error, isLoading } = useDispenseStock(drugUuid);

const toStockDispense = (inventoryItems) => {
return t(
'stockDispenseDetails',
'Batch: {{batchNumber}} - Quantity: {{quantity}} ({{quantityUoM}}) - Expiry: {{expiration}}',
{
batchNumber: inventoryItems.batchNumber,
quantity: Math.floor(inventoryItems.quantity),
quantityUoM: inventoryItems.quantityUoM,
expiration: formatDate(new Date(inventoryItems.expiration)),
},
);
};

if (error) {
return (
<InlineNotification
aria-label="closes notification"
kind="error"
lowContrast={true}
statusIconDescription="notification"
subtitle={t('errorLoadingInventoryItems', 'Error fetching inventory items')}
title={t('error', 'Error')}
/>
);
}

if (isLoading) {
return <InlineLoading description={t('loadingInventoryItems', 'Loading inventory items...')} />;
}

return (
<Layer>
<ComboBox
id="stockDispense"
items={inventoryItems}
onChange={({ selectedItem }) => updateInventoryItem(selectedItem)}
itemToString={(item) => (item ? toStockDispense(item) : '')}
titleText={t('stockDispense', 'Stock Dispense')}
placeholder={t('selectStockDispense', 'Select stock to dispense from')}
/>
</Layer>
);
};

export default StockDispense;
78 changes: 78 additions & 0 deletions src/forms/stock-dispense/useDispenseStock.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import useSWR from 'swr';
import { openmrsFetch } from '@openmrs/esm-framework';
import { type InventoryItem, type MedicationDispense } from '../../types';
import { getUuidFromReference } from '../../utils';

//TODO: Add configuration to retrieve the stock dispense endpoint
/**
* Fetches the inventory items for a given drug UUID.
*
* @param {string} drugUuid - The UUID of the drug.
* @returns {Array} - The inventory items.
*/
export const useDispenseStock = (drugUuid: string) => {
const url = `/ws/rest/v1/stockmanagement/stockiteminventory?v=default&limit=10&totalCount=true&drugUuid=${drugUuid}&includeBatchNo=true`;
const { data, error, isLoading } = useSWR<{ data: { results: Array<InventoryItem> } }>(url, openmrsFetch);
return { inventoryItems: data?.data?.results ?? [], error, isLoading };
};

/**
* Interface for the stock dispense request object.
*/
export type StockDispenseRequest = {
locationUuid: string;
patientUuid: string;
orderUuid: string;
encounterUuid: string;
stockItemUuid: string;
stockBatchUuid: string;
quantity?: number;
};

/**
* Sends a POST request to the inventory dispense endpoint with the provided stock dispense request.
*
* @param {AbortController} abortController - The AbortController used to cancel the request.
* @returns {Promise<Response>} - A Promise that resolves to the response of the POST request.
*/
export async function sendStockDispenseRequest(
stockDispenseRequest,
abortController: AbortController,
): Promise<Response> {
const url = '/ws/rest/v1/stockmanagement/dispenserequest';
return await openmrsFetch(url, {
method: 'POST',
signal: abortController.signal,
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ dispenseItems: [stockDispenseRequest] }),
});
}

/**
* Creates a stock dispense request payload.
*
* @param inventoryItem - The inventory item to dispense.
* @param patientUuid - The UUID of the patient.
* @param encounterUuid - The UUID of the encounter.
* @param medicationDispensePayload - The medication dispense payload.
* @returns The stock dispense request payload.
*/
export const createStockDispenseRequestPayload = (
inventoryItem: InventoryItem,
patientUuid: string,
encounterUuid: string,
medicationDispensePayload: MedicationDispense,
) => {
return {
dispenseLocation: inventoryItem.locationUuid,
patient: patientUuid,
order: getUuidFromReference(medicationDispensePayload.authorizingPrescription[0].reference),
encounter: encounterUuid,
stockItem: inventoryItem?.stockItemUuid,
stockBatch: inventoryItem.stockBatchUuid,
stockItemPackagingUOM: inventoryItem.quantityUoMUuid,
quantity: medicationDispensePayload.quantity.value,
};
};
1 change: 1 addition & 0 deletions src/location/location.resource.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const pharmacyConfig: PharmacyConfig = {
substitutionReason: { uuid: '' },
substitutionType: { uuid: '' },
},
enableStockDispense: false,
};

describe('Location Resource tests', () => {
Expand Down
3 changes: 2 additions & 1 deletion src/routes.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
{
"backendDependencies": {
"fhir2": ">=1.2",
"webservices.rest": "^2.2.0"
"webservices.rest": "^2.2.0",
"stockmanagement": "^1.0.0"
},
"pages": [
{
Expand Down
24 changes: 24 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,3 +475,27 @@ export interface ValueSet {
];
};
}

export type InventoryItem = {
partyUuid: string;
locationUuid: string;
partyName: string;
stockItemUuid: string | null;
drugId: string | null;
drugUuid: string | null;
drugStrength: string | null;
conceptId: string | null;
conceptUuid: string | null;
stockBatchUuid: string;
batchNumber: string;
quantity: number;
quantityUoM: string;
quantityFactor: number;
quantityUoMUuid: string;
expiration: string;
commonName: string | null;
acronym: string | null;
drugName: string | null;
conceptName: string | null;
resourceVersion: string;
};
Loading

0 comments on commit cfb6d33

Please sign in to comment.