Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

O3-1709: Permissions for which Dispensing fields can be edited and by which role #54

Merged
merged 5 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@
This repository is for the OpenMRS Dispensing App. For more information, please see the
[Design Documentation](https://wiki.openmrs.org/display/projects/Dispensing+Design+Components).

## Required privileges

Note that following privileges need to be installed and assigned to roles:

* "Task: dispensing.create.dispense" - Allows user to Dispense Medication
* "Task: dispensing.create.dispense.andModifyDetails" - Allows user to modify the Quantity, Drug, Formulation and Dose Instructions (from the values specified in the Order / Medication Request) when Dispensing
mogoodrich marked this conversation as resolved.
Show resolved Hide resolved
* "Task: dispensing.edit.dispense" - Allows user to edit an existing Medication Dispense
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a world where we would want this to be different than the "create" privileges? Would we ever want to prevent someone from editing something they created? What if they made a typo?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point, this is a current requirement, so leaving this in, but I could see this changing in the future... having the two privileges gives us the flexibility.

* "Task: dispensing.delete.dispense" - Allows user to delete an existing Medication Dispense
* "Task: dispensing.delete.dispense.ifCreator" - Allows user to delete an existing Medication Dispense, *but only* if they created it originally

## Running this code

```sh
Expand All @@ -30,3 +40,4 @@ For more information, please see the

In particular, the [Setup](https://openmrs.github.io/openmrs-esm-core/#/getting_started/setup)
section can help you get started developing microfrontends in general.

27 changes: 25 additions & 2 deletions src/components/medication-dispense-review.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import React, { useEffect, useState } from "react";
import { Medication, MedicationDispense } from "../types";
import MedicationCard from "./medication-card.component";
import { TextArea, ComboBox, Dropdown, NumberInput } from "@carbon/react";
import { useLayoutType, useConfig } from "@openmrs/esm-framework";
import {
useLayoutType,
useConfig,
useSession,
userHasAccess,
} from "@openmrs/esm-framework";
import { useTranslation } from "react-i18next";
import {
getConceptCodingUuid,
Expand All @@ -21,6 +26,7 @@ import {
useSubstitutionReasonValueSet,
useSubstitutionTypeValueSet,
} from "../medication-dispense/medication-dispense.resource";
import { PRIVILEGE_CREATE_DISPENSE_MODIFY_DETAILS } from "../constants";

interface MedicationDispenseReviewProps {
medicationDispense: MedicationDispense;
Expand All @@ -35,6 +41,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({
}) => {
const { t } = useTranslation();
const config = useConfig() as PharmacyConfig;
const session = useSession();
const [isEditingFormulation, setIsEditingFormulation] = useState(false);
const [isSubstitution, setIsSubstitution] = useState(false);
// Dosing Unit eg Tablets
Expand All @@ -49,6 +56,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({
const [substitutionTypes, setSubstitutionTypes] = useState([]);
// reason for substitution question
const [substitutionReasons, setSubstitutionReasons] = useState([]);
const [userCanModify, setUserCanModify] = useState(false);

const isTablet = useLayoutType() === "tablet";

Expand Down Expand Up @@ -217,14 +225,23 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({
medicationRequest?.medicationReference,
]);

useEffect(() => {
setUserCanModify(
session?.user &&
userHasAccess(PRIVILEGE_CREATE_DISPENSE_MODIFY_DETAILS, session.user)
);
}, [session]);

return (
<div className={styles.medicationDispenseReviewContainer}>
{!isEditingFormulation ? (
<MedicationCard
medication={getMedicationReferenceOrCodeableConcept(
medicationDispense
)}
editAction={() => setIsEditingFormulation(true)}
editAction={
userCanModify ? () => setIsEditingFormulation(true) : null
}
/>
) : (
<Dropdown
Expand Down Expand Up @@ -332,6 +349,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({
<div className={styles.dispenseDetailsContainer}>
<NumberInput
allowEmpty={false}
disabled={!userCanModify}
hideSteppers={true}
id="quantity"
invalidText={t("numberIsNotValid", "Number is not valid")}
Expand All @@ -354,6 +372,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({

<ComboBox
id="quantityUnits"
disabled={!userCanModify}
light={isTablet}
items={drugDispensingUnits}
titleText={t("drugDispensingUnit", "Dispensing unit")}
Expand Down Expand Up @@ -382,6 +401,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({
<div className={styles.dispenseDetailsContainer}>
<NumberInput
allowEmpty={false}
disabled={!userCanModify}
hideSteppers={true}
id="dosingQuanity"
light={isTablet}
Expand Down Expand Up @@ -420,6 +440,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({

<ComboBox
id="dosingUnits"
disabled={!userCanModify}
light={isTablet}
items={drugDosingUnits}
titleText={t("doseUnit", "Dose unit")}
Expand Down Expand Up @@ -459,6 +480,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({

<ComboBox
id="editRoute"
disabled={!userCanModify}
light={isTablet}
items={drugRoutes}
initialSelectedItem={{
Expand Down Expand Up @@ -493,6 +515,7 @@ const MedicationDispenseReview: React.FC<MedicationDispenseReviewProps> = ({

<ComboBox
id="frequency"
disabled={!userCanModify}
light={isTablet}
items={orderFrequencies}
initialSelectedItem={{
Expand Down
8 changes: 8 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,11 @@ export const spaBasePath = `${window.spaBase}${basePath}`;
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 PRIVILEGE_CREATE_DISPENSE = "Task: dispensing.create.dispense";
export const PRIVILEGE_CREATE_DISPENSE_MODIFY_DETAILS =
mogoodrich marked this conversation as resolved.
Show resolved Hide resolved
"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";
122 changes: 88 additions & 34 deletions src/history/history-and-comments.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,32 @@ import {
Tile,
} from "@carbon/react";
import { useTranslation } from "react-i18next";
import { parseDate, formatDatetime } from "@openmrs/esm-framework";
import {
parseDate,
formatDatetime,
useSession,
userHasAccess,
Session,
} from "@openmrs/esm-framework";
import styles from "./history-and-comments.scss";
import { usePrescriptionDetails } from "../medication-request/medication-request.resource";
import { deleteMedicationDispense } from "../medication-dispense/medication-dispense.resource";
import MedicationEvent from "../components/medication-event.component";
import { launchOverlay } from "../hooks/useOverlay";
import DispenseForm from "../forms/dispense-form.component";
import { MedicationDispense } from "../types";
import {
PRIVILEGE_DELETE_DISPENSE,
PRIVILEGE_DELETE_DISPENSE_THIS_PROVIDER_ONLY,
PRIVILEGE_EDIT_DISPENSE,
} from "../constants";

const HistoryAndComments: React.FC<{
encounterUuid: string;
mutate: Function;
}> = ({ encounterUuid, mutate }) => {
const { t } = useTranslation();
const session = useSession();
const {
requests,
dispenses,
Expand All @@ -30,44 +42,86 @@ const HistoryAndComments: React.FC<{
isLoading,
} = usePrescriptionDetails(encounterUuid);

const userCanEdit: Function = (session: Session) =>
session?.user && userHasAccess(PRIVILEGE_EDIT_DISPENSE, session.user);

const userCanDelete: Function = (
session: Session,
medicationDispense: MedicationDispense
) => {
if (session?.user) {
if (userHasAccess(PRIVILEGE_DELETE_DISPENSE, session.user)) {
return true;
} else if (
userHasAccess(
PRIVILEGE_DELETE_DISPENSE_THIS_PROVIDER_ONLY,
session.user
) &&
session.currentProvider?.uuid &&
medicationDispense.performer?.find(
(performer) =>
performer?.actor?.reference?.length > 1 &&
performer.actor.reference.split("/")[1] ===
session.currentProvider.uuid
) != null
) {
return true;
}
}
return false;
};

const generateMedicationDispenseActionMenu: Function = (
medicationDispense: MedicationDispense
) => (
<OverflowMenu
ariaLabel={t(
"medicationDispenseActionMenu",
"Medication Dispense Action Menu"
)}
flipped={true}
className={styles.medicationEventActionMenu}
>
<OverflowMenuItem
onClick={() =>
launchOverlay(
t("editDispenseRecord", "Edit Dispense Record"),
<DispenseForm
medicationDispenses={[medicationDispense]}
mode="edit"
mutate={() => {
) => {
const editable = userCanEdit(session);
const deletable = userCanDelete(session, medicationDispense);

if (!editable && !deletable) {
return null;
} else {
return (
<OverflowMenu
ariaLabel={t(
"medicationDispenseActionMenu",
"Medication Dispense Action Menu"
)}
flipped={true}
className={styles.medicationEventActionMenu}
>
{editable && (
<OverflowMenuItem
onClick={() =>
launchOverlay(
t("editDispenseRecord", "Edit Dispense Record"),
<DispenseForm
medicationDispenses={[medicationDispense]}
mode="edit"
mutate={() => {
mutate();
mutatePrescriptionDetails();
}}
isLoading={false}
/>
)
}
itemText={t("editRecord", "Edit Record")}
></OverflowMenuItem>
)}
{deletable && (
<OverflowMenuItem
onClick={() => {
deleteMedicationDispense(medicationDispense.id);
mutate();
mutatePrescriptionDetails();
}}
isLoading={false}
/>
)
}
itemText={t("editRecord", "Edit Record")}
></OverflowMenuItem>
<OverflowMenuItem
onClick={() => {
deleteMedicationDispense(medicationDispense.id);
mutate();
mutatePrescriptionDetails();
}}
itemText={t("delete", "Delete")}
></OverflowMenuItem>
</OverflowMenu>
);
itemText={t("delete", "Delete")}
></OverflowMenuItem>
)}
</OverflowMenu>
);
}
};

// TODO: assumption is dispenses always are after requests?
return (
Expand Down
39 changes: 21 additions & 18 deletions src/prescriptions/prescription-expanded.component.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import React from "react";
import { PatientUuid } from "@openmrs/esm-framework";
import { PatientUuid, UserHasAccess } from "@openmrs/esm-framework";
import { Tab, Tabs, TabList, TabPanels, TabPanel, Button } from "@carbon/react";
import { useTranslation } from "react-i18next";
import HistoryAndComments from "../history/history-and-comments.component";
import styles from "./prescription-expanded.scss";
import PrescriptionDetails from "./prescription-details.component";
import InitializeDispenseFormFromRequests from "../forms/initialize-dispense-form-from-requests.component";
import { launchOverlay } from "../hooks/useOverlay";
import { PRIVILEGE_CREATE_DISPENSE } from "../constants";

interface TabItem {
name: string;
Expand Down Expand Up @@ -67,24 +68,26 @@ const PrescriptionExpanded: React.FC<{
</div>
{status === "active" && (
<div className={styles.prescriptionActions}>
<Button
kind="primary"
className={styles.dispenseBtn}
onClick={() =>
launchOverlay(
t("dispensePrescription", "Dispense prescription"),
<InitializeDispenseFormFromRequests
encounterUuid={encounterUuid}
mutate={mutate}
/>
)
}
>
{t("dispense", "Dispense")}
</Button>
<Button kind="secondary" className={styles.returnToPrescriberBtn}>
<UserHasAccess privilege={PRIVILEGE_CREATE_DISPENSE}>
<Button
kind="primary"
className={styles.dispenseBtn}
onClick={() =>
launchOverlay(
t("dispensePrescription", "Dispense prescription"),
<InitializeDispenseFormFromRequests
encounterUuid={encounterUuid}
mutate={mutate}
/>
)
}
>
{t("dispense", "Dispense")}
</Button>
</UserHasAccess>
{/* <Button kind="secondary" className={styles.returnToPrescriberBtn}>
{t("sendBackToPrescriber", "Send back to prescriber")}
</Button>
</Button>*/}
mogoodrich marked this conversation as resolved.
Show resolved Hide resolved
</div>
)}
</div>
Expand Down