diff --git a/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.component.test.tsx b/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.component.test.tsx new file mode 100644 index 0000000..5cb5fc7 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.component.test.tsx @@ -0,0 +1,91 @@ +import { render } from '@testing-library/react'; +import React from 'react'; +import ActionButtons from './action-buttons.component'; +import { type MedicationRequest, MedicationRequestStatus, NonDrugMedicationDispense } from '../types'; +import { toDateObjectStrict, useConfig } from '@openmrs/esm-framework'; +import { date } from 'zod'; + +const mockedUseConfig = useConfig as jest.Mock; +const mockPatientUuid = '558494fe-5850-4b34-a3bf-06550334ba4a'; +const mockEncounterUuid = '7aee7123-9e50-4f72-a636-895d77a63e98'; + +describe('Action Buttons Component tests', () => { + beforeEach(() => { + mockedUseConfig.mockReturnValue({ + medicationRequestExpirationPeriodInDays: 90, + actionButtons: { + pauseButton: { + enabled: true, + }, + closeButton: { + enabled: true, + }, + }, + dispenseBehavior: { + allowModifyingPrescription: false, + restrictTotalQuantityDispensed: false, + }, + }); + }); + + test('component should render dispense button if active medication', () => { + // status = active, and validity period start set to current datetime + const medicationRequest: NonDrugMedicationDispense = { + uuid: 'd4f69a68-1171-4e47-8693-478df18daf40', + patient: 'd4f69a68-1171-4e47-8693-478df18daf40', + encounter: 'd4f69a68-1171-4e47-8693-478df18daf40', + dispensingUnit: { uuid: 'd4f69a68-1171-4e47-8693-478df18daf40', display: 'Test' }, + quantity: 2, + display: 'test ', + instrucions: 'test', + status: '', + medicalSupplyOrder: 'd4f69a68-1171-4e47-8693-478df18daf40', + concept: '', + dateDispensed: new Date(), + statusReason: '', + location: '', + encounters: '', + dispenser: '', + }; + + const { getByText, container } = render( + , + ); + expect(getByText('Dispense')).toBeInTheDocument(); + }); + + // status = active, but validity period start time years in the past + test('component should not render dispense button if expired medication', () => { + // status = active, and validity period start set to current datetime + const medicationRequest: NonDrugMedicationDispense = { + uuid: 'd4f69a68-1171-4e47-8693-478df18daf40', + patient: 'd4f69a68-1171-4e47-8693-478df18daf40', + encounter: 'd4f69a68-1171-4e47-8693-478df18daf40', + dispensingUnit: { uuid: 'd4f69a68-1171-4e47-8693-478df18daf40', display: 'Test' }, + quantity: 2, + display: 'test ', + instrucions: 'test', + status: '', + medicalSupplyOrder: 'd4f69a68-1171-4e47-8693-478df18daf40', + concept: '', + dateDispensed: new Date(), + statusReason: '', + location: '', + encounters: '', + dispenser: '', + }; + + const { queryByText, container } = render( + , + ); + // expect(queryByText('Dispense')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.component.tsx b/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.component.tsx new file mode 100644 index 0000000..106fd1c --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.component.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Button } from '@carbon/react'; +import { useTranslation } from 'react-i18next'; +import { useConfig, useSession } from '@openmrs/esm-framework'; +import { + MedicationDispenseStatus, + type MedicationRequestBundle, + MedicationRequestStatus, + NonDrugMedicationDispense, +} from '../types'; +import { launchOverlay } from '../hooks/useOverlay'; +import { + computeMedicationRequestStatus, + computeQuantityRemaining, + getMostRecentMedicationDispenseStatus, +} from '../utils'; +import { type PharmacyConfig } from '../config-schema'; +import { initiateMedicationDispenseBody, useProviders } from '../medication-dispense/medication-dispense.resource'; +import DispenseForm from '../forms/dispense-form.component'; +import PauseDispenseForm from '../forms/pause-dispense-form.component'; +import CloseDispenseForm from '../forms/close-dispense-form.component'; +import styles from './action-buttons.scss'; + +interface ActionButtonsProps { + patientUuid: string; + encounterUuid: string; + medicationDispense: NonDrugMedicationDispense; +} + +const ActionButtons: React.FC = ({ patientUuid, encounterUuid, medicationDispense }) => { + const { t } = useTranslation(); + const config = useConfig(); + const session = useSession(); + const providers = useProviders(config.dispenserProviderRoles); + + return ( +
+ {medicationDispense.uuid ? ( + + ) : null} + {medicationDispense.uuid ? ( + + ) : null} + {medicationDispense.uuid ? ( + + ) : null} +
+ ); +}; + +export default ActionButtons; diff --git a/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.scss b/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.scss new file mode 100644 index 0000000..c133916 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/action-buttons.scss @@ -0,0 +1,5 @@ +.actionBtns { + float: right; + margin-left: 1rem; + margin-top: 2rem; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/components/medication-card.component.test.tsx b/packages/esm-medical-supply-dispensing-app/src/components/medication-card.component.test.tsx new file mode 100644 index 0000000..dbb3455 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/medication-card.component.test.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { type MedicationReferenceOrCodeableConcept } from '../types'; +import MedicationCard from './medication-card.component'; + +describe('Medication Card Component tests', () => { + test('component should render medication card without edit action button', () => { + const medication: MedicationReferenceOrCodeableConcept = { + medicationReference: { + display: 'Some Medication', + reference: '', + type: '', + }, + }; + + const { getByText, container } = render(); + expect(getByText('Some Medication')).toBeInTheDocument(); + expect(container.querySelector('svg')).not.toBeInTheDocument(); + }); + + test('component should render medication card with edit action button', () => { + const medication: MedicationReferenceOrCodeableConcept = { + medicationReference: { + display: 'Some Medication', + reference: '', + type: '', + }, + }; + + const action = () => 0; + + const { getByText, container } = render(); + expect(getByText('Some Medication')).toBeInTheDocument(); + expect(container.querySelector('svg')).toBeInTheDocument(); + }); +}); diff --git a/packages/esm-medical-supply-dispensing-app/src/components/medication-card.component.tsx b/packages/esm-medical-supply-dispensing-app/src/components/medication-card.component.tsx new file mode 100644 index 0000000..92808d3 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/medication-card.component.tsx @@ -0,0 +1,22 @@ +import React, { ReactSVGElement } from 'react'; +import { Tile } from '@carbon/react'; +import { Edit } from '@carbon/react/icons'; +import { type MedicationReferenceOrCodeableConcept } from '../types/index'; +import styles from './medication-card.scss'; +import { getMedicationDisplay } from '../utils'; + +const MedicationCard: React.FC<{ + medication: MedicationReferenceOrCodeableConcept; + editAction?: Function; +}> = ({ medication, editAction }) => { + return ( + +

+ {getMedicationDisplay(medication)} +

+ {editAction && } />} +
+ ); +}; + +export default MedicationCard; diff --git a/packages/esm-medical-supply-dispensing-app/src/components/medication-card.scss b/packages/esm-medical-supply-dispensing-app/src/components/medication-card.scss new file mode 100644 index 0000000..632af2d --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/medication-card.scss @@ -0,0 +1,20 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.medicationTile { + display: flex; + flex-direction: row; + justify-content: space-between; + width: 100%; + margin: 2px 0 8px; + padding: 0 8px 0 8px; + background-color: #fff; + border-left: 4px solid var(--brand-03); + color: $text-02; + margin-bottom: 1rem !important; +} + +.medicationName { + font-size: 15px !important; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/components/medication-dispense-review.scss b/packages/esm-medical-supply-dispensing-app/src/components/medication-dispense-review.scss new file mode 100644 index 0000000..83243e8 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/medication-dispense-review.scss @@ -0,0 +1,95 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.medicationDispenseReviewContainer { + display: flex; + flex-direction: column; + padding-bottom: 10px; + + :global(.cds--css-grid) { + padding-left: 0 !important; + padding-right: 0 !important; + } +} + +.dispenseDetailsContainer { + display: flex; + flex-direction: row; + flex: 1; + gap: spacing.$spacing-05; +} + +.substitutionReason { + width: 100%; +} + +.substitutionType { + width: 100%; +} + +.productiveHeading02 { + color: $color-gray-70; + @include type.type-style('productive-heading-02'); +} + +:global(.omrs-breakpoint-lt-desktop) .formWrapper { + background-color: $openmrs-background-grey; +} + +:global(.omrs-breakpoint-gt-tablet) .formWrapper { + background-color: $ui-02; +} + +.formGroup { + display: flex; + margin-bottom: spacing.$spacing-02; + padding: spacing.$spacing-05; +} + +:global(.omrs-breakpoint-lt-desktop) .formGroup > span { + flex: 1; +} + +:global(.omrs-breakpoint-lt-desktop) .formGroup > div { + flex: 3; +} + +.formGroup span { + @extend .productiveHeading02; +} + +.patientInfo { + position: sticky; + z-index: 1000; + background-color: $ui-02; + top: 3rem; + overflow-y: auto; +} + +:global(.omrs-breakpoint-lt-desktop) .formGroup { + flex-direction: row; +} + +:global(.omrs-breakpoint-gt-tablet) .formGroup { + flex-direction: column; +} + +.buttonGroup { + display: flex; + position: sticky; + bottom: 0; + width: 100%; +} + +:global(.omrs-breakpoint-lt-desktop) .buttonGroup { + padding: spacing.$spacing-05 spacing.$spacing-06; + background-color: $ui-02; +} + +.buttonGroup button { + max-width: none; + width: 50%; + height: spacing.$spacing-10; + align-items: flex-start; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/components/medication-event.component.tsx b/packages/esm-medical-supply-dispensing-app/src/components/medication-event.component.tsx new file mode 100644 index 0000000..7364b80 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/medication-event.component.tsx @@ -0,0 +1,36 @@ +import React from 'react'; +import { + NonDrugDispensingUnit, + NonDrugMedicationDispense, + type DosageInstruction, + type MedicationDispense, + type MedicationRequest, + type Quantity, +} from '../types'; +import styles from './medication-event.scss'; + +import { useTranslation } from 'react-i18next'; + +// can render MedicationRequest or MedicationDispense +const MedicationEvent: React.FC<{ medicationDispense: NonDrugMedicationDispense }> = ({ medicationDispense }) => { + const { t } = useTranslation(); + + return ( +
+

+ {medicationDispense.display} +

+ +

+ {t('quantity', 'Quantity').toUpperCase()}{' '} + : {String(medicationDispense.quantity)} +

+

+ {t('dispenseUnit', 'Dispense Unit').toUpperCase()}{' '} + : {String(medicationDispense.dispensingUnit?.display)} +

+
+ ); +}; + +export default MedicationEvent; diff --git a/packages/esm-medical-supply-dispensing-app/src/components/medication-event.scss b/packages/esm-medical-supply-dispensing-app/src/components/medication-event.scss new file mode 100644 index 0000000..96edb38 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/medication-event.scss @@ -0,0 +1,18 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.medicationName { + font-size: 15px !important; +} + +.bodyLong01 { + font-size: 13px !important; +} + +.dosage, +.quantity, +.refills { + color: #525252; + font-weight: bold; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/components/patient-details.component.tsx b/packages/esm-medical-supply-dispensing-app/src/components/patient-details.component.tsx new file mode 100644 index 0000000..edc66ef --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/patient-details.component.tsx @@ -0,0 +1,72 @@ +import React, { useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +import { attach, detach, ExtensionSlot, type PatientUuid, usePatient } from '@openmrs/esm-framework'; +import styles from './patient-details.scss'; + +const PatientDetails: React.FC<{ + patientUuid: PatientUuid; +}> = ({ patientUuid }) => { + const { t } = useTranslation(); + const { patient } = usePatient(patientUuid); + + const patientName = patient; + const patientPhotoSlotState = React.useMemo(() => ({ patientUuid, patientName }), [patientUuid, patientName]); + + const [showContactDetails, setShowContactDetails] = React.useState(false); + const toggleContactDetails = React.useCallback((event: MouseEvent) => { + event.stopPropagation(); + setShowContactDetails((value) => !value); + }, []); + + const patientAvatar = ( +
+ +
+ ); + + useEffect(() => { + attach('dispensing-patient-banner-slot', 'patient-banner'); + attach('dispensing-patient-vitals-slot', 'vitals-overview-widget'); + attach('dispensing-patient-allergies-slot', 'allergies-overview-widget'); + + return () => { + detach('dispensing-patient-banner-slot', 'patient-banner'); + detach('dispensing-patient-vitals-slot', 'vitals-overview-widget'); + detach('dispensing-patient-allergies-slot', 'allergies-overview-widget'); + }; + }, []); + + return ( +
+ {patient && ( +
+ + + + + +
+ )} +
+ ); +}; + +export default PatientDetails; diff --git a/packages/esm-medical-supply-dispensing-app/src/components/patient-details.scss b/packages/esm-medical-supply-dispensing-app/src/components/patient-details.scss new file mode 100644 index 0000000..7f36846 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/components/patient-details.scss @@ -0,0 +1,93 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +.patientDetailsContainer > div { + margin-bottom: 1rem; +} + +.container { + border-bottom: 0.063rem solid $ui-03; + background-color: $ui-02; + display: grid; + grid-template-columns: 1fr auto; +} + +.patientBanner { + text-decoration: none; + display: grid; + grid-template-columns: auto 1fr; +} + +.patientName { + @include type.type-style('heading-03'); + margin-right: 0.25rem; +} + +.patientAvatar { + width: 5rem; + height: 5rem; + margin: 1rem; + border-radius: 1px; +} + +.patientAvatarButton { + cursor: pointer; + border: none; + padding: 0px; + background: none; +} + +.patientInfo { + width: 100%; + padding-right: 1rem; +} + +.demographics { + @include type.type-style('body-compact-02'); + color: $text-02; + margin-top: 0.375rem; +} + +.row { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: flex-start; +} + +.patientNameRow { + margin-top: spacing.$spacing-05; +} + +.flexRow { + display: flex; + flex-flow: row wrap; + align-items: center; +} + +.identifiers { + @include type.type-style('body-compact-02'); + color: $ui-04; + margin-top: 0.375rem; +} + +.tooltipPadding { + padding: 0.25rem; +} + +.tooltipSmallText { + font-size: 80%; +} + +.actionsButtonText { + @include type.type-style('body-compact-01'); + color: $interactive-01; +} + +.buttonCol { + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: flex-end; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/config-schema.ts b/packages/esm-medical-supply-dispensing-app/src/config-schema.ts index 11f523b..4601451 100644 --- a/packages/esm-medical-supply-dispensing-app/src/config-schema.ts +++ b/packages/esm-medical-supply-dispensing-app/src/config-schema.ts @@ -1 +1,197 @@ -export const configSchema = {}; +import { Type } from '@openmrs/esm-framework'; + +/** + * This is the config schema. It expects a configuration object which + * looks like this: + * + * ```json + * { "casualGreeting": true, "whoToGreet": ["Mom"] } + * ``` + * + * In OpenMRS Microfrontends, all config parameters are optional. Thus, + * all elements must have a reasonable default. A good default is one + * that works well with the reference application. + * + * To understand the schema below, please read the configuration system + * documentation: + * https://openmrs.github.io/openmrs-esm-core/#/main/config + * Note especially the section "How do I make my module configurable?" + * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=im-developing-an-esm-module-how-do-i-make-it-configurable + * and the Schema Reference + * https://openmrs.github.io/openmrs-esm-core/#/main/config?id=schema-reference + */ +export const configSchema = { + appName: { + _type: Type.String, + _default: 'Medical Supplies Dispensing', + }, + + orders: { + medicalSupplyOrderTypeUuid: { + _type: Type.UUID, + _description: "UUID for the 'Medical Supply' order type", + _default: 'dab3ab30-2feb-48ec-b4af-8332a0831b49', //dab3ab30-2feb-48ec-b4af-8332a0831b49 + }, + medicalSupplyOrderableConcepts: { + _type: Type.Array, + _description: + 'UUIDs of concepts that represent orderable medical supply. If an empty array `[]` is provided, every concept with class `Medical supply` will be considered orderable.', + _elements: { + _type: Type.UUID, + }, + _default: [], + }, + }, + + actionButtons: { + pauseButton: { + enabled: { + _type: Type.Boolean, + _description: 'Enabled/Disable including a Pause button in the button action bar', + _default: true, + }, + }, + closeButton: { + enabled: { + _type: Type.Boolean, + _description: 'Enabled/Disable including a Close button in the button action bar', + _default: true, + }, + }, + }, + dispenseBehavior: { + allowModifyingPrescription: { + _type: Type.Boolean, + _description: + 'Enable/Disable editing the prescription. If Disabled, Quantity will be he only editable field on prescription form. Note that thins means that quantity units will need to be mandatory and set correctly on the prescription.', + _default: true, + }, + restrictTotalQuantityDispensed: { + _type: Type.Boolean, + _description: + 'Enable/Disable restricting dispensing quantity greater than total quantity ordered. Marks prescription as complete when total quantity dispensed. If true, allowModifyingPrescription *must* be false, as this functionality relies solely on numeric quantity and assumes no change in formulation, dosage, unit, etc', + _default: false, + }, + }, + dispenserProviderRoles: { + _type: Type.Array, + _description: + 'Array of provider roles uuids. If specified, only providers with these roles will be listed in the "Dispensed By" dropdown. Note that this simply restricts the providers that can be recorded as Dispensers, it does not limit who can create dispense events.', + _default: [], + }, + medicationRequestExpirationPeriodInDays: { + _type: Type.Number, + _description: 'Medication Requests older that this will be considered expired', + _default: 90, + }, + locationBehavior: { + locationColumn: { + enabled: { + _type: Type.Boolean, + _description: + 'Enabled/Disable including a Location column in the main prescriptions table showing ordering location', + _default: false, + }, + }, + locationFilter: { + enabled: { + _type: Type.Boolean, + _description: 'Enable/Disable Location filter on main prescriptions page', + _default: false, + }, + tag: { + _type: Type.String, + _description: 'Name of the location tag to use when fetching locations to populate filter', + _default: 'Login Location', + }, + }, + }, + refreshInterval: { + _type: Type.Number, + _description: 'The interval, in milliseconds, to query the backend for new/changed data', + _default: 60000, + }, + valueSets: { + reasonForPause: { + uuid: { + _type: Type.UUID, + _description: + "UUID for the Value Set of valid answers to the 'Reason for Pause' question. Defaults to CIEL value set: https://app.openconceptlab.org/#/orgs/CIEL/sources/CIEL/concepts/168099/", + _default: '2dd3e5c0-3d3f-4f3d-9860-19b3f9ab26ff', + }, + }, + reasonForClose: { + uuid: { + _type: Type.UUID, + _description: + "UUID for the Value Set of valid answers to the 'Reason for Close' question. Defaults to CIEL value set: https://app.openconceptlab.org/#/orgs/CIEL/sources/CIEL/concepts/168099/", + _default: 'bd6c1fc2-7cfc-4562-94a0-e4765e5e977e', + }, + }, + substitutionReason: { + uuid: { + _type: Type.UUID, + _description: + "UUID for the Value Set of valid answers to the 'Reason for Substitution' question. Defaults to CIEL value set: https://app.openconceptlab.org/#/orgs/CIEL/sources/CIEL/concepts/167862/", + _default: 'de8671b8-ed2e-4f7e-a9f8-dcd00878f2eb', + }, + }, + substitutionType: { + uuid: { + _type: Type.UUID, + _description: + "UUID for the Value Set of valid answers to the 'Type of Substitution' question. Defaults to CIEL value set: https://app.openconceptlab.org/#/orgs/CIEL/sources/CIEL/concepts/167859/", + _default: 'b9c5bca0-d026-4245-a4d2-e4c0a8999082', + }, + }, + }, + enableStockDispense: { + _type: Type.Boolean, + _description: + 'Enable or disable stock deduction during the dispensing process. Requires the stock management module to be installed and configured.', + _default: false, + }, +}; + +export interface PharmacyConfig { + appName: string; + actionButtons: { + pauseButton: { + enabled: boolean; + }; + closeButton: { + enabled: boolean; + }; + }; + refreshInterval: number; + dispenseBehavior: { + allowModifyingPrescription: boolean; + restrictTotalQuantityDispensed: boolean; + }; + dispenserProviderRoles: []; + medicationRequestExpirationPeriodInDays: number; + locationBehavior: { + locationColumn: { + enabled: boolean; + }; + locationFilter: { + enabled: boolean; + tag: string; + }; + }; + valueSets: { + reasonForPause: { + uuid: string; + }; + reasonForClose: { + uuid: string; + }; + substitutionReason: { + uuid: string; + }; + substitutionType: { + uuid: string; + }; + }; + enableStockDispense: boolean; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/constants.ts b/packages/esm-medical-supply-dispensing-app/src/constants.ts index f1daf6f..154b83a 100644 --- a/packages/esm-medical-supply-dispensing-app/src/constants.ts +++ b/packages/esm-medical-supply-dispensing-app/src/constants.ts @@ -1 +1,26 @@ export const moduleName = '@kenyaemr/esm-medical-supply-dispensing-app'; +export const spaRoot = window['getOpenmrsSpaBase']; +export const basePath = '/dispensing'; +export const spaBasePath = `${window.spaBase}${basePath}`; + +// defined in FHIR 2 module +export const OPENMRS_FHIR_PREFIX = 'http://fhir.openmrs.org'; +export const OPENMRS_FHIR_EXT_PREFIX = OPENMRS_FHIR_PREFIX + '/ext'; +export const OPENMRS_FHIR_EXT_MEDICINE = OPENMRS_FHIR_EXT_PREFIX + '/medicine'; +export const OPENMRS_FHIR_EXT_DISPENSE_RECORDED = OPENMRS_FHIR_EXT_PREFIX + '/medicationdispense/recorded'; +export const OPENMRS_FHIR_EXT_REQUEST_FULFILLER_STATUS = + OPENMRS_FHIR_EXT_PREFIX + '/medicationrequest/fullfillerstatus'; + +export const PRIVILEGE_CREATE_DISPENSE = 'Task: dispensing.create.dispense'; +export const PRIVILEGE_CREATE_DISPENSE_MODIFY_DETAILS = 'Task: dispensing.create.dispense.allowSubstitutions'; +export const PRIVILEGE_EDIT_DISPENSE = 'Task: dispensing.edit.dispense'; +export const PRIVILEGE_DELETE_DISPENSE = 'Task: dispensing.delete.dispense'; +export const PRIVILEGE_DELETE_DISPENSE_THIS_PROVIDER_ONLY = 'Task: dispensing.delete.dispense.ifCreator'; + +export const JSON_MERGE_PATH_MIME_TYPE = 'application/merge-patch+json'; + +export const PRESCRIPTIONS_TABLE_ENDPOINT = 'Encounter?_query=encountersWithMedicationRequests'; + +export const PRESCRIPTION_DETAILS_ENDPOINT = 'MedicationRequest'; + +export const medicalSupplyOrderConceptClass_UUID = '0dcf23d4-3008-4d8e-b12c-4ec95d1cfd97'; diff --git a/packages/esm-medical-supply-dispensing-app/src/dashboard/dispensing-dashboard-link.component.tsx b/packages/esm-medical-supply-dispensing-app/src/dashboard/dispensing-dashboard-link.component.tsx new file mode 100644 index 0000000..f1012fa --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/dashboard/dispensing-dashboard-link.component.tsx @@ -0,0 +1,35 @@ +import { ConfigurableLink } from '@openmrs/esm-framework'; +import React, { useMemo } from 'react'; +import classNames from 'classnames'; +import { useTranslation } from 'react-i18next'; +import { BrowserRouter } from 'react-router-dom'; + +const DispensingDashboardLink = () => { + return ( + + + + ); +}; + +export default DispensingDashboardLink; + +function DashboardExtension() { + const { t } = useTranslation(); + const spaBasePath = `${window.spaBase}/home`; + const navLink = useMemo(() => { + const pathArray = location.pathname.split('/home'); + const lastElement = pathArray[pathArray.length - 1]; + return decodeURIComponent(lastElement); + }, [location.pathname]); + + return ( + + {t('dispensing', 'Dispensing')} + + ); +} diff --git a/packages/esm-medical-supply-dispensing-app/src/dashboard/dispensing-dashboard.component.tsx b/packages/esm-medical-supply-dispensing-app/src/dashboard/dispensing-dashboard.component.tsx new file mode 100644 index 0000000..697a82d --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/dashboard/dispensing-dashboard.component.tsx @@ -0,0 +1,40 @@ +import React from 'react'; +import { InlineNotification } from '@carbon/react'; +import Overlay from '../forms/overlay/overlay.component'; +import { PharmacyHeader } from '../pharmacy-header/pharmacy-header.component'; +import PrescriptionTabLists from '../prescriptions/prescription-tab-lists.component'; +import { useConfig } from '@openmrs/esm-framework'; +import { useTranslation } from 'react-i18next'; +import { type PharmacyConfig } from '../config-schema'; +import { useOrdersWorklist } from '../hooks/useOrdersWorklist'; + +export default function DispensingDashboard() { + const config = useConfig(); + const { t } = useTranslation(); + // const { workListEntries, isLoading, isError } = useOrdersWorklist('', ''); + + // console.log(workListEntries?.length); + + if (config.dispenseBehavior.restrictTotalQuantityDispensed && config.dispenseBehavior.allowModifyingPrescription) { + return ( +
+ +
+ ); + } else { + return ( +
+ + {/* */} + + +
+ ); + } +} diff --git a/packages/esm-medical-supply-dispensing-app/src/dispensing-link.component.tsx b/packages/esm-medical-supply-dispensing-app/src/dispensing-link.component.tsx new file mode 100644 index 0000000..7ff6f09 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/dispensing-link.component.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { ConfigurableLink } from '@openmrs/esm-framework'; +import { useTranslation } from 'react-i18next'; +import { spaBasePath } from './constants'; + +export default function DispensingLink() { + const { t } = useTranslation(); + return {t('dispensing', 'Dispensing')}; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/dispensing.test.tsx b/packages/esm-medical-supply-dispensing-app/src/dispensing.test.tsx new file mode 100644 index 0000000..76497d2 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/dispensing.test.tsx @@ -0,0 +1,31 @@ +/** + * This is the root test for this page. It simply checks that the page + * renders. If the components of your page are highly interdependent, + * (e.g., if the `Hello` component had state that communicated + * information between `Greeter` and `PatientGetter`) then you might + * want to do most of your testing here. If those components are + * instead quite independent (as is the case in this example), then + * it would make more sense to test those components independently. + * + * The key thing to remember, always, is: write tests that behave like + * users. They should *look* for elements by their visual + * characteristics, *interact* with them, and (mostly) *assert* based + * on things that would be visually apparent to a user. + * + * To learn more about how we do testing, see the following resources: + * https://kentcdodds.com/blog/how-to-know-what-to-test + * https://kentcdodds.com/blog/testing-implementation-details + * https://kentcdodds.com/blog/common-mistakes-with-react-testing-library + * + * Kent C. Dodds is the inventor of `@testing-library`: + * https://testing-library.com/docs/guiding-principles + */ +import React from 'react'; +import { render } from '@testing-library/react'; +import MedicalSupplyDispensing from './medicalsupplydispensing.component'; + +describe('
', () => { + test('renders dispening without error', () => { + render(); + }); +}); diff --git a/packages/esm-medical-supply-dispensing-app/src/forms/close-dispense-form.component.tsx b/packages/esm-medical-supply-dispensing-app/src/forms/close-dispense-form.component.tsx new file mode 100644 index 0000000..02435da --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/forms/close-dispense-form.component.tsx @@ -0,0 +1,187 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { + ExtensionSlot, + showNotification, + showToast, + useConfig, + useLayoutType, + usePatient, +} from '@openmrs/esm-framework'; +import { Button, ComboBox, InlineLoading } from '@carbon/react'; +import { saveMedicationDispense, useReasonForCloseValueSet } from '../medication-dispense/medication-dispense.resource'; +import { closeOverlay } from '../hooks/useOverlay'; +import styles from './forms.scss'; +import { updateMedicationRequestFulfillerStatus } from '../medication-request/medication-request.resource'; +import { type MedicationDispense, MedicationDispenseStatus, MedicationRequestFulfillerStatus } from '../types'; +import { type PharmacyConfig } from '../config-schema'; +import { getUuidFromReference, revalidate } from '../utils'; + +interface CloseDispenseFormProps { + mode: 'enter' | 'edit'; + patientUuid?: string; + encounterUuid: string; +} + +const CloseDispenseForm: React.FC = ({ mode, patientUuid, encounterUuid }) => { + const { t } = useTranslation(); + const config = useConfig(); + const isTablet = useLayoutType() === 'tablet'; + const { patient, isLoading } = usePatient(patientUuid); + + // Keep track of medication dispense payload + const [medicationDispensePayload, setMedicationDispensePayload] = useState(); + + // whether or not the form is valid and ready to submit + const [isValid, setIsValid] = useState(false); + + // to prevent duplicate submits + const [isSubmitting, setIsSubmitting] = useState(false); + const [reasonsForClose, setReasonsForClose] = useState([]); + const { reasonForCloseValueSet } = useReasonForCloseValueSet(config.valueSets.reasonForClose.uuid); + + // useEffect(() => { + // const reasonForCloseOptions = []; + + // if (reasonForCloseValueSet?.compose?.include) { + // const uuidValueSet = reasonForCloseValueSet.compose.include.find((include) => !include.system); + // if (uuidValueSet) { + // uuidValueSet.concept?.forEach((concept) => + // reasonForCloseOptions.push({ + // id: concept.code, + // text: concept.display, + // }), + // ); + // reasonForCloseOptions.sort((a, b) => a.text.localeCompare(b.text)); + // } + // } + // setReasonsForClose(reasonForCloseOptions); + // }, [reasonForCloseValueSet]); + + // const handleSubmit = () => { + // if (!isSubmitting) { + // setIsSubmitting(true); + // const abortController = new AbortController(); + // saveMedicationDispense(medicationDispensePayload, MedicationDispenseStatus.declined, abortController) + // .then((response) => { + // // only update request status when added a new dispense event, not updating + // if (response.ok && !medicationDispense.id) { + // return updateMedicationRequestFulfillerStatus( + // getUuidFromReference( + // medicationDispensePayload.authorizingPrescription[0].reference, // assumes authorizing prescription exist + // ), + // MedicationRequestFulfillerStatus.declined, + // ); + // } else { + // return response; + // } + // }) + // .then((response) => { + // if (response.ok) { + // closeOverlay(); + // revalidate(encounterUuid); + // showToast({ + // critical: true, + // kind: 'success', + // description: t( + // mode === 'enter' ? 'medicationDispenseClosed' : 'medicationDispenseUpdated', + // mode === 'enter' ? 'Medication dispense closed.' : 'Dispense record successfully updated.', + // ), + // title: t( + // mode === 'enter' ? 'medicationDispenseClosed' : 'medicationDispenseUpdated', + // mode === 'enter' ? 'Medication dispense closed.' : 'Dispense record successfully updated.', + // ), + // }); + // } + // }) + // .catch((error) => { + // showNotification({ + // title: t( + // mode === 'enter' ? 'medicationDispenseCloseError' : 'medicationDispenseUpdatedError', + // mode === 'enter' ? 'Error closing medication dispense.' : 'Error updating dispense record', + // ), + // kind: 'error', + // critical: true, + // description: error?.message, + // }); + // setIsSubmitting(false); + // }); + // } + // }; + + // const checkIsValid = () => { + // if (medicationDispensePayload && medicationDispensePayload.statusReasonCodeableConcept?.coding[0].code) { + // setIsValid(true); + // } else { + // setIsValid(false); + // } + // }; + + // // initialize the internal dispense payload with the dispenses passed in as props + // useEffect(() => setMedicationDispensePayload(medicationDispense), [medicationDispense]); + + // // check is valid on any changes + // useEffect(checkIsValid, [medicationDispensePayload]); + + // const bannerState = useMemo(() => { + // if (patient) { + // return { + // patient, + // patientUuid, + // hideActionsOverflow: true, + // }; + // } + // }, [patient, patientUuid]); + + // return ( + //
+ //
+ // {isLoading && ( + // + // )} + // {patient && } + //
+ // item?.text} + // initialSelectedItem={{ + // id: medicationDispense.statusReasonCodeableConcept?.coding[0]?.code, + // text: medicationDispense.statusReasonCodeableConcept?.text, + // }} + // onChange={({ selectedItem }) => { + // setMedicationDispensePayload({ + // ...medicationDispensePayload, + // statusReasonCodeableConcept: { + // coding: [ + // { + // code: selectedItem?.id, + // }, + // ], + // }, + // }); + // }} + // /> + //
+ //
+ // + // + //
+ //
+ //
+ // ); + return
; +}; + +export default CloseDispenseForm; diff --git a/packages/esm-medical-supply-dispensing-app/src/forms/dispense-form.component.tsx b/packages/esm-medical-supply-dispensing-app/src/forms/dispense-form.component.tsx new file mode 100644 index 0000000..973806c --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/forms/dispense-form.component.tsx @@ -0,0 +1,226 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { Button, FormLabel, InlineLoading } from '@carbon/react'; +import { ExtensionSlot, showNotification, showToast, useConfig, usePatient } from '@openmrs/esm-framework'; +import { closeOverlay } from '../hooks/useOverlay'; +import { + type MedicationDispense, + MedicationDispenseStatus, + type MedicationRequestBundle, + type InventoryItem, + NonDrugMedicationDispense, +} from '../types'; +import { + computeNewFulfillerStatusAfterDispenseEvent, + getFulfillerStatus, + getUuidFromReference, + revalidate, +} from '../utils'; +import { type PharmacyConfig } from '../config-schema'; +import { createStockDispenseRequestPayload, sendStockDispenseRequest } from './stock-dispense/stock.resource'; +import { saveMedicationDispense } from '../medication-dispense/medication-dispense.resource'; +import { updateMedicationRequestFulfillerStatus } from '../medication-request/medication-request.resource'; +import MedicationDispenseReview from './medication-dispense-review.component'; +import StockDispense from './stock-dispense/stock-dispense.component'; +import styles from './forms.scss'; + +interface DispenseFormProps { + medicationDispense: NonDrugMedicationDispense; + mode: 'enter' | 'edit'; + patientUuid?: string; + encounterUuid: string; +} + +const DispenseForm: React.FC = ({ medicationDispense, mode, patientUuid, encounterUuid }) => { + const { t } = useTranslation(); + const { patient, isLoading } = usePatient(patientUuid); + const config = useConfig(); + + // Keep track of inventory item + const [inventoryItem, setInventoryItem] = useState(); + + // Keep track of medication dispense payload + const [medicationDispensePayload, setMedicationDispensePayload] = useState(); + + // whether or not the form is valid and ready to submit + const [isValid, setIsValid] = useState(false); + + // to prevent duplicate submits + const [isSubmitting, setIsSubmitting] = useState(false); + + // Submit medication dispense form + const handleSubmit = () => { + if (!isSubmitting) { + setIsSubmitting(true); + const abortController = new AbortController(); + saveMedicationDispense(medicationDispensePayload, MedicationDispenseStatus.completed, abortController) + .then((response) => { + // if (response.ok) { + // const newFulfillerStatus = computeNewFulfillerStatusAfterDispenseEvent( + // medicationDispensePayload, + // medicationRequestBundle, + // config.dispenseBehavior.restrictTotalQuantityDispensed, + // ); + // if (getFulfillerStatus(medicationRequestBundle.request) !== newFulfillerStatus) { + // return updateMedicationRequestFulfillerStatus( + // getUuidFromReference( + // medicationDispensePayload.authorizingPrescription[0].reference, // assumes authorizing prescription exist + // ), + // newFulfillerStatus, + // ); + // } + // } + return response; + }) + .then((response) => { + const { status } = response; + if (config.enableStockDispense && (status === 201 || status === 200)) { + const stockDispenseRequestPayload = createStockDispenseRequestPayload( + inventoryItem, + patientUuid, + encounterUuid, + medicationDispensePayload, + ); + sendStockDispenseRequest(stockDispenseRequestPayload, abortController).then( + () => { + showToast({ + critical: true, + 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 }); + }, + ); + } + return response; + }) + .then( + ({ status }) => { + if (status === 201 || status === 200) { + closeOverlay(); + revalidate(encounterUuid); + showToast({ + critical: true, + kind: 'success', + description: t('medicationListUpdated', 'Medication dispense list has been updated.'), + title: t( + mode === 'enter' ? 'medicationDispensed' : 'medicationDispenseUpdated', + mode === 'enter' ? 'Medication successfully dispensed.' : 'Dispense record successfully updated.', + ), + }); + } + }, + (error) => { + showNotification({ + title: t( + mode === 'enter' ? 'medicationDispenseError' : 'medicationDispenseUpdatedError', + mode === 'enter' ? 'Error dispensing medication.' : 'Error updating dispense record', + ), + kind: 'error', + critical: true, + description: error?.message, + }); + setIsSubmitting(false); + }, + ); + } + }; + + // const checkIsValid = () => { + // if ( + // medicationDispensePayload && + // medicationDispensePayload.performer && + // medicationDispensePayload.performer[0]?.actor.reference && + // medicationDispensePayload.quantity?.value && + // (!quantityRemaining || medicationDispensePayload?.quantity?.value <= quantityRemaining) && + // medicationDispensePayload.quantity?.code && + // medicationDispensePayload.dosageInstruction[0]?.doseAndRate[0]?.doseQuantity?.value && + // medicationDispensePayload.dosageInstruction[0]?.doseAndRate[0]?.doseQuantity?.code && + // medicationDispensePayload.dosageInstruction[0]?.route?.coding[0].code && + // medicationDispensePayload.dosageInstruction[0]?.timing?.code.coding[0].code && + // (!medicationDispensePayload.substitution.wasSubstituted || + // (medicationDispensePayload.substitution.reason[0]?.coding[0].code && + // medicationDispensePayload.substitution.type?.coding[0].code)) + // ) { + // setIsValid(true); + // } else { + // setIsValid(false); + // } + // }; + + // initialize the internal dispense payload with the dispenses passed in as props + + useEffect(() => setMedicationDispensePayload(medicationDispense), [medicationDispense]); + + // check is valid on any changes + // useEffect(checkIsValid, [medicationDispensePayload, quantityRemaining, inventoryItem]); + + const isButtonDisabled = (config.enableStockDispense ? !inventoryItem : false) || !isValid || isSubmitting; + + const bannerState = useMemo(() => { + if (patient) { + return { + patient, + patientUuid, + hideActionsOverflow: true, + }; + } + }, [patient, patientUuid]); + + return ( +
+
+ {isLoading && ( + + )} + {/* {patient && } */} +
+ + {t( + config.dispenseBehavior.allowModifyingPrescription ? 'drugHelpText' : 'drugHelpTextNoEdit', + config.dispenseBehavior.allowModifyingPrescription + ? 'You may edit the formulation and quantity dispensed here' + : 'You may edit quantity dispensed here', + )} + + {medicationDispensePayload ? ( +
+ + {config.enableStockDispense && ( + + )} +
+ ) : null} +
+
+ + +
+
+
+ ); +}; + +export default DispenseForm; diff --git a/packages/esm-medical-supply-dispensing-app/src/forms/forms.scss b/packages/esm-medical-supply-dispensing-app/src/forms/forms.scss new file mode 100644 index 0000000..5879c2e --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/forms/forms.scss @@ -0,0 +1,154 @@ +@use '@carbon/styles/scss/spacing'; +@use '@carbon/styles/scss/type'; +@import '~@openmrs/esm-styleguide/src/vars'; + +// TO DO Move this styles to style - guide +// https://github.com/openmrs/openmrs-esm-core/blob/master/packages/framework/esm-styleguide/src/_vars.scss +$color-blue-30: #a6c8ff; + +.productiveHeading02 { + color: $color-gray-70; + @include type.type-style('productive-heading-02'); +} + +:global(.omrs-breakpoint-lt-desktop) .formWrapper { + background-color: $openmrs-background-grey; +} + +:global(.omrs-breakpoint-gt-tablet) .formWrapper { + background-color: $ui-02; +} + +.formGroup { + display: flex; + margin-bottom: spacing.$spacing-02; + padding: spacing.$spacing-05; +} + +:global(.omrs-breakpoint-lt-desktop) .formGroup > span { + flex: 1; +} + +:global(.omrs-breakpoint-lt-desktop) .formGroup > div { + flex: 3; +} + +.formGroup span { + @extend .productiveHeading02; +} + +.bannerLoading { + width: 100%; + display: flex; + justify-content: center; +} + +.patientInfo { + position: sticky; + z-index: 1000; + background-color: $ui-02; + top: 3rem; + overflow-y: auto; +} + +.dateTimeContainer { + display: flex; + margin: 1rem 0; +} + +.timePickersContainer, +.frequencyAndDay { + display: flex; + + > div { + margin-right: 0.5rem; + } +} + +.datePickerControl { + display: flex; + flex-direction: column; + + > div { + margin-bottom: 1rem; + } +} + +:global(.omrs-breakpoint-lt-desktop) .formGroup { + flex-direction: row; +} + +:global(.omrs-breakpoint-gt-tablet) .formGroup { + flex-direction: column; +} + +.buttonGroup { + display: flex; + position: sticky; + bottom: 0; + width: 100%; +} + +:global(.omrs-breakpoint-lt-desktop) .buttonGroup { + padding: spacing.$spacing-05 spacing.$spacing-06; + background-color: $ui-02; +} + +.buttonGroup button { + max-width: none; + width: 50%; + height: spacing.$spacing-10; + align-items: flex-start; +} + +.contentSwitcherWrapper { + display: flex; + width: fit-content; + justify-content: flex-end; + align-items: center; + width: 60%; +} + +.contentSwitcherWrapper > div > button { + background-color: $ui-02; +} + +.contentSwitcherWrapper > div button:first-child { + border-top: 1px solid $color-blue-30; + border-bottom: 1px solid $color-blue-30; + border-left: 1px solid $color-blue-30; + border-right: none; + border-radius: spacing.$spacing-02 0 0px spacing.$spacing-02; +} + +.contentSwitcherWrapper > div button:last-child { + border-top: 1px solid $color-blue-30; + border-bottom: 1px solid $color-blue-30; + border-right: 1px solid $color-blue-30; + border-left: none; + border-radius: 0px spacing.$spacing-02 spacing.$spacing-02 0px; +} + +.contentSwitcherWrapper > div > button[aria-selected='true'], +.contentSwitcherWrapper > div > button[aria-selected='true']:first-child { + background-color: $color-blue-10; + color: $color-blue-60-2; + border-color: $color-blue-60-2; + border-right: 1px solid $color-blue-60-2; +} + +.contentSwitcherWrapper > div > button[aria-selected='true'], +.contentSwitcherWrapper > div > button[aria-selected='true']:last-child { + background-color: $color-blue-10; + color: $color-blue-60-2; + border-color: $color-blue-60-2; + border-left: 1px solid $color-blue-60-2; +} + +.contentSwitcherWrapper > div > button[aria-selected='true']:focus { + box-shadow: none; +} + +.reviewContainer { + margin: 0.8rem 0 0 0 !important; +} diff --git a/packages/esm-medical-supply-dispensing-app/src/forms/medication-dispense-review.component.test.tsx b/packages/esm-medical-supply-dispensing-app/src/forms/medication-dispense-review.component.test.tsx new file mode 100644 index 0000000..9444ace --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/forms/medication-dispense-review.component.test.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { render } from '@testing-library/react'; +import { type MedicationDispense, MedicationDispenseStatus, NonDrugMedicationDispense } from '../types'; +import MedicationDispenseReview from './medication-dispense-review.component'; +import { OpenmrsDatePicker, useConfig } from '@openmrs/esm-framework'; + +const mockUseConfig = jest.mocked(useConfig); +const mockOpenmrsDatePicker = jest.mocked(OpenmrsDatePicker); + +beforeEach(() => { + mockUseConfig.mockReturnValue({ + dispenseBehavior: { + allowModifyingPrescription: false, + restrictTotalQuantityDispensed: false, + }, + valueSets: { + substitutionType: { uuid: '123' }, + substitutionReason: { uuid: 'abc' }, + }, + }); + + mockOpenmrsDatePicker.mockReturnValue(
); +}); + +describe('Medication Dispense Review Component tests', () => { + test('component should render medication dispense review', () => { + const medicationDispense: NonDrugMedicationDispense = { + uuid: 'd4f69a68-1171-4e47-8693-478df18daf40', + patient: 'd4f69a68-1171-4e47-8693-478df18daf40', + encounter: 'd4f69a68-1171-4e47-8693-478df18daf40', + dispensingUnit: { uuid: 'd4f69a68-1171-4e47-8693-478df18daf40', display: 'Test' }, + quantity: 2, + display: 'test ', + instrucions: 'test', + status: '', + medicalSupplyOrder: 'd4f69a68-1171-4e47-8693-478df18daf40', + concept: '', + dateDispensed: new Date(), + statusReason: '', + location: '', + encounters: '', + dispenser: '', + }; + + const mockUpdate: Function = jest.fn(); + render(); + + // TODO test expected views and various interactions + }); +}); diff --git a/packages/esm-medical-supply-dispensing-app/src/forms/medication-dispense-review.component.tsx b/packages/esm-medical-supply-dispensing-app/src/forms/medication-dispense-review.component.tsx new file mode 100644 index 0000000..094a919 --- /dev/null +++ b/packages/esm-medical-supply-dispensing-app/src/forms/medication-dispense-review.component.tsx @@ -0,0 +1,237 @@ +import React, { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { ComboBox, Dropdown, NumberInput, Stack, TextArea } from '@carbon/react'; +import { OpenmrsDatePicker, 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, 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 { NonDrugMedicationDispense, type Medication, type MedicationDispense } from '../types/index'; +import { type PharmacyConfig } from '../config-schema'; +import styles from '../components/medication-dispense-review.scss'; +import dayjs from 'dayjs'; + +interface MedicationDispenseReviewProps { + medicationDispense: NonDrugMedicationDispense; + updateMedicationDispense: Function; +} + +const MedicationDispenseReview: React.FC = ({ + medicationDispense, + updateMedicationDispense, +}) => { + const { t } = useTranslation(); + const config = useConfig(); + const session = useSession(); + const [isEditingFormulation, setIsEditingFormulation] = useState(false); + const [isSubstitution, setIsSubstitution] = useState(false); + // Dosing Unit eg Tablets + const [drugDosingUnits, setDrugDosingUnits] = useState([]); + // Dispensing Unit eg Tablets + const [drugDispensingUnits, setDrugDispensingUnits] = useState([]); + // Route eg Oral + const [drugRoutes, setDrugRoutes] = useState([]); + // Frequency eg Twice daily + const [orderFrequencies, setOrderFrequencies] = useState([]); + // type of substitution question + const [substitutionTypes, setSubstitutionTypes] = useState([]); + // reason for substitution question + const [substitutionReasons, setSubstitutionReasons] = useState([]); + const [userCanModify, setUserCanModify] = useState(false); + + const isTablet = useLayoutType() === 'tablet'; + const prescriptionDate = Date.now(); + const allowEditing = config.dispenseBehavior.allowModifyingPrescription; + + const { orderConfigObject } = useOrderConfig(); + const { substitutionTypeValueSet } = useSubstitutionTypeValueSet(config.valueSets.substitutionType.uuid); + const { substitutionReasonValueSet } = useSubstitutionReasonValueSet(config.valueSets.substitutionReason.uuid); + const providers = useProviders(config.dispenserProviderRoles); + + useEffect(() => { + if (orderConfigObject) { + // sync drug route options order config + const availableRoutes = drugRoutes.map((x) => x.id); + const otherRouteOptions = []; + orderConfigObject.drugRoutes.forEach( + (x) => availableRoutes.includes(x.uuid) || otherRouteOptions.push({ id: x.uuid, text: x.display }), + ); + setDrugRoutes([...drugRoutes, ...otherRouteOptions]); + + // sync dosage.unit options with what's defined in the order config + const availableDosingUnits = drugDosingUnits.map((x) => x.id); + const otherDosingUnits = []; + orderConfigObject.drugDosingUnits.forEach( + (x) => availableDosingUnits.includes(x.uuid) || otherDosingUnits.push({ id: x.uuid, text: x.display }), + ); + setDrugDosingUnits([...drugDosingUnits, ...otherDosingUnits]); + + // sync dispensing unit options with what's defined in the order config + const availableDispensingUnits = drugDispensingUnits.map((x) => x.id); + const otherDispensingUnits = []; + orderConfigObject.drugDispensingUnits.forEach( + (x) => availableDispensingUnits.includes(x.uuid) || otherDispensingUnits.push({ id: x.uuid, text: x.display }), + ); + setDrugDispensingUnits([...drugDispensingUnits, ...otherDispensingUnits]); + + // sync order frequency options with order config + const availableFrequencies = orderFrequencies.map((x) => x.id); + const otherFrequencyOptions = []; + orderConfigObject.orderFrequencies.forEach( + (x) => availableFrequencies.includes(x.uuid) || otherFrequencyOptions.push({ id: x.uuid, text: x.display }), + ); + setOrderFrequencies([...orderFrequencies, ...otherFrequencyOptions]); + } + }, [orderConfigObject]); + + useEffect(() => { + const substitutionTypeOptions = []; + + if (substitutionTypeValueSet?.compose?.include) { + const uuidValueSet = substitutionTypeValueSet.compose.include.find((include) => !include.system); + if (uuidValueSet) { + uuidValueSet.concept?.forEach((concept) => + substitutionTypeOptions.push({ + id: concept.code, + text: concept.display, + }), + ); + } + substitutionTypeOptions.sort((a, b) => a.text.localeCompare(b.text)); + } + setSubstitutionTypes(substitutionTypeOptions); + }, [substitutionTypeValueSet]); + + useEffect(() => { + const substitutionReasonOptions = []; + + if (substitutionReasonValueSet?.compose?.include) { + const uuidValueSet = substitutionReasonValueSet.compose.include.find((include) => !include.system); + if (uuidValueSet) { + uuidValueSet.concept?.forEach((concept) => + substitutionReasonOptions.push({ + id: concept.code, + text: concept.display, + }), + ); + } + substitutionReasonOptions.sort((a, b) => a.text.localeCompare(b.text)); + } + setSubstitutionReasons(substitutionReasonOptions); + }, [substitutionReasonValueSet]); + + useEffect(() => { + setUserCanModify(session?.user && userHasAccess(PRIVILEGE_CREATE_DISPENSE_MODIFY_DETAILS, session.user)); + }, [session]); + + return ( +
+ +
+ { + updateMedicationDispense({ + ...medicationDispense, + quantity: { + ...medicationDispense.quantity, + value: e.target?.value ? parseFloat(e.target.value) : '', + }, + }); + }} + /> + + item?.text} + initialSelectedItem={{ + id: medicationDispense.dispensingUnit.uuid, + text: medicationDispense.dispensingUnit.display, + }} + onChange={({ selectedItem }) => { + updateMedicationDispense({ + ...medicationDispense, + // note that we specifically recreate doesQuantity to overwrite any unit or system properties that may have been set + quantity: { + value: medicationDispense.quantity, + code: selectedItem?.id, + }, + }); + }} + required + /> +
+ +