diff --git a/src/languages/en.ts b/src/languages/en.ts index a864e70b6189c..54fbdb7fc9a17 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -49,6 +49,7 @@ import type { PaySomeoneParams, ReimbursementRateParams, RemovedTheRequestParams, + RemoveMembersWarningPrompt, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, ReportArchiveReasonsMergedParams, @@ -2369,6 +2370,8 @@ export default { people: { genericFailureMessage: 'An error occurred removing a user from the workspace, please try again.', removeMembersPrompt: 'Are you sure you want to remove these members?', + removeMembersWarningPrompt: ({memberName, ownerName}: RemoveMembersWarningPrompt) => + `${memberName} is an approver in this workspace. When you unshare this workspace with them, we’ll replace them in the approval workflow with the workspace owner, ${ownerName}`, removeMembersTitle: 'Remove members', removeMemberButtonTitle: 'Remove from workspace', removeMemberGroupButtonTitle: 'Remove from group', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2a11549e73550..a72dcf4af8afc 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -49,6 +49,7 @@ import type { PaySomeoneParams, ReimbursementRateParams, RemovedTheRequestParams, + RemoveMembersWarningPrompt, RenamedRoomActionParams, ReportArchiveReasonsClosedParams, ReportArchiveReasonsMergedParams, @@ -2404,6 +2405,8 @@ export default { people: { genericFailureMessage: 'Se ha producido un error al intentar eliminar a un miembro del espacio de trabajo. Por favor, inténtalo más tarde.', removeMembersPrompt: '¿Estás seguro de que deseas eliminar a estos miembros?', + removeMembersWarningPrompt: ({memberName, ownerName}: RemoveMembersWarningPrompt) => + `${memberName} es un aprobador en este espacio de trabajo. Cuando lo elimine de este espacio de trabajo, los sustituiremos en el flujo de trabajo de aprobación por el propietario del espacio de trabajo, ${ownerName}`, removeMembersTitle: 'Eliminar miembros', removeMemberButtonTitle: 'Quitar del espacio de trabajo', removeMemberGroupButtonTitle: 'Quitar del grupo', diff --git a/src/languages/types.ts b/src/languages/types.ts index de9b1d2dadebc..c38fb4aadae57 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -298,6 +298,11 @@ type DistanceRateOperationsParams = {count: number}; type ReimbursementRateParams = {unit: Unit}; +type RemoveMembersWarningPrompt = { + memberName: string; + ownerName: string; +}; + export type { AddressLineParams, AdminCanceledRequestParams, @@ -402,4 +407,5 @@ export type { WelcomeNoteParams, WelcomeToRoomParams, ZipCodeExampleFormatParams, + RemoveMembersWarningPrompt, }; diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 9874a175d0a23..f8472bd430984 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -101,6 +101,14 @@ Onyx.connect({ }, }); +/** Check if the passed employee is an approver in the policy's employeeList */ +function isApprover(policy: OnyxEntry, employeeAccountID: number) { + const employeeLogin = allPersonalDetails?.[employeeAccountID]?.login; + return Object.values(policy?.employeeList ?? {}).some( + (employee) => employee?.submitsTo === employeeLogin || employee?.forwardsTo === employeeLogin || employee?.overLimitForwardsTo === employeeLogin, + ); +} + /** * Returns the policy of the report */ @@ -243,6 +251,42 @@ function removeMembers(accountIDs: number[], policyID: string) { failureMembersState[email] = {errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.people.error.genericRemove')}; }); + Object.keys(policy?.employeeList ?? {}).forEach((employeeEmail) => { + const employee = policy?.employeeList?.[employeeEmail]; + optimisticMembersState[employeeEmail] = optimisticMembersState[employeeEmail] ?? {}; + failureMembersState[employeeEmail] = failureMembersState[employeeEmail] ?? {}; + if (employee?.submitsTo && emailList.includes(employee?.submitsTo)) { + optimisticMembersState[employeeEmail] = { + ...optimisticMembersState[employeeEmail], + submitsTo: policy?.owner, + }; + failureMembersState[employeeEmail] = { + ...failureMembersState[employeeEmail], + submitsTo: employee?.submitsTo, + }; + } + if (employee?.forwardsTo && emailList.includes(employee?.forwardsTo)) { + optimisticMembersState[employeeEmail] = { + ...optimisticMembersState[employeeEmail], + forwardsTo: policy?.owner, + }; + failureMembersState[employeeEmail] = { + ...failureMembersState[employeeEmail], + forwardsTo: employee?.forwardsTo, + }; + } + if (employee?.overLimitForwardsTo && emailList.includes(employee?.overLimitForwardsTo)) { + optimisticMembersState[employeeEmail] = { + ...optimisticMembersState[employeeEmail], + overLimitForwardsTo: policy?.owner, + }; + failureMembersState[employeeEmail] = { + ...failureMembersState[employeeEmail], + overLimitForwardsTo: employee?.overLimitForwardsTo, + }; + } + }); + const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -801,6 +845,7 @@ export { inviteMemberToWorkspace, acceptJoinRequest, declineJoinRequest, + isApprover, }; export type {NewCustomUnit}; diff --git a/src/pages/workspace/WorkspaceMembersPage.tsx b/src/pages/workspace/WorkspaceMembersPage.tsx index 76665a9232248..8f1bb6ee12bad 100644 --- a/src/pages/workspace/WorkspaceMembersPage.tsx +++ b/src/pages/workspace/WorkspaceMembersPage.tsx @@ -36,6 +36,7 @@ import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; +import {getDisplayNameForParticipant} from '@libs/ReportUtils'; import * as Member from '@userActions/Policy/Member'; import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; @@ -95,6 +96,17 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, const selectionListRef = useRef(null); const isFocused = useIsFocused(); const policyID = route.params.policyID; + + const confirmModalPrompt = useMemo(() => { + const approverAccountID = selectedEmployees.find((selectedEmployee) => Member.isApprover(policy, selectedEmployee)); + if (!approverAccountID) { + return translate('workspace.people.removeMembersPrompt'); + } + return translate('workspace.people.removeMembersWarningPrompt', { + memberName: getDisplayNameForParticipant(approverAccountID), + ownerName: getDisplayNameForParticipant(policy?.ownerAccountID), + }); + }, [selectedEmployees, policy, translate]); /** * Get filtered personalDetails list with current employeeList */ @@ -549,7 +561,7 @@ function WorkspaceMembersPage({personalDetails, invitedEmailsToAccountIDsDraft, isVisible={removeMembersConfirmModalVisible} onConfirm={removeUsers} onCancel={() => setRemoveMembersConfirmModalVisible(false)} - prompt={translate('workspace.people.removeMembersPrompt')} + prompt={confirmModalPrompt} confirmText={translate('common.remove')} cancelText={translate('common.cancel')} onModalHide={() => { diff --git a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx index 1675c78b2efb3..c23266f578913 100644 --- a/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx +++ b/src/pages/workspace/members/WorkspaceMemberDetailsPage.tsx @@ -66,6 +66,14 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM const isCurrentUserAdmin = policy?.employeeList?.[personalDetails?.[currentUserPersonalDetails?.accountID]?.login ?? '']?.role === CONST.POLICY.ROLE.ADMIN; const isCurrentUserOwner = policy?.owner === currentUserPersonalDetails?.login; + const confirmModalPrompt = useMemo(() => { + const isApprover = Member.isApprover(policy, accountID); + if (!isApprover) { + translate('workspace.people.removeMemberPrompt', {memberName: displayName}); + } + return translate('workspace.people.removeMembersWarningPrompt', {memberName: displayName, ownerName: policy?.owner ?? ''}); + }, [accountID, policy, displayName, translate]); + const roleItems: ListItemType[] = useMemo( () => [ { @@ -188,7 +196,7 @@ function WorkspaceMemberDetailsPage({personalDetails, policy, route}: WorkspaceM isVisible={isRemoveMemberConfirmModalVisible} onConfirm={removeUser} onCancel={() => setIsRemoveMemberConfirmModalVisible(false)} - prompt={translate('workspace.people.removeMemberPrompt', {memberName: displayName})} + prompt={confirmModalPrompt} confirmText={translate('common.remove')} cancelText={translate('common.cancel')} /> diff --git a/src/types/onyx/PolicyEmployee.ts b/src/types/onyx/PolicyEmployee.ts index 741e1e01ec057..91b7f16538218 100644 --- a/src/types/onyx/PolicyEmployee.ts +++ b/src/types/onyx/PolicyEmployee.ts @@ -8,12 +8,18 @@ type PolicyEmployee = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Email of the user */ email?: string; - /** Email of the user this user forwards all approved reports to */ + /** Determines if this employee should approve a report. If report total > approvalLimit, next approver will be 'overLimitForwardsTo', otherwise 'forwardsTo' */ + approvalLimit?: number; + + /** Email of the user this user forwards all approved reports to (when report total under 'approvalLimit' or when 'overLimitForwardsTo' is not set) */ forwardsTo?: string; /** Email of the user this user submits all reports to */ submitsTo?: string; + /** Email of the user this user forwards all reports to when the report total is over the 'approvalLimit' */ + overLimitForwardsTo?: string; + /** * Errors from api calls on the specific user * {: 'error message', : 'error message 2'}