From 6ea071982e229d1afe91afb00e42c14ce940475b Mon Sep 17 00:00:00 2001 From: Ogollah Date: Thu, 9 Jan 2025 18:15:42 +0300 Subject: [PATCH 1/2] Create provider account --- .../user-management.workspace.scss | 30 +- .../user-management.workspace.tsx | 397 +++++++++++++++--- .../users/userManagementFormSchema.tsx | 4 + packages/esm-admin-app/src/config-schema.ts | 39 ++ packages/esm-admin-app/src/routes.json | 5 +- .../src/user-management.resources.ts | 84 +++- .../src/navbar/nav-utils.tsx | 6 +- 7 files changed, 464 insertions(+), 101 deletions(-) diff --git a/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.scss b/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.scss index 6ffb3ff6..ac0d19fb 100644 --- a/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.scss +++ b/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.scss @@ -37,19 +37,6 @@ border-radius: layout.$spacing-05; } -.listItem { - list-style: none; - padding: layout.$spacing-03; - cursor: pointer; - background-color: colors.$gray-10; - font-size: 14px; - transition: background-color 0.3s ease; -} - -.listItem.active { - background-color: colors.$gray-20; -} - .checkboxLabelSingleLine { white-space: nowrap; overflow: hidden; @@ -98,39 +85,38 @@ font-size: 14px; } -.leftTabsContainer { +.leftContainer { display: flex; flex-direction: column; border-radius: layout.$spacing-05; overflow: hidden; } -.leftTabsLayout { +.leftLayout { display: flex; flex-direction: row; height: 100%; } -.tabList { +.progressIndicator { display: flex; flex-direction: column; min-width: 200px; - background-color: colors.$white; - padding: layout.$spacing-03; + padding-top: layout.$spacing-03; border-right: 1px solid colors.$gray-30; } -.tabPanels { +.sections { flex: 1; padding: layout.$spacing-04; } @media (max-width: 768px) { - .leftTabsLayout { + .leftLayout { flex-direction: column; } - .tabList { + .progressIndicator { flex-direction: row; border-right: none; border-bottom: 1px solid colors.$gray-30; @@ -139,7 +125,7 @@ padding: layout.$spacing-02; } - .tabPanels { + .sections { padding: layout.$spacing-03; } } diff --git a/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx b/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx index b5f5dc11..53b58f84 100644 --- a/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx +++ b/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx @@ -23,18 +23,27 @@ import { Select, PasswordInput, Column, - ClickableTile, - Tile, + ProgressIndicator, + ProgressStep, + ComboBox, } from '@carbon/react'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import classNames from 'classnames'; -import { createUser, handleMutation, useRoles, usePersonAttribute } from '../../../user-management.resources'; +import { + createUser, + handleMutation, + useRoles, + usePersonAttribute, + useProvider, + useProviderAttributeType, + useLocation, +} from '../../../user-management.resources'; import UserManagementFormSchema from '../userManagementFormSchema'; import { CardHeader } from '@openmrs/esm-patient-common-lib/src'; import { ChevronSortUp } from '@carbon/react/icons'; import { useSystemUserRoleConfigSetting } from '../../hook/useSystemRoleSetting'; -import { User } from '../../../config-schema'; +import { Provider, User } from '../../../config-schema'; type ManageUserWorkspaceProps = DefaultWorkspaceProps & { initialUserValue?: User; @@ -49,27 +58,84 @@ const ManageUserWorkspace: React.FC = ({ const { t } = useTranslation(); const isTablet = useLayoutType() === 'tablet'; const [activeSection, setActiveSection] = useState('demographic'); + const [currentIndex, setCurrentIndex] = useState(0); const { userManagementFormSchema } = UserManagementFormSchema(); + const { providerAttributeType = [] } = useProviderAttributeType(); + const providerLicenseAttributeType = + providerAttributeType.find((type) => type.name === 'Practising License Number')?.uuid || ''; + const licenseExpiryDateAttributeType = + providerAttributeType.find((type) => type.name === 'License Expiry Date')?.uuid || ''; + const primaryFacilityAttributeType = + providerAttributeType.find((type) => type.name === 'Primary Facility')?.uuid || ''; + + const { provider = [], loadingProvider, providerError } = useProvider(initialUserValue.systemId); + const { location, loadingLocation } = useLocation(); + + function getProviderAttributes() { + if (!Array.isArray(provider)) { + return []; + } + return provider.flatMap((item) => item.attributes || []); + } + + function getProviderLicenseNumber() { + const providerAttributes = getProviderAttributes(); + const providerLicense = providerAttributes.find( + (attr) => attr.attributeType?.uuid === providerLicenseAttributeType && attr.value, + ); + return providerLicense?.value; + } + + function getPrimaryFacility() { + const providerAttributes = getProviderAttributes(); + const primaryFacility = providerAttributes.find( + (attr) => attr.attributeType?.uuid === primaryFacilityAttributeType && attr.value, + ); + if (primaryFacility && primaryFacility.value) { + if (typeof primaryFacility.value === 'object' && primaryFacility.value !== null) { + return primaryFacility.value?.name; + } + } + } + + function getProviderLicenseExpiryDate() { + const providerAttributes = getProviderAttributes(); + const licenseExpiryDate = providerAttributes.find( + (attr) => attr.attributeType?.uuid === licenseExpiryDateAttributeType && attr.value, + ); + + if (licenseExpiryDate?.value) { + const date = new Date(licenseExpiryDate.value); + return [ + date.getFullYear(), + String(date.getMonth() + 1).padStart(2, '0'), + String(date.getDate()).padStart(2, '0'), + ].join('-'); + } + } + const isInitialValuesEmpty = Object.keys(initialUserValue).length === 0; type UserFormSchema = z.infer; - const formDefaultValues = - Object.keys(initialUserValue).length > 0 - ? { - ...initialUserValue, - ...extractNameParts(initialUserValue.person?.display || ''), - phoneNumber: extractAttributeValue(initialUserValue.person?.attributes, 'Telephone'), - email: extractAttributeValue(initialUserValue.person?.attributes, 'Email'), - roles: - initialUserValue.roles?.map((role) => ({ - uuid: role.uuid, - display: role.display, - description: role.description, - })) || [], - gender: initialUserValue.person?.gender || 'M', - } - : {}; + const formDefaultValues = !isInitialValuesEmpty + ? { + ...initialUserValue, + ...extractNameParts(initialUserValue.person?.display || ''), + phoneNumber: extractAttributeValue(initialUserValue.person?.attributes, 'Telephone'), + email: extractAttributeValue(initialUserValue.person?.attributes, 'Email'), + roles: + initialUserValue.roles?.map((role) => ({ + uuid: role.uuid, + display: role.display, + description: role.description, + })) || [], + gender: initialUserValue.person?.gender || 'M', + providerLicense: getProviderLicenseNumber(), + licenseExpiryDate: getProviderLicenseExpiryDate(), + primaryFacility: getPrimaryFacility(), + } + : {}; function extractNameParts(display = '') { const nameParts = display.split(' '); @@ -105,6 +171,26 @@ const ManageUserWorkspace: React.FC = ({ const onSubmit = async (data: UserFormSchema) => { const emailAttribute = attributeTypes.find((attr) => attr.name === 'Email address')?.uuid || ''; const telephoneAttribute = attributeTypes.find((attr) => attr.name === 'Telephone contact')?.uuid || ''; + const setProvider = data.providerIdentifiers; + const facility = data.primaryFacility.split(' '); + const mflCode = facility[facility.length - 1]; + const providerPayload: Partial = { + attributes: [ + { + attributeType: primaryFacilityAttributeType, + value: mflCode, + }, + { + attributeType: providerLicenseAttributeType, + value: data.providerLicense, + }, + { + attributeType: licenseExpiryDateAttributeType, + value: data.licenseExpiryDate, + }, + ], + }; + const payload: Partial = { username: data.username, password: data.password, @@ -136,7 +222,7 @@ const ManageUserWorkspace: React.FC = ({ }; try { - const response = await createUser(payload, initialUserValue?.uuid ?? ''); + const response = await createUser(payload, setProvider, providerPayload, initialUserValue?.uuid ?? ''); if (response.ok) { showSnackbar({ @@ -183,36 +269,34 @@ const ManageUserWorkspace: React.FC = ({ }, [isDirty, promptBeforeClosing]); const toggleSection = (section) => { - setActiveSection((prev) => (prev === section ? null : section)); + setActiveSection((prev) => (prev !== section ? section : prev)); }; + const steps = [ + { id: 'demographic', label: t('demographicInformation', 'Demographic Info') }, + { id: 'provider', label: t('providerAccount', 'Provider Account') }, + { id: 'login', label: t('loginInformation', 'Login Info') }, + { id: 'roles', label: t('roles', 'Roles Info') }, + ]; + return ( -
+
-
- - toggleSection('demographic')}> - {t('demographicInformation', 'Demographic Info')} - - toggleSection('provider')}> - {t('providerAccount', 'Provider Account')} - - toggleSection('login')}> - {t('loginInformation', 'Login Info')} - - toggleSection('roles')}> - {t('roles', 'Roles Info')} - - -
+
+ { + toggleSection(steps[newIndex].id); + setCurrentIndex(newIndex); + }}> + {steps.map((step, index) => ( + + ))} + +
@@ -339,32 +423,213 @@ const ManageUserWorkspace: React.FC = ({ )} - {activeSection === 'provider' && ( - - ( - - field.onChange(e.target.checked)} + + {loadingProvider || providerError ? ( + + ) : provider.length > 0 ? ( + <> + + ( + + )} + /> + + + {loadingLocation ? ( + - + ) : ( + ( + { + if (!item) { + return ''; + } + const attributeValue = item.attributes?.[0]?.value || ''; + return `${item.name || ''} ${attributeValue}`.trim(); + }} + titleText={t('primaryFacility', 'Primary Facility')} + selectedItem={ + (location || []).find((item) => `${item.name || ''}`.trim() === field.value) || + null + } + onChange={({ selectedItem }) => { + if (selectedItem) { + const attributeValue = selectedItem.attributes?.[0]?.value || ''; + const formattedString = `${selectedItem.name || ''} ${attributeValue}`.trim(); + field.onChange(formattedString); + } else { + field.onChange(''); + } + }} + /> + )} + /> + )} + + + ( + + )} + /> + + + ( + + )} + /> + + + ) : ( + <> + ( + + field.onChange(e.target.checked)} + /> + + )} + /> + + {userFormMethods.watch('providerIdentifiers') && ( + <> + + {loadingLocation ? ( + + ) : ( + ( + { + if (!item) { + return ''; + } + const attributeValue = item.attributes?.[0]?.value || ''; + return `${item.name || ''} ${attributeValue}`.trim(); + }} + titleText={t('primaryFacility', 'Primary Facility')} + selectedItem={ + (location || []).find( + (item) => `${item.name || ''}`.trim() === field.value, + ) || null + } + onChange={({ selectedItem }) => { + if (selectedItem) { + const attributeValue = selectedItem.attributes?.[0]?.value || ''; + const formattedString = `${ + selectedItem.name || '' + } ${attributeValue}`.trim(); + field.onChange(formattedString); + } else { + field.onChange(''); + } + }} + /> + )} + /> + )} + + + ( + + )} + /> + + + ( + + )} + /> + + )} - /> - + + )} )} diff --git a/packages/esm-admin-app/src/components/users/userManagementFormSchema.tsx b/packages/esm-admin-app/src/components/users/userManagementFormSchema.tsx index a469b805..9d22dc22 100644 --- a/packages/esm-admin-app/src/components/users/userManagementFormSchema.tsx +++ b/packages/esm-admin-app/src/components/users/userManagementFormSchema.tsx @@ -30,6 +30,10 @@ const UserManagementFormSchema = () => { ) .optional(), primaryRole: z.string().optional(), + systemId: z.string().optional(), + primaryFacility: z.string().optional(), + providerLicense: z.string().optional(), + licenseExpiryDate: z.string().optional(), }); return { userManagementFormSchema }; diff --git a/packages/esm-admin-app/src/config-schema.ts b/packages/esm-admin-app/src/config-schema.ts index 4032d0d4..19439bd4 100755 --- a/packages/esm-admin-app/src/config-schema.ts +++ b/packages/esm-admin-app/src/config-schema.ts @@ -59,3 +59,42 @@ export interface UserRoleSchema { description: string; category: string; } + +export interface Provider { + uuid?: string; + identifier?: string; + retired: boolean; + attributes?: Array; +} + +export interface ProviderAttributes { + uuid?: string; + identifier?: string; + retired: boolean; + attributes?: Array; +} + +export interface AttributeItems { + attributeType?: AttributeType; + value?: any; +} + +export interface AttributeType { + uuid?: string; + description?: string; + display?: string; + name?: string; +} + +export interface AttributeValue { + name?: string; + uuid?: string; +} + +export interface ProviderLocation { + uuid: string; + name?: string; + description?: string; + retired: boolean; + attributes?: Array; +} diff --git a/packages/esm-admin-app/src/routes.json b/packages/esm-admin-app/src/routes.json index f092925c..ac04a8b0 100755 --- a/packages/esm-admin-app/src/routes.json +++ b/packages/esm-admin-app/src/routes.json @@ -26,11 +26,8 @@ "component": "manageUserWorkspace", "title": "Manage User Workspace", "type": "other-form", - "canHide": true, "canMaximize": true, - "width": "extra-wide", - "hasOwnSidebar": true - + "width": "extra-wide" } ], "modals": [ diff --git a/packages/esm-admin-app/src/user-management.resources.ts b/packages/esm-admin-app/src/user-management.resources.ts index 56e78443..8ecd9a4e 100644 --- a/packages/esm-admin-app/src/user-management.resources.ts +++ b/packages/esm-admin-app/src/user-management.resources.ts @@ -1,6 +1,6 @@ import { openmrsFetch, restBaseUrl } from '@openmrs/esm-framework'; import useSWR, { mutate } from 'swr'; -import { Role, User } from './config-schema'; +import { AttributeType, Provider, ProviderAttributes, ProviderLocation, Role, User } from './config-schema'; export const useUser = () => { const url = `${restBaseUrl}/user?v=custom:(uuid,username,display,systemId,retired,person:(uuid,display,gender,names:(givenName,familyName,middleName),attributes:(uuid,display)),roles:(uuid,description,display,name))`; @@ -15,18 +15,54 @@ export const useUser = () => { }; }; -export const createUser = (user: Partial, uuid?: string) => { - const url = uuid ? `${restBaseUrl}/user/${uuid}` : `${restBaseUrl}/user`; +export const postUser = async (user: Partial, url: string) => { + const response = await openmrsFetch(url, { + method: 'POST', + body: JSON.stringify(user), + headers: { + 'Content-Type': 'application/json', + }, + }); + return response.json(); +}; + +export const createProvider = async (uuid: string, identifier: string, attributes: Partial) => { + const providerUrl = `${restBaseUrl}/provider`; + const providerBody = { + person: uuid, + identifier: identifier, + ...attributes, + retired: false, + }; - return openmrsFetch(url, { + return await openmrsFetch(providerUrl, { method: 'POST', - body: user, + body: JSON.stringify(providerBody), headers: { 'Content-Type': 'application/json', }, }); }; +export const createUser = async ( + user: Partial, + setProvider: boolean, + attributes: Partial, + uuid?: string, +) => { + const url = uuid ? `${restBaseUrl}/user/${uuid}` : `${restBaseUrl}/user`; + + const response = await postUser(user, url); + + if (setProvider || (response.person && response.person.uuid)) { + const personUUID = response.person.uuid; + const identifier = response.systemId; + return await createProvider(personUUID, identifier, attributes); + } + + return response; +}; + export const handleMutation = (url: string) => { mutate((key) => typeof key === 'string' && key.startsWith(url), undefined, { revalidate: true }); }; @@ -46,7 +82,7 @@ export const useRoles = () => { export const usePersonAttribute = () => { const url = `${restBaseUrl}/personattributetype?v=custom:(name,uuid)`; - const { data, isLoading, error } = useSWR<{ data: { results: Array } }>(url, openmrsFetch, { + const { data, isLoading, error } = useSWR<{ data: { results: Array } }>(url, openmrsFetch, { errorRetryCount: 2, }); return { @@ -55,3 +91,39 @@ export const usePersonAttribute = () => { error, }; }; + +export const useProvider = (systemId: string) => { + const url = `${restBaseUrl}/provider?q=${systemId}&v=custom:(uuid,identifier,retired,attributes:(value:(name),attributeType:(uuid,name)))`; + const { data, isLoading, error } = useSWR<{ data: { results: Array } }>(url, openmrsFetch, { + errorRetryCount: 2, + }); + return { + provider: data?.data?.results, + loadingProvider: isLoading, + providerError: error, + }; +}; + +export const useProviderAttributeType = () => { + const url = `${restBaseUrl}/providerattributetype?v=custom:(uuid,name,display)`; + const { data, isLoading, error } = useSWR<{ data: { results: Array } }>(url, openmrsFetch, { + errorRetryCount: 2, + }); + return { + providerAttributeType: data?.data?.results, + isLoading, + error, + }; +}; + +export const useLocation = () => { + const url = `${restBaseUrl}/location?v=custom:(uuid,name,description,retired,attributes:(value))`; + const { data, isLoading, error } = useSWR<{ data: { results: Array } }>(url, openmrsFetch, { + errorRetryCount: 2, + }); + return { + location: data?.data?.results, + loadingLocation: isLoading, + locationError: error, + }; +}; diff --git a/packages/esm-patient-flags-app/src/navbar/nav-utils.tsx b/packages/esm-patient-flags-app/src/navbar/nav-utils.tsx index e2a4bf83..70b12f4f 100644 --- a/packages/esm-patient-flags-app/src/navbar/nav-utils.tsx +++ b/packages/esm-patient-flags-app/src/navbar/nav-utils.tsx @@ -101,10 +101,10 @@ export const useModuleLinks = () => { privilege: 'o3: View Bed Management Dashboard', }, { - label: 'ETL Administration', + label: 'Administration', url: `${openmrsSpaBase}admin`, - icon: , - privilege: 'o3: View ETL Administration Dashboard', + icon: , + privilage: 'o3: View Administration Dashboard', }, { label: 'Cross Border', From 4548098ef09b4a1f65b3f6d4f1073a1251f2fcfe Mon Sep 17 00:00:00 2001 From: Ogollah Date: Fri, 10 Jan 2025 10:14:34 +0300 Subject: [PATCH 2/2] add edit provider --- .../users/manage-users/user-management.workspace.tsx | 9 ++++++++- .../esm-admin-app/src/user-management.resources.ts | 12 +++++++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx b/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx index 53b58f84..43a010b7 100644 --- a/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx +++ b/packages/esm-admin-app/src/components/users/manage-users/user-management.workspace.tsx @@ -174,6 +174,7 @@ const ManageUserWorkspace: React.FC = ({ const setProvider = data.providerIdentifiers; const facility = data.primaryFacility.split(' '); const mflCode = facility[facility.length - 1]; + const providerUUID = provider[0].uuid; const providerPayload: Partial = { attributes: [ { @@ -222,7 +223,13 @@ const ManageUserWorkspace: React.FC = ({ }; try { - const response = await createUser(payload, setProvider, providerPayload, initialUserValue?.uuid ?? ''); + const response = await createUser( + payload, + setProvider, + providerPayload, + initialUserValue?.uuid ?? '', + providerUUID ?? '', + ); if (response.ok) { showSnackbar({ diff --git a/packages/esm-admin-app/src/user-management.resources.ts b/packages/esm-admin-app/src/user-management.resources.ts index 8ecd9a4e..fbe11839 100644 --- a/packages/esm-admin-app/src/user-management.resources.ts +++ b/packages/esm-admin-app/src/user-management.resources.ts @@ -26,8 +26,12 @@ export const postUser = async (user: Partial, url: string) => { return response.json(); }; -export const createProvider = async (uuid: string, identifier: string, attributes: Partial) => { - const providerUrl = `${restBaseUrl}/provider`; +export const createProvider = async ( + uuid: string, + identifier: string, + attributes: Partial, + providerUrl?: string, +) => { const providerBody = { person: uuid, identifier: identifier, @@ -49,6 +53,7 @@ export const createUser = async ( setProvider: boolean, attributes: Partial, uuid?: string, + providerUUID?: string, ) => { const url = uuid ? `${restBaseUrl}/user/${uuid}` : `${restBaseUrl}/user`; @@ -57,7 +62,8 @@ export const createUser = async ( if (setProvider || (response.person && response.person.uuid)) { const personUUID = response.person.uuid; const identifier = response.systemId; - return await createProvider(personUUID, identifier, attributes); + const providerUrl = providerUUID ? `${restBaseUrl}/provider/${providerUUID}` : `${restBaseUrl}/provider`; + return await createProvider(personUUID, identifier, attributes, providerUrl); } return response;