diff --git a/src/hooks/useAssignCard.ts b/src/hooks/useAssignCard.ts index 75d46a3a0651..6d1ac10ca348 100644 --- a/src/hooks/useAssignCard.ts +++ b/src/hooks/useAssignCard.ts @@ -197,9 +197,9 @@ function useInitialAssignCardStep({policyID, selectedFeed}: UseInitialAssignCard }; } - const employeeList = Object.values(policy?.employeeList ?? {}).filter((employee) => !isDeletedPolicyEmployee(employee, isOffline)); - if (employeeList.length === 1) { - const userEmail = Object.keys(policy?.employeeList ?? {}).at(0) ?? ''; + const activeEmployees = Object.entries(policy?.employeeList ?? {}).filter(([, employee]) => !isDeletedPolicyEmployee(employee, isOffline)); + if (activeEmployees.length === 1) { + const userEmail = activeEmployees.at(0)?.[0] ?? ''; cardToAssign.email = userEmail; const personalDetails = getPersonalDetailByEmail(userEmail); const memberName = personalDetails?.firstName ? personalDetails.firstName : personalDetails?.login; diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx index 8e8d8aa946d6..a7acf3945324 100644 --- a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -87,9 +87,11 @@ function AssigneeStep({route}: AssigneeStepProps) { const personalDetail = getPersonalDetailByEmail(assignee?.login ?? ''); const memberName = personalDetail?.firstName ? personalDetail.firstName : Str.removeSMSDomain(personalDetail?.login ?? ''); const defaultCardName = getDefaultCardName(memberName); + // Keep the name the user manually typed in CardNameStep. Otherwise always recompute it from the currently selected assignee. + const customCardName = assignCard?.cardToAssign?.isCustomCardNameEdited ? (assignCard?.cardToAssign?.customCardName ?? defaultCardName) : defaultCardName; const cardToAssign: Partial = { email: assignee?.login ?? '', - ...(!assignCard?.cardToAssign?.customCardName ? {customCardName: defaultCardName} : {}), + customCardName, }; Keyboard.dismiss(); @@ -100,7 +102,6 @@ function AssigneeStep({route}: AssigneeStepProps) { if (assignCard?.cardToAssign?.encryptedCardNumber) { cardToAssign.encryptedCardNumber = assignCard.cardToAssign.encryptedCardNumber; cardToAssign.cardName = assignCard.cardToAssign.cardName; - cardToAssign.customCardName = assignCard.cardToAssign.customCardName ?? defaultCardName; cardToAssign.startDate = getCardAssignmentStartDate(isEditing, assignCard?.cardToAssign?.startDate); cardToAssign.dateOption = getCardAssignmentDateOption(isEditing, assignCard?.cardToAssign?.dateOption); setAssignCardStepAndData({ @@ -136,7 +137,6 @@ function AssigneeStep({route}: AssigneeStepProps) { if (assignCard?.cardToAssign?.encryptedCardNumber) { cardToAssign.encryptedCardNumber = assignCard.cardToAssign.encryptedCardNumber; cardToAssign.cardName = assignCard.cardToAssign.cardName; - cardToAssign.customCardName = assignCard.cardToAssign.customCardName ?? defaultCardName; cardToAssign.startDate = getCardAssignmentStartDate(isEditing, assignCard?.cardToAssign?.startDate); cardToAssign.dateOption = getCardAssignmentDateOption(isEditing, assignCard?.cardToAssign?.dateOption); setAssignCardStepAndData({ diff --git a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx index c442759a7665..067c14746f36 100644 --- a/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/CardNameStep.tsx @@ -45,6 +45,7 @@ function CardNameStep({route}: CardNameStepProps) { setAssignCardStepAndData({ cardToAssign: { customCardName: values.name, + isCustomCardNameEdited: true, }, isEditing: false, }); diff --git a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx index 6603a53cd2d4..b793ccccbfd1 100644 --- a/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx +++ b/src/pages/workspace/companyCards/assignCard/InviteNewMemberStep.tsx @@ -12,6 +12,7 @@ import {getCardAssignmentDateOption, getCardAssignmentStartDate, getDefaultCardN import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getPersonalDetailByEmail} from '@libs/PersonalDetailsUtils'; import Navigation from '@navigation/Navigation'; @@ -27,6 +28,7 @@ import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {AssignCardData} from '@src/types/onyx/AssignCard'; +import {Str} from 'expensify-common'; import React, {useEffect} from 'react'; type InviteeNewMemberStepProps = PlatformStackScreenProps & @@ -59,10 +61,15 @@ function InviteNewMemberStep({route, currentUserPersonalDetails}: InviteeNewMemb }; const goToNextStep = () => { - const defaultCardName = getDefaultCardName(assignCard?.cardToAssign?.invitingMemberEmail); + const invitingMemberEmail = assignCard?.cardToAssign?.invitingMemberEmail ?? ''; + const personalDetail = getPersonalDetailByEmail(invitingMemberEmail); + const memberName = personalDetail?.firstName ? personalDetail.firstName : Str.removeSMSDomain(personalDetail?.login ?? invitingMemberEmail); + const defaultCardName = getDefaultCardName(memberName); + // Keep the name the user manually typed in CardNameStep. Otherwise always recompute it from the inviting member. + const customCardName = assignCard?.cardToAssign?.isCustomCardNameEdited ? (assignCard?.cardToAssign?.customCardName ?? defaultCardName) : defaultCardName; const cardToAssign: Partial = { email: assignCard?.cardToAssign?.invitingMemberEmail, - customCardName: defaultCardName, + customCardName, invitingMemberEmail: '', }; @@ -71,7 +78,6 @@ function InviteNewMemberStep({route, currentUserPersonalDetails}: InviteeNewMemb if (assignCard?.cardToAssign?.encryptedCardNumber) { cardToAssign.encryptedCardNumber = assignCard.cardToAssign.encryptedCardNumber; cardToAssign.cardName = assignCard.cardToAssign.cardName; - cardToAssign.customCardName = assignCard.cardToAssign.customCardName ?? defaultCardName; cardToAssign.startDate = getCardAssignmentStartDate(true, assignCard?.cardToAssign?.startDate); cardToAssign.dateOption = getCardAssignmentDateOption(true, assignCard?.cardToAssign?.dateOption); setAssignCardStepAndData({ diff --git a/src/types/onyx/AssignCard.ts b/src/types/onyx/AssignCard.ts index 189a9295a497..65a134a905ea 100644 --- a/src/types/onyx/AssignCard.ts +++ b/src/types/onyx/AssignCard.ts @@ -47,6 +47,12 @@ type AssignCardData = { */ customCardName: string; + /** + * Whether the user has manually edited the custom card name in CardNameStep. + * When true, the name is preserved instead of being recomputed from the selected assignee. + */ + isCustomCardNameEdited: boolean; + /** The transaction start date of the card */ startDate: string; diff --git a/tests/unit/hooks/useAssignCard.test.ts b/tests/unit/hooks/useAssignCard.test.ts index 158377e293f7..5d7e358cfada 100644 --- a/tests/unit/hooks/useAssignCard.test.ts +++ b/tests/unit/hooks/useAssignCard.test.ts @@ -8,6 +8,8 @@ import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import {setAssignCardStepAndData} from '@libs/actions/CompanyCards'; + import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {CombinedCardFeeds, CompanyCardFeedWithDomainID, Policy} from '@src/types/onyx'; @@ -272,4 +274,38 @@ describe('useAssignCard', () => { expect(() => result.current.assignCard(cardName, cardID)).not.toThrow(); }); }); + + describe('assignCard function - single active employee shortcut', () => { + it('should auto-assign to the single active employee, not the first (deleted) employee in the list', () => { + // Given a policy whose first (unfiltered) employee is pending deletion, leaving a single active employee later in the list + const policyWithDeletedFirstEmployee = createMock({ + id: mockPolicyID, + policyAccountID: workspaceAccountID, + employeeList: { + 'admin@example.com': {email: 'admin@example.com', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, + 'employee@example.com': {email: 'employee@example.com'}, + }, + }); + jest.mocked(usePolicy).mockReturnValue(policyWithDeletedFirstEmployee); + jest.mocked(useCardFeeds).mockReturnValue([mockDirectFeedData, {status: 'loaded'}, undefined, {}, workspaceAccountID]); + + const {result} = renderHook(() => + useAssignCard({ + feedName: mockDirectFeed, + policyID: mockPolicyID, + setShouldShowOfflineModal: mockSetShouldShowOfflineModal, + }), + ); + + result.current.assignCard('Chase Checking 0000', 'Chase Checking 0000'); + + // Then the flow jumps to confirmation pre-assigned to the single active employee (not the deleted admin) + expect(setAssignCardStepAndData).toHaveBeenCalledWith( + expect.objectContaining({ + currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, + cardToAssign: expect.objectContaining({email: 'employee@example.com'}), + }), + ); + }); + }); });