O3: What should be the UX of a non-provider user?

Hello community, we recently realised that when a logged in user is not a provider and the try to open the order basket on patient chart, the workspace just keeps loading without stopping, this is because the API returns a 500 status code with a message indicating that the provider UUID if not found.

The quick approach to that would be displaying an error message in a Notification component instead of endless loading how while working on that fix.

I wonder whether we even need to launch the form at all, my thought was that maybe we just need to disable all buttons that open the order basket if a user is not a provider and probably display a tooltip or a message on top of the Active Medications table telling the user that he is not a provider and therefore can not create or modify an order basket. The message would be something like the below screenshot.

Here is the O3 ticket (O3-1652) with steps to reproduce the error.

@grace @burke @cduffy @mksd please let me know what the desired UX should be.

Thanks @hadijah315!

Cc-ing @ibacher @pauladams as well.

IMHO being or not a provider is, functionally, similar to being granted certain privileges or not. Therefore this should be hooked in the same way in the frontend code as permissioning. I would love to hear about it from others here, I’m sure people (like @burke) have opinions about this.

The fact is that almost every user is an OpenMRS provider in practice, so this is not a common issue. The archetypical counter example is that of registration clerks of course.

Yes, I would think that registration clerks and the like, who have no clinical or order-handling function, don’t need to see the order basket at all (or certain other types of clinical information, perhaps test results).

Permissions like this should be managed at the user level.

We should follow the principle that if a user can never have the permissions to click a button, that button should never be visible to them. Otherwise, it prompts the question ‘why can’t I click this button’ which the UI needs to answer.

So for a registration clerk, they should be able to create a patient chart, but not be able to add any medical data to that chart, right? In this example, @hadijah315 I think almost all items in the side-rail should not be visible to this kind of user. The Order basket, clinical form, visit note, and tasks, can all be hidden. The only item that should be visible to this kind of user is the ‘workspace patient chart’ feature.

Side note: This feature is not yet implemented correctly, but is an often requested feature. It is not intended for the user to be redirected away from the patient chart, if they click the patient lists feature in the siderail. This is actually very undesirable. That’s perhaps a bit off topic for now, but thought it was worth mentioning. Mock-ups of this feature here: Zeplin - Login

As a summary, if the user can never click the button, hide the button from the user and ideally manage these permissions at the user role level.

Hope this helps :smile:

1 Like

I would recommend we avoid programming against organizational roles and focus on application roles – i.e., avoid assuming a nurse can’t register a patient or that a registration clerk can’t enter vitals. Who can perform which actions in the application is ideally controlled with permissions. If a user role like “Nurse” is used to control application behavior, this makes two big assumptions: (1) someone can be given the role even if they are not a nurse and (2) the permissions of a nurse will be aligned across every implementation in ever country where the application is used.

The frontend framework provides tooling to check permissions, albeit most components have not implemented this (There’s a userHasAccess() function and a React component wrapper for that function, which should make it very easy to hide these components).

Being a provider / not being a provider should not really be a form of access control… It’s not really a proper permission, just a statement about where the user should appear.

There’s probably some legacy cruft permission-wise that needs to be dealt with. At least the reference application defaults probably give users too many privileges… For instance, the default “Registration Clerk” role has both Add Order and Edit Order privileges.

From what I understand from your second paragraph @ibacher, there is no need to hide any actions depending on whether a user is a provider or not, right? correct me if I misunderstood that, but if that is the case all that needs to be done is display the error message coming from the backend.

However, the confusing bit with that is that showing that error but still opens the order basket which looks weird because an error technically means something has failed. How would you suggest we handle this in a user-friendly way cc @cduffy

Well, being a provider or not is not really a security concern, so no.

What you didn’t say in this post and that would be helpful to know is what call the frontend is making to the backend the results in the 500 error… I have no idea what the frontend would need to access that would need to check if the user was a provider or not.

The endpoint is a POST to /encounter in case an encounter does not exist.

So the encounter creation endpoint expects a provider UUID and when the logged-in user is not a provider we get a 500 with this message.

    "message": "[could not execute statement => Column 'provider_id' cannot be null]",
    "code": "org.hibernate.exception.internal.SQLExceptionTypeDelegate:59",
    "detail": .....
}

So, from a quick look at the code, I think the order basket is still kind of broken in key ways.

