diff --git a/src/languages/de.ts b/src/languages/de.ts index d4ec90aaedad..23247348c2a2 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -1090,6 +1090,8 @@ const translations: TranslationDeepObject = { 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'}!`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 49231202c3c3..ffcce37345fe 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -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', diff --git a/src/languages/es.ts b/src/languages/es.ts index 6bbce90defb1..44908fd39356 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1060,6 +1060,8 @@ const translations: TranslationDeepObject = { 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: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 8835afd1f80e..b8b8f8d4edfc 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -1106,6 +1106,8 @@ const translations: TranslationDeepObject = { 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', diff --git a/src/languages/it.ts b/src/languages/it.ts index 66553799472d..98e8924324ef 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -1104,6 +1104,8 @@ const translations: TranslationDeepObject = { 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', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d7b657fe9246..555f1eaeb75f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -1086,6 +1086,8 @@ const translations: TranslationDeepObject = { talkToAccountExecutive: 'アカウントエグゼクティブに相談する', forGuidedSetup: 'ガイド付きセットアップ用です。', configureApprovalsSubText: 'レポート承認を定義する', + setupTravel: '出張を設定', + setupTravelSubText: '出張用のルールを設定する', }, yourSpend: { title: 'あなたの支出', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 9f1296a245c0..e8d5cc944f6c 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -1102,6 +1102,8 @@ const translations: TranslationDeepObject = { talkToAccountExecutive: 'Praat met je accountmanager', forGuidedSetup: 'voor begeleide installatie.', configureApprovalsSubText: 'Definieer rapportgoedkeuringen', + setupTravel: 'Reizen instellen', + setupTravelSubText: 'Reisspecifieke regels instellen', }, yourSpend: { title: 'Je uitgaven', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d59cdf1a66d8..621935c31c89 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -1100,6 +1100,8 @@ const translations: TranslationDeepObject = { 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', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index b718cf601e4f..60f6f6a37005 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -1102,6 +1102,8 @@ const translations: TranslationDeepObject = { 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', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 4f86c8a31bf4..09397513788a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -1069,6 +1069,8 @@ const translations: TranslationDeepObject = { talkToAccountExecutive: '联系您的客户经理', forGuidedSetup: '以获取引导式设置。', configureApprovalsSubText: '定义报表审批', + setupTravel: '设置差旅', + setupTravelSubText: '配置差旅专用规则', }, yourSpend: {title: '您的支出', awaitingApproval: '等待审批', repaidLast30Days: '过去30天内已偿还', recentTransactions: ({lastFour}: {lastFour: string}) => `最近交易 • ${lastFour}`}, seeMore: ({count}: {count: number}) => `再查看 ${count} 个`, diff --git a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts index e99f0fa59ac3..36fae51dc27d 100644 --- a/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts +++ b/src/pages/home/GettingStartedSection/hooks/useGettingStartedItems.ts @@ -20,6 +20,7 @@ import { isPaidGroupPolicy, isPendingDeletePolicy, isPolicyAdmin, + isWorkspaceProvisionedForTravel, } from '@libs/PolicyUtils'; import {generateReportID} from '@libs/ReportUtils'; import {isDeletedTransaction, isTransactionPendingDelete} from '@libs/TransactionUtils'; @@ -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), + 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', @@ -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', diff --git a/tests/unit/hooks/useGettingStartedItems.test.ts b/tests/unit/hooks/useGettingStartedItems.test.ts index 6fc2637c26c2..34ae044f6326 100644 --- a/tests/unit/hooks/useGettingStartedItems.test.ts +++ b/tests/unit/hooks/useGettingStartedItems.test.ts @@ -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({ @@ -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}});