Skip to content
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1090,6 +1090,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: 'Sprechen Sie mit Ihrer Kundenbetreuung',
forGuidedSetup: 'für die geführte Einrichtung.',
configureApprovalsSubText: 'Berichtsfreigaben festlegen',
setupTravel: 'Reisen einrichten',
setupTravelSubText: 'Reisespezifische Regeln konfigurieren',
},
freeTrialSection: {
title: ({days}: {days: number}) => `Kostenlose Testversion: Noch ${days} ${days === 1 ? 'Tag' : 'Tage'}!`,
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1153,6 +1153,8 @@ const translations = {
linkCompanyCardsSubText: 'Import expenses automatically',
issueExpensifyCards: 'Issue Expensify cards',
issueExpensifyCardsSubtitle: 'Customize controls and streamline spending',
setupTravel: 'Setup travel',
setupTravelSubText: 'Configure travel specific rules',
configureApprovals: 'Configure approval workflow',
configureApprovalsSubText: 'Define report approvals',
setupRules: 'Set up spend rules',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1060,6 +1060,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToConcierge: 'Habla con Concierge',
forGuidedSetup: 'para la configuración guiada.',
configureApprovalsSubText: 'Definir aprobaciones de informes',
setupTravel: 'Configurar viajes',
setupTravelSubText: 'Configura reglas específicas de viaje',
},
upcomingTravel: 'Próximos viajes',
upcomingTravelSection: {
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1106,6 +1106,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: 'Parlez à votre chargé de compte',
forGuidedSetup: 'pour la configuration guidée.',
configureApprovalsSubText: 'Définir les approbations de notes de frais',
setupTravel: 'Configurer les déplacements',
setupTravelSubText: 'Configurer des règles spécifiques aux déplacements',
},
yourSpend: {
title: 'Vos dépenses',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,6 +1104,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: 'Parla con il tuo account executive',
forGuidedSetup: 'per la configurazione guidata.',
configureApprovalsSubText: 'Definisci le approvazioni dei report',
setupTravel: 'Configura viaggi',
setupTravelSubText: 'Configura regole specifiche per i viaggi',
},
yourSpend: {
title: 'Le tue spese',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: 'アカウントエグゼクティブに相談する',
forGuidedSetup: 'ガイド付きセットアップ用です。',
configureApprovalsSubText: 'レポート承認を定義する',
setupTravel: '出張を設定',
setupTravelSubText: '出張用のルールを設定する',
},
yourSpend: {
title: 'あなたの支出',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: 'Praat met je accountmanager',
forGuidedSetup: 'voor begeleide installatie.',
configureApprovalsSubText: 'Definieer rapportgoedkeuringen',
setupTravel: 'Reizen instellen',
setupTravelSubText: 'Reisspecifieke regels instellen',
},
yourSpend: {
title: 'Je uitgaven',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: 'Porozmawiaj ze swoim opiekunem klienta',
forGuidedSetup: 'z prowadzeniem konfiguracji.',
configureApprovalsSubText: 'Zdefiniuj zatwierdzanie raportów',
setupTravel: 'Skonfiguruj podróże',
setupTravelSubText: 'Skonfiguruj zasady dotyczące podróży',
},
yourSpend: {
title: 'Twoje wydatki',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1102,6 +1102,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: 'Fale com seu executivo de contas',
forGuidedSetup: 'para configuração guiada.',
configureApprovalsSubText: 'Definir aprovações de relatórios',
setupTravel: 'Configurar viagem',
setupTravelSubText: 'Configurar regras específicas de viagem',
},
yourSpend: {
title: 'Seus gastos',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,8 @@ const translations: TranslationDeepObject<typeof en> = {
talkToAccountExecutive: '联系您的客户经理',
forGuidedSetup: '以获取引导式设置。',
configureApprovalsSubText: '定义报表审批',
setupTravel: '设置差旅',
setupTravelSubText: '配置差旅专用规则',
},
yourSpend: {title: '您的支出', awaitingApproval: '等待审批', repaidLast30Days: '过去30天内已偿还', recentTransactions: ({lastFour}: {lastFour: string}) => `最近交易 • ${lastFour}`},
seeMore: ({count}: {count: number}) => `再查看 ${count} 个`,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isPaidGroupPolicy,
isPendingDeletePolicy,
isPolicyAdmin,
isWorkspaceProvisionedForTravel,
} from '@libs/PolicyUtils';
import {generateReportID} from '@libs/ReportUtils';
import {isDeletedTransaction, isTransactionPendingDelete} from '@libs/TransactionUtils';
Expand Down Expand Up @@ -188,6 +189,16 @@ function useGettingStartedItems(): UseGettingStartedItemsResult {
});
}

if (policy.isTravelEnabled) {
items.push({
key: 'setupTravel',
label: translate('homePage.gettingStartedSection.setupTravel'),
subText: translate('homePage.gettingStartedSection.setupTravelSubText'),
isComplete: isWorkspaceProvisionedForTravel(policy.travelSettings),
Comment thread
blimpich marked this conversation as resolved.
route: ROUTES.WORKSPACE_TRAVEL.getRoute(activePolicyID),
});
}