The error you’re reporting is because when a user opens the order basket we call a function called getCurrentOrderBasketEncounter(). If this function doesn’t find an encounter (more on that below), it attempts to create an encounter, but the code to create an encounter is setup in such a way that it can return a JSON document that has this fragment:

"encounterProviders": [{
  "provider": null,
  "encounterRole": "<UUID from config>"
}]

This isn’t a valid encounterProvider entry, though the error message could be better. Ideally, if there’s no provider, we just skip the encounterProviders entry altogether (but more considerations further on).

It seems wrong for the app to try and create an encounter when the user opens the workspace. That should, ideally, happen once the user performs an action. In my mind, the ideal moment would be when someone goes to submit the order, but even delaying it until a user actually creates a draft order would be better than the current implementation. (Right now, if Dr. X clicks on the order basket icon, an encounter is created, even if nothing is done inside that encounter).

Now, when we create the encounter, its not associated with a visit. This also seems wrong… I think the idea was to have a single “Order Entry” encounter for each visit. If we’re going to model things that way, then there’s really no sensible encounter provider to associate with the “Order Entry” and we just shouldn’t be trying to provide one. This is because two different providers might both place orders for the same patient on the same visit, but with the current implementation, one of them will be marked as the encounter provider and the other will not, which means we’re creating bad data.

Finally, before we get into all of this, the getCurrentOrderBasketEncounter(). I’d expect it to look for an “Order Encounter” for the currently active visit and return that if it exists. Instead, it’s implementation looks like this:

export function getCurrentOrderBasketEncounter(patientUuid: string, abortController?: AbortController) {
  const [nowDateString] = new Date().toISOString().split('T');
  return openmrsFetch(`/ws/rest/v1/encounter?patient=${patientUuid}&fromdate=${nowDateString}&limit=1`, {
    signal: abortController?.signal,
  });
}

Which is asking for the backend for the first encounter it decides to return from the current day. There’s no guarantee what encounter this will be. It could be the “Order Entry” encounter we created or it could be… any other encounter that was entered for the patient. In other words, we will end up attaching orders created via the order basket to a basically random encounter the patient had.

From the documentation, I can see we can already support having the encounterType, at least, used to filter down this list and it should be pretty easy to add the filtering for the current visit to that too.

@burke You’re right, there will be differences in what permissions different roles should have across countries / implementations. So we probably shouldn’t be too prescriptive with permissions here.

Is it an idea to make roles themselves configurable. So an implementation can decide which permissions get allocated to which role. This keeps managing and assigning roles in the EMR clear understandable. This would require some design work in the implementation tools to show how it would be configured on the frontend, but for now maybe we can just think on how to enable such a feature.

For example, as a person configuring OpenMRS for an implementation, I can manage what permissions come under Nurse / Clerk / Clinician / Community Health Worker etc. Probably I can create ‘custom roles’ as well with custom permission sets.

We could have a default configuration for what permissions come under each pre-defined role, but implementations are free to change them and allow all clerks record vitals, if that’s what is needed there. I, as an implementer could equally say, I want to have clerks as a role with the default permissions, and that’s what 3 of the clerks at my site have, but I also want to create a custom role called ‘Triage clerk’ and assign Tom to that role. Now I can add the ‘Clinical forms’ permission to that role, along with the permissions clerks already have, so that the clinical forms item appears in the siderail, and Tom can record Vitals and perhaps some other forms necessary when registering patients.

The principle I’d like us to follow, however permissions gets implemented, is that if a user never has permission to use a feature, that feature should never be visible to that user. So if a clerk can never add to the order basket, the basket should never be visible to them.

We shouldn’t be showing an ‘error message’ to a user, because they don’t have the correct permissions. Similarly, buttons should not be inexplicably disabled for users.

Complicated! Might be something we need to discuss and find some design time for too, sooner rather than later!

Based on similar projects with similar dilemmas that I’ve participated in over the years, I think (similar to what @burke is saying) that we shouldn’t assign the privilege specifically to the role. In other words, a doctor may typically have permission to write orders, enter documentation, and view live results, But I would specify those as three distinct permissions that can be individually assigned to someone. Then, if you wish, somewhere else there can be a configurable table which says that by default new doctors get these three permissions.

Otherwise, if you have any kind of exceptional cases of someone who needs one extra permission, or one fewer, you wind up having to give them a role that is not what they actually do for work, and it will mess up some other accounting that uses that role.