Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions src/hooks/useAssignCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AssignCardData> = {
email: assignee?.login ?? '',
...(!assignCard?.cardToAssign?.customCardName ? {customCardName: defaultCardName} : {}),
customCardName,
};

Keyboard.dismiss();
Expand All @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ function CardNameStep({route}: CardNameStepProps) {
setAssignCardStepAndData({
cardToAssign: {
customCardName: values.name,
isCustomCardNameEdited: true,
},
isEditing: false,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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<SettingsNavigatorParamList, typeof SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD_INVITE_NEW_MEMBER> &
Expand Down Expand Up @@ -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<AssignCardData> = {
email: assignCard?.cardToAssign?.invitingMemberEmail,
customCardName: defaultCardName,
customCardName,
invitingMemberEmail: '',
};

Expand All @@ -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({
Expand Down
6 changes: 6 additions & 0 deletions src/types/onyx/AssignCard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
36 changes: 36 additions & 0 deletions tests/unit/hooks/useAssignCard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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<Policy>({
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'}),
}),
);
});
});
});
Loading