const activeMemberCount = Object.values(policy.employeeList ?? {}).filter((member) => member?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length;
items.push({
key: 'inviteAccountant',
Expand Down Expand Up @@ -248,6 +259,16 @@ function useGettingStartedItems(): UseGettingStartedItemsResult {
});
}

if (policy.isTravelEnabled) {
items.push({
key: 'setupTravel',
label: translate('homePage.gettingStartedSection.setupTravel'),
subText: translate('homePage.gettingStartedSection.setupTravelSubText'),
isComplete: isWorkspaceProvisionedForTravel(policy.travelSettings),
route: ROUTES.WORKSPACE_TRAVEL.getRoute(activePolicyID),
});
}

if (policy.areWorkflowsEnabled) {
items.push({
key: 'configureApprovals',
Expand Down
129 changes: 129 additions & 0 deletions tests/unit/hooks/useGettingStartedItems.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,88 @@ describe('useGettingStartedItems', () => {
});
});

describe('row - Setup travel', () => {
it('should be shown when the travel feature is enabled', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {isTravelEnabled: true},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem).toBeDefined();
});

it('should not be shown when the travel feature is not enabled', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {isTravelEnabled: false},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem).toBeUndefined();
});

it('should navigate to the workspace travel route', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {isTravelEnabled: true},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem?.route).toBe(ROUTES.WORKSPACE_TRAVEL.getRoute(POLICY_ID));
});

it('should be not completed when the workspace has not been provisioned with Spotnana', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {isTravelEnabled: true, travelSettings: undefined},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem?.isComplete).toBe(false);
});

it('should be completed once the workspace is provisioned with a Spotnana company ID', async () => {
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {isTravelEnabled: true, travelSettings: {spotnanaCompanyID: 'spotnana-company-1'}},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem?.isComplete).toBe(true);
});

it('should be completed once the workspace is provisioned with an associated Spotnana travel domain account (no Spotnana company ID)', async () => {
// Real-world Spotnana entity-based provisioning populates associatedTravelDomainAccountID rather
// than spotnanaCompanyID, so isComplete must not rely on spotnanaCompanyID alone.
await setupManageTeamScenario({
accounting: CONST.POLICY.CONNECTIONS.NAME.QBO,
policy: {isTravelEnabled: true, travelSettings: {associatedTravelDomainAccountID: '12345', hasAcceptedTerms: true}},
});

const {result} = renderHook(() => useGettingStartedItems());
await waitForBatchedUpdates();

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem?.isComplete).toBe(true);
});
});

describe('row 4 - Set up spend rules', () => {
it('should be shown when areRulesEnabled is true', async () => {
await setupManageTeamScenario({
Expand Down Expand Up @@ -1495,6 +1577,53 @@ describe('useGettingStartedItems', () => {
});
});

describe('setup travel step', () => {
it('should insert setupTravel after linkCompanyCards when both travel and company cards are enabled', async () => {
await setupTrackWorkspaceScenario({policy: {isTravelEnabled: true, areCompanyCardsEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());

const keys = result.current.items.map((item) => item.key);
expect(keys).toEqual(['createWorkspace', 'customizeCategories', 'linkCompanyCards', 'setupTravel', 'inviteAccountant']);
});

it('should not show setupTravel when travel is not enabled', async () => {
await setupTrackWorkspaceScenario({policy: {isTravelEnabled: false}});

const {result} = renderHook(() => useGettingStartedItems());

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem).toBeUndefined();
});

it('should navigate to the workspace travel route', async () => {
await setupTrackWorkspaceScenario({policy: {isTravelEnabled: true}});

const {result} = renderHook(() => useGettingStartedItems());

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem?.route).toBe(ROUTES.WORKSPACE_TRAVEL.getRoute(POLICY_ID));
});

it('should be completed once the workspace is provisioned with a Spotnana company ID', async () => {
await setupTrackWorkspaceScenario({policy: {isTravelEnabled: true, travelSettings: {spotnanaCompanyID: 'spotnana-company-1'}}});

const {result} = renderHook(() => useGettingStartedItems());

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem?.isComplete).toBe(true);
});

it('should be completed once the workspace is provisioned with an associated Spotnana travel domain account (no Spotnana company ID)', async () => {
await setupTrackWorkspaceScenario({policy: {isTravelEnabled: true, travelSettings: {associatedTravelDomainAccountID: '12345', hasAcceptedTerms: true}}});

const {result} = renderHook(() => useGettingStartedItems());

const travelItem = result.current.items.find((item) => item.key === 'setupTravel');
expect(travelItem?.isComplete).toBe(true);
});
});

describe('feature toggles hide and show steps', () => {
it('should hide the customizeCategories step when Categories is disabled', async () => {
await setupTrackWorkspaceScenario({policy: {areCategoriesEnabled: false, areCompanyCardsEnabled: true}});
Expand Down
Loading