diff --git a/src/libs/WorkflowUtils.ts b/src/libs/WorkflowUtils.ts index 61f784d8f2283..bd81db204418c 100644 --- a/src/libs/WorkflowUtils.ts +++ b/src/libs/WorkflowUtils.ts @@ -574,6 +574,23 @@ function getOpenConnectedToPolicyBusinessBankAccounts(bankAccountList: BankAccou }); } +/** + * Filter out members who are already assigned to a non-default approval workflow. + * This prevents them from appearing in the "Expenses from" picker when creating a new workflow. + */ +function filterAvailableMembersForNewWorkflow(approvalWorkflows: ApprovalWorkflow[], availableMembers: Member[]): Member[] { + const membersInExistingWorkflows = new Set(); + for (const workflow of approvalWorkflows) { + if (workflow.isDefault) { + continue; + } + for (const member of workflow.members) { + membersInExistingWorkflows.add(member.email); + } + } + return availableMembers.filter((member) => !membersInExistingWorkflows.has(member.email)); +} + /** * Combine workflow members with available members, deduplicating by email. */ @@ -587,6 +604,7 @@ export { calculateApprovers, convertPolicyEmployeesToApprovalWorkflows, convertApprovalWorkflowToPolicyEmployees, + filterAvailableMembersForNewWorkflow, getApprovalLimitDescription, getEligibleExistingBusinessBankAccounts, getOpenConnectedToPolicyBusinessBankAccounts, diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index e9ffc40bf3ea9..713dd40fc865c 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -51,7 +51,7 @@ import { } from '@libs/PolicyUtils'; import {hasInProgressVBBA} from '@libs/ReimbursementAccountUtils'; import tokenizedSearch from '@libs/tokenizedSearch'; -import {convertPolicyEmployeesToApprovalWorkflows, getEligibleExistingBusinessBankAccounts, INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils'; +import {convertPolicyEmployeesToApprovalWorkflows, filterAvailableMembersForNewWorkflow, getEligibleExistingBusinessBankAccounts, INITIAL_APPROVAL_WORKFLOW} from '@libs/WorkflowUtils'; import type {WorkspaceSplitNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import ExpenseReportRulesSection from '@pages/workspace/rules/ExpenseReportRulesSection'; @@ -100,6 +100,8 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { localeCompare, }); + const availableMembersForNewWorkflow = useMemo(() => filterAvailableMembersForNewWorkflow(approvalWorkflows, availableMembers), [approvalWorkflows, availableMembers]); + const hasValidExistingAccounts = getEligibleExistingBusinessBankAccounts(bankAccountList, policy?.outputCurrency, true).length > 0; const isAdvanceApproval = (approvalWorkflows.length > 1 || (approvalWorkflows?.at(0)?.approvers ?? []).length > 1) && isControlPolicy(policy); @@ -158,7 +160,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { const addApprovalAction = useCallback(() => { setApprovalWorkflow({ ...INITIAL_APPROVAL_WORKFLOW, - availableMembers, + availableMembers: availableMembersForNewWorkflow, usedApproverEmails, }); @@ -174,7 +176,7 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { } Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_EXPENSES_FROM.getRoute(route.params.policyID)); - }, [policy, route.params.policyID, availableMembers, usedApproverEmails]); + }, [policy, route.params.policyID, availableMembersForNewWorkflow, usedApproverEmails]); const filteredApprovalWorkflows = policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.ADVANCED || policy?.approvalMode === CONST.POLICY.APPROVAL_MODE.DYNAMICEXTERNAL diff --git a/tests/unit/WorkflowUtilsTest.ts b/tests/unit/WorkflowUtilsTest.ts index 8c32b0cc0a307..794ed8fd7747a 100644 --- a/tests/unit/WorkflowUtilsTest.ts +++ b/tests/unit/WorkflowUtilsTest.ts @@ -4,6 +4,7 @@ import { calculateApprovers, convertApprovalWorkflowToPolicyEmployees, convertPolicyEmployeesToApprovalWorkflows, + filterAvailableMembersForNewWorkflow, getApprovalLimitDescription, getOpenConnectedToPolicyBusinessBankAccounts, mergeWorkflowMembersWithAvailableMembers, @@ -1006,6 +1007,60 @@ describe('WorkflowUtils', () => { }); }); + describe('filterAvailableMembersForNewWorkflow', () => { + it('Should return all members when there are no non-default workflows', () => { + const availableMembers = [buildMember(1), buildMember(2), buildMember(3)]; + const workflows = [buildWorkflow([1, 2, 3], [1], {isDefault: true})]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual(availableMembers); + }); + + it('Should filter out members already in a non-default workflow', () => { + const availableMembers = [buildMember(1), buildMember(2), buildMember(3)]; + const workflows = [buildWorkflow([1, 2, 3], [1], {isDefault: true}), buildWorkflow([2], [4])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual([buildMember(1), buildMember(3)]); + }); + + it('Should filter out members from multiple non-default workflows', () => { + const availableMembers = [buildMember(1), buildMember(2), buildMember(3), buildMember(4)]; + const workflows = [buildWorkflow([1, 2, 3, 4], [1], {isDefault: true}), buildWorkflow([1], [5]), buildWorkflow([3], [6])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual([buildMember(2), buildMember(4)]); + }); + + it('Should return all members when workflows list is empty', () => { + const availableMembers = [buildMember(1), buildMember(2)]; + + const result = filterAvailableMembersForNewWorkflow([], availableMembers); + + expect(result).toEqual(availableMembers); + }); + + it('Should return empty array when all members are in non-default workflows', () => { + const availableMembers = [buildMember(1), buildMember(2)]; + const workflows = [buildWorkflow([1, 2], [3])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, availableMembers); + + expect(result).toEqual([]); + }); + + it('Should return empty array when available members list is empty', () => { + const workflows = [buildWorkflow([1], [2])]; + + const result = filterAvailableMembersForNewWorkflow(workflows, []); + + expect(result).toEqual([]); + }); + }); + describe('getOpenConnectedToPolicyBusinessBankAccounts', () => { const matchingBankAccountID = 12345;