From fbeb24b1319c3e82134c98a546dd7ab26dfeeac1 Mon Sep 17 00:00:00 2001 From: p-netm Date: Fri, 30 Jun 2023 18:57:15 +0300 Subject: [PATCH] Update PractitionerAssigment Behavior to include new strategy (#1217) * Add a pkg-config env on practiitoner assignment strategy * Update team assignment to support new practitioner assignment * Update envs in app and docs * Update documentation * Update strategy value in evs * Change practitioner assignment strategy enum value * Fix typo --- app/.env.sample | 1 + app/src/configs/dispatchConfig.ts | 2 + app/src/configs/env.ts | 5 +++ docs/env.md | 6 +++ docs/fhir-web-docker-deployment.md | 1 + .../components/AddEditOrganization/Form.tsx | 12 +++++- .../components/AddEditOrganization/index.tsx | 23 +++++++---- .../AddEditOrganization/tests/Form.test.tsx | 40 +++++++++++++++++++ .../AddEditOrganization/tests/index.test.tsx | 8 ++-- .../components/AddEditOrganization/utils.ts | 37 +++++++++++++++-- packages/pkg-config/src/configStore/index.ts | 12 ++++++ .../src/configStore/tests/index.test.tsx | 1 + 12 files changed, 132 insertions(+), 16 deletions(-) diff --git a/app/.env.sample b/app/.env.sample index 8435cbcec..82f130ad9 100644 --- a/app/.env.sample +++ b/app/.env.sample @@ -66,3 +66,4 @@ REACT_APP_ENABLE_REPORTS=false REACT_APP_ENABLE_FHIR_GROUP=true REACT_APP_ENABLE_FHIR_COMMODITY=true REACT_APP_COMMODITIES_LIST_RESOURCE_ID="uuid" +REACT_APP_PRACTITIONER_TO_ORG_ASSIGNMENT_STRATEGY=ONE_TO_MANY diff --git a/app/src/configs/dispatchConfig.ts b/app/src/configs/dispatchConfig.ts index 628099a9a..0026fd3cb 100644 --- a/app/src/configs/dispatchConfig.ts +++ b/app/src/configs/dispatchConfig.ts @@ -13,6 +13,7 @@ import { PROJECT_CODE, FHIR_API_BASE_URL, DEFAULTS_TABLE_PAGE_SIZE, + PRACTITIONER_TO_ORG_ASSIGNMENT_STRATEGY, } from './env'; import { URL_BACKEND_LOGIN, URL_REACT_LOGIN } from '../constants'; @@ -29,6 +30,7 @@ const configObject: ConfigState = { opensrpBaseURL: OPENSRP_API_BASE_URL, fhirBaseURL: FHIR_API_BASE_URL, defaultTablesPageSize: DEFAULTS_TABLE_PAGE_SIZE, + practToOrgAssignmentStrategy: PRACTITIONER_TO_ORG_ASSIGNMENT_STRATEGY, }; setAllConfigs(configObject); diff --git a/app/src/configs/env.ts b/app/src/configs/env.ts index 9e541cfcc..43e19261a 100644 --- a/app/src/configs/env.ts +++ b/app/src/configs/env.ts @@ -232,3 +232,8 @@ export const ENABLE_REPORTS = setEnv('REACT_APP_ENABLE_REPORTS', 'false') === 't export const ENABLE_FHIR_COMMODITY = setEnv('REACT_APP_ENABLE_FHIR_COMMODITY', 'false') === 'true'; export const COMMODITIES_LIST_RESOURCE_ID = setEnv('REACT_APP_COMMODITIES_LIST_RESOURCE_ID', ''); + +export const PRACTITIONER_TO_ORG_ASSIGNMENT_STRATEGY = setEnv( + 'REACT_APP_PRACTITIONER_TO_ORG_ASSIGNMENT_STRATEGY', + undefined +); diff --git a/docs/env.md b/docs/env.md index e7501d728..90fdf3b70 100644 --- a/docs/env.md +++ b/docs/env.md @@ -343,6 +343,12 @@ Below is a list of currently supported environment variables: - default: 'false' - **REACT_APP_COMMODITIES_LIST_RESOURCE_ID** + - scopes down what commodities are shown on the fhir commidities module. - **Conditionally Optional**(`string`) - required when `REACT_APP_ENABLE_FHIR_COMMODITY` is set to `"true"` - default: '' + +- **REACT_APP_PRACTITIONER_TO_ORG_ASSIGNMENT_STRATEGY=ONE_TO_ONE** + - define the assignment relationship between practitioners to organizations for fhir deployments. This strategy only applies unilaterally, i.e. form practitioner to organization. It does not imply any relations in the opposite direction i.e. from organization to practitioner. + - **Optional**(`ONE_TO_ONE, ONE_TO_MANY`) + - default: `ONE_TO_MANY` diff --git a/docs/fhir-web-docker-deployment.md b/docs/fhir-web-docker-deployment.md index 6aa644630..5fb4ae2af 100644 --- a/docs/fhir-web-docker-deployment.md +++ b/docs/fhir-web-docker-deployment.md @@ -176,6 +176,7 @@ We use different technologies to deploy OpenSRP FHIR Web. This documentation wil REACT_APP_OPENSRP_LOGOUT_URL: 'null', REACT_APP_OPENSRP_ROLES: '{"USERS":"ROLE_EDIT_KEYCLOAK_USERS","LOCATIONS":"ROLE_VIEW_KEYCLOAK_USERS","TEAMS":"ROLE_VIEW_KEYCLOAK_USERS","CARE_TEAM":"ROLE_VIEW_KEYCLOAK_USERS","QUEST":"ROLE_VIEW_KEYCLOAK_USERS","HEALTHCARE_SERVICE":"ROLE_VIEW_KEYCLOAK_USERS","GROUP":"ROLE_VIEW_KEYCLOAK_USERS","COMMODITY":"ROLE_VIEW_KEYCLOAK_USERS",}', + REACT_APP_PRACTITIONER_TO_ORG_ASSIGNMENT_STRATEGY: 'ONE_TO_MANY', // optional sentry config // REACT_APP_SENTRY_CONFIG_JSON: "{\"dsn\":\"\",\"environment\":\"\",\"release\":\"\",\"release-name\":\"\",\"release-namespace\":\"\",\"tags\":{}}", diff --git a/packages/fhir-team-management/src/components/AddEditOrganization/Form.tsx b/packages/fhir-team-management/src/components/AddEditOrganization/Form.tsx index 29b74bf66..6ae41f4c3 100644 --- a/packages/fhir-team-management/src/components/AddEditOrganization/Form.tsx +++ b/packages/fhir-team-management/src/components/AddEditOrganization/Form.tsx @@ -35,6 +35,7 @@ import { validationRulesFactory, } from './utils'; import { formItemLayout, tailLayout } from '@opensrp/react-utils'; +import { PractToOrgAssignmentStrategy } from '@opensrp/pkg-config'; const { Item: FormItem } = Form; interface OrganizationFormProps { @@ -45,6 +46,8 @@ interface OrganizationFormProps { successUrl?: string; practitioners: IPractitioner[]; existingPractitionerRoles: IPractitionerRole[]; + allPractitionerRoles: IPractitionerRole[]; + configuredPractAssignmentStrategy?: PractToOrgAssignmentStrategy; } const defaultProps = { @@ -61,6 +64,8 @@ const OrganizationForm = (props: OrganizationFormProps) => { successUrl, practitioners, existingPractitionerRoles, + allPractitionerRoles, + configuredPractAssignmentStrategy, } = props; const queryClient = useQueryClient(); @@ -117,7 +122,12 @@ const OrganizationForm = (props: OrganizationFormProps) => { { label: t('Inactive'), value: false }, ]; - const practitionersSelectOptions = getPractitionerOptions(practitioners); + const practitionersSelectOptions = getPractitionerOptions( + practitioners, + existingPractitionerRoles, + allPractitionerRoles, + configuredPractAssignmentStrategy + ); const validationRules = validationRulesFactory(t); return ( diff --git a/packages/fhir-team-management/src/components/AddEditOrganization/index.tsx b/packages/fhir-team-management/src/components/AddEditOrganization/index.tsx index 3c19a8e2d..f04551ca9 100644 --- a/packages/fhir-team-management/src/components/AddEditOrganization/index.tsx +++ b/packages/fhir-team-management/src/components/AddEditOrganization/index.tsx @@ -21,6 +21,7 @@ import type { IPractitionerRole } from '@smile-cdr/fhirts/dist/FHIR-R4/interface import { IOrganization } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IOrganization'; import { getOrgFormFields } from './utils'; import { useTranslation } from '../../mls'; +import { getConfig } from '@opensrp/pkg-config'; export interface AddEditOrganizationProps { fhirBaseURL: string; @@ -35,6 +36,7 @@ export const AddEditOrganization = (props: AddEditOrganizationProps) => { const { id: orgId } = useParams(); const { t } = useTranslation(); + const configuredPractAssignmentStrategy = getConfig('practToOrgAssignmentStrategy'); const organization = useQuery( [organizationResourceType, orgId], @@ -57,12 +59,9 @@ export const AddEditOrganization = (props: AddEditOrganizationProps) => { ); // practitioners already assigned to this organization - const assignedPractitioners = useQuery( + const allPractitionerRoles = useQuery( [practitionerResourceType, organizationResourceType, orgId], - () => - loadAllResources(fhirBaseUrl, practitionerRoleResourceType, { - organization: orgId as string, - }), + () => loadAllResources(fhirBaseUrl, practitionerRoleResourceType), { onError: () => sendErrorNotification(t('An Error occurred')), select: (res) => { @@ -75,7 +74,7 @@ export const AddEditOrganization = (props: AddEditOrganizationProps) => { if ( (!organization.isIdle && organization.isLoading) || (!practitioners.isIdle && practitioners.isLoading) || - (!assignedPractitioners.isIdle && assignedPractitioners.isLoading) + (!allPractitionerRoles.isIdle && allPractitionerRoles.isLoading) ) { return ; } @@ -84,7 +83,13 @@ export const AddEditOrganization = (props: AddEditOrganizationProps) => { return ; } - const initialValues = getOrgFormFields(organization.data, assignedPractitioners.data); + const assignedPractitionerRoles = (allPractitionerRoles.data ?? []).filter( + (practitionerRole) => + practitionerRole.organization?.reference === + `${organizationResourceType}/${(organization.data as IOrganization).id}` + ); + + const initialValues = getOrgFormFields(organization.data, assignedPractitionerRoles); const pageTitle = organization.data ? t('Edit team | {{teamName}}', { teamName: organization.data.name ?? '' }) @@ -101,9 +106,11 @@ export const AddEditOrganization = (props: AddEditOrganizationProps) => { fhirBaseUrl={fhirBaseUrl} initialValues={initialValues} practitioners={practitioners.data ?? []} - existingPractitionerRoles={assignedPractitioners.data ?? []} + existingPractitionerRoles={assignedPractitionerRoles} + allPractitionerRoles={allPractitionerRoles.data ?? []} cancelUrl={ORGANIZATION_LIST_URL} successUrl={ORGANIZATION_LIST_URL} + configuredPractAssignmentStrategy={configuredPractAssignmentStrategy} /> diff --git a/packages/fhir-team-management/src/components/AddEditOrganization/tests/Form.test.tsx b/packages/fhir-team-management/src/components/AddEditOrganization/tests/Form.test.tsx index 3b45600cb..241e78797 100644 --- a/packages/fhir-team-management/src/components/AddEditOrganization/tests/Form.test.tsx +++ b/packages/fhir-team-management/src/components/AddEditOrganization/tests/Form.test.tsx @@ -29,6 +29,7 @@ import { IPractitionerRole } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IPr import { getOrgFormFields } from '../utils'; import * as notifications from '@opensrp/notifications'; import userEvents from '@testing-library/user-event'; +import { PractToOrgAssignmentStrategy } from '@opensrp/pkg-config'; jest.mock('@opensrp/notifications', () => ({ __esModule: true, @@ -72,6 +73,7 @@ describe('OrganizationForm', () => { practitioners: getResourcesFromBundle(allPractitioners), existingPractitionerRoles: [], initialValues: getOrgFormFields(), + allPractitionerRoles: [], }; beforeAll(() => { @@ -407,4 +409,42 @@ describe('OrganizationForm', () => { wrapper.unmount(); }); + + it('#1210 - assigns practitioners to organizations using a 1-1 assignment strategy', () => { + const container = document.createElement('div'); + document.body.appendChild(container); + + const someMockURL = '/someURL'; + + const props = { + ...formProps, + configuredPractAssignmentStrategy: PractToOrgAssignmentStrategy.ONE_TO_ONE, + allPractitionerRoles: getResourcesFromBundle(org105Practitioners), + existingPractitionerRoles: [], + }; + + const wrapper = mount( + + + , + { attachTo: container } + ); + + // simulate value selection for members + wrapper.find('input#members').simulate('mousedown'); + + const optionTexts = [ + ...document.querySelectorAll( + '#members_list+div.rc-virtual-list .ant-select-item-option-content' + ), + ].map((option) => { + return option.textContent; + }); + + // three options instead of 5 + expect(optionTexts).toHaveLength(3); + expect(optionTexts).toEqual(['Ward N Williams MD', 'Allay Allan', 'test fhir']); + + wrapper.unmount(); + }); }); diff --git a/packages/fhir-team-management/src/components/AddEditOrganization/tests/index.test.tsx b/packages/fhir-team-management/src/components/AddEditOrganization/tests/index.test.tsx index 2b016fc3d..ee04d6346 100644 --- a/packages/fhir-team-management/src/components/AddEditOrganization/tests/index.test.tsx +++ b/packages/fhir-team-management/src/components/AddEditOrganization/tests/index.test.tsx @@ -15,7 +15,7 @@ import { practitionerResourceType, practitionerRoleResourceType, } from '../../../constants'; -import { allPractitioners, org105, org105Practitioners } from '../tests/fixtures'; +import { allPractitioners, org105 } from '../tests/fixtures'; jest.mock('fhirclient', () => { return jest.requireActual('fhirclient/lib/entry/browser'); @@ -111,11 +111,11 @@ test('renders correctly for edit locations', async () => { nock(props.fhirBaseURL) .get(`/${practitionerRoleResourceType}/_search`) - .query({ _summary: 'count', organization: '105' }) + .query({ _summary: 'count' }) .reply(200, { total: 1000 }) .get(`/${practitionerRoleResourceType}/_search`) - .query({ _count: 1000, organization: '105' }) - .reply(200, org105Practitioners); + .query({ _count: 1000 }) + .reply(200, allPractitioners); render( diff --git a/packages/fhir-team-management/src/components/AddEditOrganization/utils.ts b/packages/fhir-team-management/src/components/AddEditOrganization/utils.ts index 71d46e742..bb509bef8 100644 --- a/packages/fhir-team-management/src/components/AddEditOrganization/utils.ts +++ b/packages/fhir-team-management/src/components/AddEditOrganization/utils.ts @@ -10,11 +10,12 @@ import { practitionerResourceType, } from '../../constants'; import { getObjLike, parseFhirHumanName, IdentifierUseCodes } from '@opensrp/react-utils'; -import { flatten } from 'lodash'; +import { flatten, groupBy } from 'lodash'; import { Rule } from 'rc-field-form/lib/interface'; import { v4 } from 'uuid'; import { IPractitioner } from '@smile-cdr/fhirts/dist/FHIR-R4/interfaces/IPractitioner'; import type { TFunction } from '@opensrp/i18n'; +import { PractToOrgAssignmentStrategy } from '@opensrp/pkg-config'; export interface OrganizationFormFields { id?: string; @@ -172,9 +173,30 @@ export const getAssignedPractsOptions = (roles: IPractitionerRole[]) => { * map practitioner to select options * * @param practitioners - map of practitioners + * @param existingPractitionerRoles - practitioner Roles that reference organizatio, [] when creating an organization + * @param allPractitionerRoles - all practitioner roles resources + * @param assignmentStrategy - strategy to use when generating options to assign */ -export const getPractitionerOptions = (practitioners: IPractitioner[]) => { - return practitioners.map((pract) => { +export const getPractitionerOptions = ( + practitioners: IPractitioner[], + existingPractitionerRoles: IPractitionerRole[], + allPractitionerRoles: IPractitionerRole[], + assignmentStrategy?: PractToOrgAssignmentStrategy +) => { + let allowedPractitioners = practitioners; + const rolesWithOrganizations = allPractitionerRoles.filter( + (practRole) => practRole.organization?.reference + ); + // group allPractitionerRoles by practitioner references + const rolesByPractReference = groupBy(rolesWithOrganizations, 'practitioner.reference'); + + if (assignmentStrategy && assignmentStrategy === PractToOrgAssignmentStrategy.ONE_TO_ONE) { + allowedPractitioners = allowedPractitioners.filter((pract) => { + const practReference = `${pract.resourceType}/${pract.id}`; + return !rolesByPractReference[practReference] as boolean; + }); + } + const newPractitionerOptions = allowedPractitioners.map((pract) => { const nameObj = getObjLike(pract.name, 'use', HumanNameUseCodes.OFFICIAL)[0]; const value = `${practitionerResourceType}/${pract.id}`; const label = parseFhirHumanName(nameObj); @@ -183,4 +205,13 @@ export const getPractitionerOptions = (practitioners: IPractitioner[]) => { label: label ?? value, }; }); + const existingPractitionerOptions = existingPractitionerRoles.map((role) => { + const value = role.practitioner?.reference as string; + const label = role.practitioner?.display; + return { + value, + label: label ?? value, + }; + }); + return [...newPractitionerOptions, ...existingPractitionerOptions]; }; diff --git a/packages/pkg-config/src/configStore/index.ts b/packages/pkg-config/src/configStore/index.ts index e01be1930..c6743198c 100644 --- a/packages/pkg-config/src/configStore/index.ts +++ b/packages/pkg-config/src/configStore/index.ts @@ -25,12 +25,23 @@ export interface ConfigState { opensrpBaseURL?: string; fhirBaseURL?: string; defaultTablesPageSize?: number; // static value of the default number of rows per page + practToOrgAssignmentStrategy?: PractToOrgAssignmentStrategy; } export interface UserPreference { tablespref?: Record; } +/** + * This strategy only applies unilaterally, i.e. from practitioner to organization. + * It does not imply any relations in the opposite direction i.e. from organization + * to practitioner. + */ +export enum PractToOrgAssignmentStrategy { + ONE_TO_ONE = 'ONE_TO_ONE', // one practitioner assignable to one organization + ONE_TO_MANY = 'ONE_TO_MANY', // one practitioner assignable to multiple organizations +} + const defaultConfigs: GlobalState = { languageCode: 'en', appLoginURL: undefined, @@ -40,6 +51,7 @@ const defaultConfigs: GlobalState = { tablespref: undefined, defaultTablesPageSize: 5, projectCode: 'core', + practToOrgAssignmentStrategy: PractToOrgAssignmentStrategy.ONE_TO_MANY, }; let localstorage: UserPreference = localStorage.getItem(USER_PREFERENCE_KEY) diff --git a/packages/pkg-config/src/configStore/tests/index.test.tsx b/packages/pkg-config/src/configStore/tests/index.test.tsx index 5a96897bf..6ec1fea91 100644 --- a/packages/pkg-config/src/configStore/tests/index.test.tsx +++ b/packages/pkg-config/src/configStore/tests/index.test.tsx @@ -16,6 +16,7 @@ describe('pkg-configs/configStore', () => { keycloakBaseURL: undefined, languageCode: 'en', opensrpBaseURL: undefined, + practToOrgAssignmentStrategy: 'ONE_TO_MANY', fhirBaseURL: undefined, projectCode: 'core', defaultTablesPageSize: 5,