From 7361efaf9338a147bd6ca84888df0dbcc50f9643 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 5 Sep 2025 15:30:39 +0200 Subject: [PATCH 01/16] feat: add translation for reports --- src/languages/de.ts | 5 +++++ src/languages/en.ts | 5 +++++ src/languages/es.ts | 5 +++++ src/languages/fr.ts | 5 +++++ src/languages/it.ts | 5 +++++ src/languages/ja.ts | 5 +++++ src/languages/nl.ts | 5 +++++ src/languages/pl.ts | 5 +++++ src/languages/pt-BR.ts | 5 +++++ src/languages/zh-hans.ts | 5 +++++ 10 files changed, 50 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 79493b3cb53e..2517809e3172 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5546,6 +5546,11 @@ const translations = { 'Expensify Travel ist eine neue Plattform für die Buchung und Verwaltung von Geschäftsreisen, die es Mitgliedern ermöglicht, Unterkünfte, Flüge, Transportmittel und mehr zu buchen.', onlyAvailableOnPlan: 'Reisen ist im Collect-Plan verfügbar, beginnend bei', }, + reports: { + title: 'Berichte', + description: 'Erstellen Sie organisierte Spesenabrechnungen, um Ihre Geschäftsausgaben zu verfolgen, zur Genehmigung einzureichen und Ihren Erstattungsprozess zu optimieren.', + onlyAvailableOnPlan: 'Berichte sind im Collect-Plan verfügbar, beginnend bei ', + }, multiLevelTags: { title: 'Mehrstufige Tags', description: diff --git a/src/languages/en.ts b/src/languages/en.ts index db9ea387b69b..06b67e60c354 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5523,6 +5523,11 @@ const translations = { description: 'Expensify Travel is a new corporate travel booking and management platform that allows members to book accommodations, flights, transportation, and more.', onlyAvailableOnPlan: 'Travel is available on the Collect plan, starting at ', }, + reports: { + title: 'Reports', + description: 'Create organized expense reports to track your business spending, submit for approvals, and streamline your reimbursement process.', + onlyAvailableOnPlan: 'Reports are available on the Collect plan, starting at ', + }, multiLevelTags: { title: 'Multi-level tags', description: diff --git a/src/languages/es.ts b/src/languages/es.ts index 74917854d705..62e798a921b7 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5556,6 +5556,11 @@ const translations = { 'Expensify Travel es una nueva plataforma corporativa de reserva y gestión de viajes que permite a los miembros reservar alojamientos, vuelos, transporte y mucho más.', onlyAvailableOnPlan: 'Los viajes están disponibles en el plan Recopilar, a partir de ', }, + reports: { + title: 'Informes', + description: 'Crea informes de gastos organizados para hacer seguimiento de tus gastos comerciales, enviarlos para aprobación y optimizar tu proceso de reembolso.', + onlyAvailableOnPlan: 'Los informes están disponibles en el plan Recopilar, a partir de ', + }, multiLevelTags: { title: 'Etiquetas multinivel', description: diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c6f9a5beec9a..1f988c0532c0 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5561,6 +5561,11 @@ const translations = { "Expensify Travel est une nouvelle plateforme de réservation et de gestion de voyages d'affaires qui permet aux membres de réserver des hébergements, des vols, des transports, et plus encore.", onlyAvailableOnPlan: 'Le voyage est disponible sur le plan Collect, à partir de', }, + reports: { + title: 'Rapports', + description: 'Créez des rapports de dépenses organisés pour suivre vos dépenses professionnelles, les soumettre pour approbation et rationaliser votre processus de remboursement.', + onlyAvailableOnPlan: 'Les rapports sont disponibles sur le plan Collect, à partir de ', + }, multiLevelTags: { title: 'Tags multi-niveaux', description: diff --git a/src/languages/it.ts b/src/languages/it.ts index 9f921013a49a..a7051cf70e4e 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5558,6 +5558,11 @@ const translations = { 'Expensify Travel è una nuova piattaforma aziendale per la prenotazione e la gestione dei viaggi che consente ai membri di prenotare alloggi, voli, trasporti e altro.', onlyAvailableOnPlan: 'Il viaggio è disponibile nel piano Collect, a partire da', }, + reports: { + title: 'Report', + description: 'Crea report spese organizzati per tenere traccia delle tue spese aziendali, inviarli per approvazione e semplificare il processo di rimborso.', + onlyAvailableOnPlan: 'I report sono disponibili nel piano Collect, a partire da ', + }, multiLevelTags: { title: 'Tag multi-livello', description: diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 780db157863d..c9efec4792c8 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5528,6 +5528,11 @@ const translations = { description: 'Expensify Travelは、メンバーが宿泊施設、フライト、交通機関などを予約できる新しい法人向け旅行予約および管理プラットフォームです。', onlyAvailableOnPlan: '旅行は、Collectプランで利用可能です。料金は', }, + reports: { + title: 'レポート', + description: '組織化された経費レポートを作成して、ビジネス支出を追跡し、承認のために提出し、払い戻しプロセスを合理化します。', + onlyAvailableOnPlan: 'レポートは、Collectプランで利用可能です。料金は ', + }, multiLevelTags: { title: 'マルチレベルタグ', description: diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 23cb26aa126b..76f17eca3012 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5556,6 +5556,11 @@ const translations = { description: 'Expensify Travel is een nieuw platform voor het boeken en beheren van zakelijke reizen waarmee leden accommodaties, vluchten, vervoer en meer kunnen boeken.', onlyAvailableOnPlan: 'Reizen is beschikbaar op het Collect-plan, beginnend bij', }, + reports: { + title: 'Rapporten', + description: 'Maak georganiseerde onkostenrapporten om uw zakelijke uitgaven bij te houden, in te dienen voor goedkeuring en uw vergoedingsproces te stroomlijnen.', + onlyAvailableOnPlan: 'Rapporten zijn beschikbaar op het Collect-plan, beginnend bij ', + }, multiLevelTags: { title: 'Meerniveautags', description: diff --git a/src/languages/pl.ts b/src/languages/pl.ts index fc0a20dc4abf..4740f34da98b 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5544,6 +5544,11 @@ const translations = { 'Expensify Travel to nowa platforma do rezerwacji i zarządzania podróżami służbowymi, która umożliwia członkom rezerwację zakwaterowania, lotów, transportu i nie tylko.', onlyAvailableOnPlan: 'Podróże są dostępne w planie Collect, zaczynając od', }, + reports: { + title: 'Raporty', + description: 'Twórz uporządkowane raporty wydatków, aby śledzić swoje wydatki biznesowe, przesyłać je do zatwierdzenia i usprawniać proces zwrotu kosztów.', + onlyAvailableOnPlan: 'Raporty są dostępne w planie Collect, zaczynając od ', + }, multiLevelTags: { title: 'Wielopoziomowe tagi', description: diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0627333d2add..452efbf392c3 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5555,6 +5555,11 @@ const translations = { description: 'Expensify Travel é uma nova plataforma de reserva e gestão de viagens corporativas que permite aos membros reservar acomodações, voos, transporte e mais.', onlyAvailableOnPlan: 'Viagens estão disponíveis no plano Collect, a partir de', }, + reports: { + title: 'Relatórios', + description: 'Crie relatórios de despesas organizados para acompanhar seus gastos empresariais, enviá-los para aprovação e otimizar seu processo de reembolso.', + onlyAvailableOnPlan: 'Os relatórios estão disponíveis no plano Collect, a partir de ', + }, multiLevelTags: { title: 'Tags multiníveis', description: diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 2f9c9c7aeecc..d3e38bfef6fd 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5460,6 +5460,11 @@ const translations = { description: 'Expensify Travel 是一个新的企业差旅预订和管理平台,允许会员预订住宿、航班、交通等。', onlyAvailableOnPlan: '旅行功能在 Collect 计划中提供,起价为', }, + reports: { + title: '报告', + description: '创建有序的费用报告来跟踪您的商业开支,提交审批,并简化您的报销流程。', + onlyAvailableOnPlan: '报告功能在 Collect 计划中提供,起价为 ', + }, multiLevelTags: { title: '多级标签', description: '多级标签帮助您更精确地跟踪费用。为每个项目分配多个标签,例如部门、客户或成本中心,以捕获每笔费用的完整上下文。这使得更详细的报告、审批流程和会计导出成为可能。', From b44877cb8826fc46297ef715ab1b550268427d57 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 5 Sep 2025 16:07:34 +0200 Subject: [PATCH 02/16] feat: add consts, enable editing report field --- src/CONST/index.ts | 8 ++++++++ .../ReportActionItem/MoneyRequestView.tsx | 9 ++++++++- src/libs/ReportUtils.ts | 16 ++++++++-------- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index c9a770a6372d..636ff9cb1893 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -6900,6 +6900,14 @@ const CONST = { description: 'workspace.upgrade.travel.description' as const, icon: 'Luggage', }, + reports: { + id: 'reports' as const, + alias: 'reports', + name: 'Reports', + title: 'workspace.upgrade.reports.title' as const, + description: 'workspace.upgrade.reports.description' as const, + icon: 'ReportReceipt', + }, }; }, REPORT_FIELD_TYPES: { diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index d31161071164..61853d140ac8 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -811,7 +811,7 @@ function MoneyRequestView({ - getOutstandingReportsForUser(currentPolicy?.id, currentUserAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), - ).length > 0 - : Object.values(allPolicies ?? {}).flatMap((currentPolicy) => + return Object.values(allPolicies ?? {}).flatMap((currentPolicy) => getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), ).length > 1 || (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)); From 34dfbd8398969fe57ea7d752d2f86ffcea0ce323 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 10 Sep 2025 17:29:16 +0200 Subject: [PATCH 03/16] feat: navigate to upgrade route --- src/ROUTES.ts | 40 ++++++++++++++++++- .../ReportActionItem/MoneyRequestView.tsx | 3 +- src/libs/Navigation/types.ts | 1 + 3 files changed, 40 insertions(+), 4 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 850378695784..cb583c5e57ee 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -768,9 +768,45 @@ const ROUTES = { }, MONEY_REQUEST_UPGRADE: { route: ':action/:iouType/upgrade/:transactionID/:reportID', - getRoute: (action: IOUAction, iouType: IOUType, transactionID: string, reportID: string, backTo = '') => + getRoute: (params: { + action: IOUAction; + iouType: IOUType; + transactionID: string; + reportID: string; + backTo?: string; + isCategorizing?: boolean; + isReporting?: boolean; + shouldSubmitExpense?: boolean; + }) => { + const {action, iouType, transactionID, reportID, backTo = '', isCategorizing = false, isReporting = false, shouldSubmitExpense = false} = params; + + const baseURL = `${action as string}/${iouType as string}/upgrade/${transactionID}/${reportID}` as const; + + const queryParams: Record = {}; + if (isCategorizing) { + queryParams.isCategorizing = 'true'; + } + if (isReporting) { + queryParams.isReporting = 'true'; + } + if (shouldSubmitExpense) { + queryParams.shouldSubmitExpense = 'true'; + } + + const queryString = + Object.keys(queryParams).length > 0 + ? Object.entries(queryParams) + .map(([key, value]) => `${key}=${value}`) + .join('&') + : ''; + + if (queryString) { + // eslint-disable-next-line no-restricted-syntax -- Legacy route generation + return getUrlWithBackToParam(`${baseURL}?${queryString}` as const, backTo); + } // eslint-disable-next-line no-restricted-syntax -- Legacy route generation - getUrlWithBackToParam(`${action as string}/${iouType as string}/upgrade/${transactionID}/${reportID}`, backTo), + return getUrlWithBackToParam(baseURL, backTo); + }, }, MONEY_REQUEST_STEP_DESTINATION: { route: ':action/:iouType/destination/:transactionID/:reportID', diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 953e62aca71e..57cc2ed3525a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -829,8 +829,7 @@ function MoneyRequestView({ } if (!policy) { - // TODO: fix it later - Navigation.navigate(ROUTES.MONEY_REQUEST_UPGRADE.getRoute()); + Navigation.navigate(ROUTES.MONEY_REQUEST_UPGRADE.getRoute({iouType, action: CONST.IOU.ACTION.EDIT, transactionID: transaction?.transactionID, reportID: report.reportID, isReporting: true})); return; } diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 913a798fd0a8..0dcf4c60f3c9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1499,6 +1499,7 @@ type MoneyRequestNavigatorParamList = { transactionID: string; reportID: string; backTo: Routes; + isReporting?: boolean; }; [SCREENS.MONEY_REQUEST.STEP_DESTINATION]: { action: IOUAction; From 1edc9adf7055eee078cd0d9abb29a10ccf8a23a8 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Sep 2025 11:05:25 +0200 Subject: [PATCH 04/16] feat: report upgrade path --- src/components/WorkspaceConfirmationForm.tsx | 7 +- src/libs/Navigation/types.ts | 5 + .../request/step/IOURequestStepUpgrade.tsx | 111 ++++++++++++------ .../workspace/upgrade/UpgradeConfirmation.tsx | 9 +- src/pages/workspace/upgrade/UpgradeIntro.tsx | 7 +- 5 files changed, 95 insertions(+), 44 deletions(-) diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx index 31a933b66fde..69e9d4d4a9ba 100644 --- a/src/components/WorkspaceConfirmationForm.tsx +++ b/src/components/WorkspaceConfirmationForm.tsx @@ -44,11 +44,14 @@ type WorkspaceConfirmationFormProps = { /** Submit function */ onSubmit: (params: WorkspaceConfirmationSubmitFunctionParams) => void; - /** go back function */ + /** Go back function */ onBackButtonPress?: () => void; + + /** Whether bottom safe area padding should be added */ + addBottomSafeAreaPadding?: boolean; }; -function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButtonPress = () => Navigation.goBack()}: WorkspaceConfirmationFormProps) { +function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButtonPress = () => Navigation.goBack(), addBottomSafeAreaPadding = true}: WorkspaceConfirmationFormProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0dcf4c60f3c9..14780dd4233a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1499,7 +1499,12 @@ type MoneyRequestNavigatorParamList = { transactionID: string; reportID: string; backTo: Routes; + /** Whether is categorizing the expense */ + isCategorizing?: boolean; + /** Whether is adding an unreported expense to a report */ isReporting?: boolean; + /** Whether the user starts to categorize expense using Categorize it whisper action */ + shouldSubmitExpense?: boolean; }; [SCREENS.MONEY_REQUEST.STEP_DESTINATION]: { action: IOUAction; diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index 5dd0e28c6412..a5dfb6616887 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -2,8 +2,11 @@ import React, {useRef, useState} from 'react'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; +import WorkspaceConfirmationForm from '@components/WorkspaceConfirmationForm'; +import type {WorkspaceConfirmationSubmitFunctionParams} from '@components/WorkspaceConfirmationForm'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import type CreateWorkspaceParams from '@libs/API/parameters/CreateWorkspaceParams'; import Navigation from '@libs/Navigation/Navigation'; @@ -14,6 +17,7 @@ import UpgradeIntro from '@pages/workspace/upgrade/UpgradeIntro'; import {setMoneyRequestParticipants} from '@userActions/IOU'; import CONST from '@src/CONST'; import * as Policy from '@src/libs/actions/Policy/Policy'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -21,67 +25,100 @@ type IOURequestStepUpgradeProps = PlatformStackScreenProps(null); + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + + const onWorkspaceConfirmationSubmit = (params: WorkspaceConfirmationSubmitFunctionParams) => { + const policyData = Policy.createWorkspace({ + policyOwnerEmail: '', + makeMeAdmin: false, + policyName: params.name, + policyID: params.policyID, + currency: params.currency, + engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, + }); + policyDataRef.current = policyData; + setCreatedPolicyName(params.name); + setShowConfirmationForm(false); + setIsUpgraded(true); + }; + + // TODO: remove this after all the changes are applied + // eslint-disable-next-line rulesdir/prefer-early-return + const onConfirmUpgrade = () => { + if (isCategorizing) { + if (shouldSubmitExpense) { + setMoneyRequestParticipants(transactionID, [ + { + selected: true, + accountID: 0, + isPolicyExpenseChat: true, + reportID: policyDataRef.current?.expenseChatReportID, + policyID: policyDataRef.current?.policyID, + searchText: policyDataRef.current?.policyName, + }, + ]); + Navigation.goBack(); + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, policyDataRef.current?.expenseChatReportID)); + } else { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID)); + } + } + if (isReporting) { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_REPORT.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, reportID)); + } + }; + return ( - { - Navigation.goBack(); - }} - /> + {(!!isUpgraded || !showConfirmationForm) && ( + Navigation.goBack()} + /> + )} {!!isUpgraded && ( { - setMoneyRequestParticipants(transactionID, [ - { - selected: true, - accountID: 0, - isPolicyExpenseChat: true, - reportID: policyDataRef.current?.expenseChatReportID, - policyID: policyDataRef.current?.policyID, - searchText: policyDataRef.current?.policyName, - }, - ]); - Navigation.goBack(); - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_CATEGORY.getRoute(action, CONST.IOU.TYPE.SUBMIT, transactionID, policyDataRef.current?.expenseChatReportID)); - }} - policyName="" - isCategorizing + onConfirmUpgrade={onConfirmUpgrade} + policyName={createdPolicyName} + isCategorizing={isCategorizing} + isReporting={isReporting} /> )} - {!isUpgraded && ( + {!isUpgraded && !showConfirmationForm && ( { - const policyData = Policy.createWorkspace({ - policyOwnerEmail: '', - makeMeAdmin: false, - policyName: '', - policyID: undefined, - engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, - }); - setIsUpgraded(true); - policyDataRef.current = policyData; - }} + onUpgrade={() => setShowConfirmationForm(true)} buttonDisabled={isOffline} loading={false} - isCategorizing + isCategorizing={isCategorizing} + isReporting={isReporting} + /> + )} + {!isUpgraded && showConfirmationForm && ( + setShowConfirmationForm(false)} + addBottomSafeAreaPadding={false} /> )} diff --git a/src/pages/workspace/upgrade/UpgradeConfirmation.tsx b/src/pages/workspace/upgrade/UpgradeConfirmation.tsx index fc4f1569d24d..c718ffa13548 100644 --- a/src/pages/workspace/upgrade/UpgradeConfirmation.tsx +++ b/src/pages/workspace/upgrade/UpgradeConfirmation.tsx @@ -12,11 +12,14 @@ import ROUTES from '@src/ROUTES'; type Props = { policyName: string; onConfirmUpgrade: () => void; + /** Whether is categorizing the expense */ isCategorizing?: boolean; + /** Whether is adding an unreported expense to a report */ + isReporting?: boolean; isTravelUpgrade?: boolean; }; -function UpgradeConfirmation({policyName, onConfirmUpgrade, isCategorizing, isTravelUpgrade}: Props) { +function UpgradeConfirmation({policyName, onConfirmUpgrade, isCategorizing, isReporting, isTravelUpgrade}: Props) { const {translate} = useLocalize(); const styles = useThemeStyles(); const {environmentURL} = useEnvironment(); @@ -32,7 +35,7 @@ function UpgradeConfirmation({policyName, onConfirmUpgrade, isCategorizing, isTr }, [updateSubscriptionLink]); const description = useMemo(() => { - if (isCategorizing) { + if (isCategorizing ?? isReporting) { return {translate('workspace.upgrade.completed.categorizeMessage')}; } @@ -45,7 +48,7 @@ function UpgradeConfirmation({policyName, onConfirmUpgrade, isCategorizing, isTr ); - }, [isCategorizing, isTravelUpgrade, policyName, styles.renderHTML, styles.textAlignCenter, styles.w100, translate, subscriptionLink]); + }, [isCategorizing, isReporting, isTravelUpgrade, policyName, styles.renderHTML, styles.textAlignCenter, styles.w100, translate, subscriptionLink]); return ( >; onUpgrade: () => void; + /** Whether is categorizing the expense */ isCategorizing?: boolean; + /** Whether is adding an unreported expense to a report */ + isReporting?: boolean; policyID?: string; backTo?: Route; }; -function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, policyID, backTo}: Props) { +function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizing, isReporting, policyID, backTo}: Props) { const styles = useThemeStyles(); const {isExtraSmallScreenWidth} = useResponsiveLayout(); const {translate} = useLocalize(); @@ -65,7 +68,7 @@ function UpgradeIntro({feature, onUpgrade, buttonDisabled, loading, isCategorizi * The "isCategorizing" flag is set to true when the user accesses the "Categorize" option in the Self-DM whisper. * In such scenarios, a separate Categories upgrade UI is displayed. */ - if (!feature || (!isCategorizing && !policyID)) { + if (!feature || (!isCategorizing && !isReporting && !policyID)) { return ( Date: Thu, 11 Sep 2025 11:23:34 +0200 Subject: [PATCH 05/16] feat: create report button --- .../step/IOURequestEditReportCommon.tsx | 28 +++++++++++++++++-- .../iou/request/step/IOURequestStepReport.tsx | 11 +++++++- 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index f9bde935b1df..8faaea8059c4 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -37,6 +37,7 @@ type Props = { isEditing?: boolean; isUnreported?: boolean; shouldShowNotFoundPage?: boolean; + createReport?: () => void; }; function IOURequestEditReportCommon({ @@ -49,11 +50,14 @@ function IOURequestEditReportCommon({ isEditing = false, isUnreported, shouldShowNotFoundPage: shouldShowNotFoundPageFromProps, + createReport, }: Props) { const {translate, localeCompare} = useLocalize(); const {options} = useOptionsList(); const [outstandingReportsByPolicyID] = useOnyx(ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID, {canBeMissing: true}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); + const policy = usePolicy(activePolicyID); const reportOwnerAccountID = useMemo( () => transactionsReports.at(0)?.ownerAccountID ?? currentUserPersonalDetails.accountID, [transactionsReports, currentUserPersonalDetails.accountID], @@ -136,8 +140,27 @@ function IOURequestEditReportCommon({ const headerMessage = useMemo(() => (searchValue && !reportOptions.length ? translate('common.noResultsFound') : ''), [searchValue, reportOptions, translate]); + const createReportOption = useMemo(() => { + if (!createReport) { + return undefined; + } + + return ( + + ); + }, [createReport, translate, policy]); + // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo(() => { + if (createReport) { + return false; + } + if (expenseReports.length === 0 || shouldShowNotFoundPageFromProps) { return true; } @@ -152,7 +175,7 @@ function IOURequestEditReportCommon({ const isSubmitter = isReportOwner(transactionReport); // If the report is Open, then only submitters, admins can move expenses return isOpen && !isAdmin && !isSubmitter; - }, [transactionsReports, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps]); + }, [transactionsReports, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps, createReport]); return ( - ) : undefined + ) : createReportOption } + listEmptyContent={createReportOption} /> ); diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 033cdc34953d..4ae520f05c98 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -3,8 +3,10 @@ import {InteractionManager} from 'react-native'; import {useSession} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; +import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import Navigation from '@libs/Navigation/Navigation'; @@ -34,7 +36,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const [transactionReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(reportID)}`, {canBeMissing: false}); const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const {removeTransaction} = useSearchContext(); - + const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const isEditing = action === CONST.IOU.ACTION.EDIT; const isCreateReport = action === CONST.IOU.ACTION.CREATE; const isFromGlobalCreate = !!transaction?.isFromGlobalCreate; @@ -42,6 +44,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const [allBetas] = useOnyx(ONYXKEYS.BETAS, {canBeMissing: true}); const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const handleGoBack = () => { if (isEditing) { @@ -155,6 +158,11 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useShowNotFoundPageInIOUStep(action, iouType, reportActionID, reportOrDraftReport, transaction); + const createReport = () => { + const createdReportID = createNewReport(currentUserPersonalDetails, activePolicyID); + handleRegularReportSelection({value: createdReportID}); + }; + return ( ); } From 067688281723798da0e6e85b5d85ddc5babd32c6 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 11 Sep 2025 11:26:43 +0200 Subject: [PATCH 06/16] fix: show create report when list is not empty --- .../step/IOURequestEditReportCommon.tsx | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index 8faaea8059c4..f64fe0ee718f 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -197,14 +197,17 @@ function IOURequestEditReportCommon({ initiallyFocusedOptionKey={transactionsReports.length === 1 ? transactionsReports.at(0)?.reportID : undefined} ListItem={InviteMemberListItem} listFooterContent={ - shouldShowRemoveFromReport ? ( - - ) : createReportOption + <> + {shouldShowRemoveFromReport && ( + + )} + {createReportOption} + } listEmptyContent={createReportOption} /> From 7e831a87d5035eaff65e8bbf060862c2a0b67e3b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Sep 2025 13:52:28 +0200 Subject: [PATCH 07/16] fix: minor changes, prettier --- src/components/ReportActionItem/MoneyRequestView.tsx | 10 +++++++++- src/languages/de.ts | 3 ++- src/languages/fr.ts | 3 ++- src/libs/ReportUtils.ts | 10 ++++++---- .../iou/request/step/IOURequestEditReportCommon.tsx | 6 +----- 5 files changed, 20 insertions(+), 12 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 4a91a6c6818f..08cecbe6e47a 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -837,7 +837,15 @@ function MoneyRequestView({ } if (!policy) { - Navigation.navigate(ROUTES.MONEY_REQUEST_UPGRADE.getRoute({iouType, action: CONST.IOU.ACTION.EDIT, transactionID: transaction?.transactionID, reportID: report.reportID, isReporting: true})); + Navigation.navigate( + ROUTES.MONEY_REQUEST_UPGRADE.getRoute({ + iouType, + action: CONST.IOU.ACTION.EDIT, + transactionID: transaction?.transactionID, + reportID: report.reportID, + isReporting: true, + }), + ); return; } diff --git a/src/languages/de.ts b/src/languages/de.ts index d4e8b6c47515..36f0e692b171 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5553,7 +5553,8 @@ const translations = { }, reports: { title: 'Berichte', - description: 'Erstellen Sie organisierte Spesenabrechnungen, um Ihre Geschäftsausgaben zu verfolgen, zur Genehmigung einzureichen und Ihren Erstattungsprozess zu optimieren.', + description: + 'Erstellen Sie organisierte Spesenabrechnungen, um Ihre Geschäftsausgaben zu verfolgen, zur Genehmigung einzureichen und Ihren Erstattungsprozess zu optimieren.', onlyAvailableOnPlan: 'Berichte sind im Collect-Plan verfügbar, beginnend bei ', }, multiLevelTags: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 2a93967a4449..a4fd2274f6d6 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5564,7 +5564,8 @@ const translations = { }, reports: { title: 'Rapports', - description: 'Créez des rapports de dépenses organisés pour suivre vos dépenses professionnelles, les soumettre pour approbation et rationaliser votre processus de remboursement.', + description: + 'Créez des rapports de dépenses organisés pour suivre vos dépenses professionnelles, les soumettre pour approbation et rationaliser votre processus de remboursement.', onlyAvailableOnPlan: 'Les rapports sont disponibles sur le plan Collect, à partir de ', }, multiLevelTags: { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7d98cf5d5c4e..4c1ba73b5250 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4398,10 +4398,12 @@ function canEditFieldOfMoneyRequest( return false; } - return Object.values(allPolicies ?? {}).flatMap((currentPolicy) => - getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), - ).length > 1 || - (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)); + return ( + Object.values(allPolicies ?? {}).flatMap((currentPolicy) => + getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), + ).length > 1 || + (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)) + ); } return true; diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index d0330173cada..fb08ac5ba15b 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -60,10 +60,6 @@ function IOURequestEditReportCommon({ const reportPolicy = usePolicy(selectedReport?.policyID); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const policy = usePolicy(activePolicyID); - const reportOwnerAccountID = useMemo( - () => transactionsReports.at(0)?.ownerAccountID ?? currentUserPersonalDetails.accountID, - [transactionsReports, currentUserPersonalDetails.accountID], - ); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policy) => policy?.id), canBeMissing: false}); @@ -181,7 +177,7 @@ function IOURequestEditReportCommon({ const isSubmitter = isReportOwner(selectedReport); // If the report is Open, then only submitters, admins can move expenses return isOpen && !isAdmin && !isSubmitter; - }, [createReport, selectedReport, transactionsReports, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps]); + }, [createReport, selectedReport, reportPolicy, expenseReports.length, shouldShowNotFoundPageFromProps]); return ( Date: Tue, 16 Sep 2025 14:11:25 +0200 Subject: [PATCH 08/16] fix: avatar, block the feature --- src/libs/ReportUtils.ts | 38 ++++++++++++++----- .../request/step/IOURequestStepUpgrade.tsx | 1 + 2 files changed, 30 insertions(+), 9 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4c1ba73b5250..55fd9b30048b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1214,6 +1214,7 @@ function isDraftReport(reportID: string | undefined): boolean { return !!draftReport; } + /** * @private */ @@ -1407,6 +1408,7 @@ function isIOUReport(reportOrID: OnyxInputOrEntry | SearchReport | strin function isIOUReportUsingReport(report: OnyxEntry): report is Report { return report?.type === CONST.REPORT.TYPE.IOU; } + /** * Checks if a report is a task report. */ @@ -2120,6 +2122,7 @@ function isArchivedNonExpenseReportWithID(report?: OnyxInputOrEntry, isR function isClosedReport(report: OnyxInputOrEntry | SearchReport): boolean { return report?.statusNum === CONST.REPORT.STATUS_NUM.CLOSED; } + /** * Whether the provided report is the admin's room */ @@ -2369,6 +2372,7 @@ function isPayAtEndExpenseReport(report: OnyxEntry, transactions: Transa return isPayAtEndExpense(transactions?.[0] ?? getReportTransactions(report?.reportID).at(0)); } + /** * Checks if a report is a transaction thread associated with a report that has only one transaction */ @@ -4367,7 +4371,8 @@ function canEditFieldOfMoneyRequest( // Unreported transaction from OldDot can have the reportID as an empty string const isUnreportedExpense = !transaction?.reportID || transaction?.reportID === CONST.REPORT.UNREPORTED_REPORT_ID; - if (isUnreportedExpense) { + // TODO: remove permission check after the Unreported Expense project is complete + if (isUnreportedExpense && Permissions.canUseUnreportedExpense()) { return true; } @@ -4381,7 +4386,9 @@ function canEditFieldOfMoneyRequest( } const isOwner = moneyRequestReport?.ownerAccountID === currentUserAccountID; - if (isInvoiceReport(moneyRequestReport)) { + // TODO: uncomment after the Unreported Expense project is complete + // if (isInvoiceReport(moneyRequestReport)) { + if (isInvoiceReport(moneyRequestReport) && !isUnreportedExpense) { return ( getOutstandingReportsForUser( moneyRequestReport?.policyID, @@ -4394,16 +4401,28 @@ function canEditFieldOfMoneyRequest( // If the report is Open, then only submitters, admins can move expenses const isOpen = isOpenExpenseReport(moneyRequestReport); - if (isOpen && !isSubmitter && !isAdmin) { + // TODO: uncomment after the Unreported Expense project is complete + // if (isOpen && !isSubmitter && !isAdmin) { + if (!isUnreportedExpense && isOpen && !isSubmitter && !isAdmin) { return false; } - return ( - Object.values(allPolicies ?? {}).flatMap((currentPolicy) => - getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), - ).length > 1 || - (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)) - ); + // TODO: uncomment after the Unreported Expense project is complete + // return ( + // Object.values(allPolicies ?? {}).flatMap((currentPolicy) => + // getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), + // ).length > 1 || + // (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)) + // ); + + return isUnreportedExpense + ? Object.values(allPolicies ?? {}).flatMap((currentPolicy) => + getOutstandingReportsForUser(currentPolicy?.id, currentUserAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), + ).length > 0 + : Object.values(allPolicies ?? {}).flatMap((currentPolicy) => + getOutstandingReportsForUser(currentPolicy?.id, moneyRequestReport?.ownerAccountID, outstandingReportsByPolicyID?.[currentPolicy?.id ?? CONST.DEFAULT_NUMBER_ID] ?? {}), + ).length > 1 || + (isOwner && isReportOutstanding(moneyRequestReport, moneyRequestReport.policyID)); } return true; @@ -11495,6 +11514,7 @@ function findReportIDForAction(action?: ReportAction): string | undefined { }) ?.replace(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}`, ''); } + function canRejectReportAction(report: Report, policy?: Policy): boolean { if (!Permissions.isBetaEnabled(CONST.BETAS.NEWDOT_REJECT, allBetas)) { return false; diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index a5dfb6616887..367f891cd54b 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -48,6 +48,7 @@ function IOURequestStepUpgrade({ policyName: params.name, policyID: params.policyID, currency: params.currency, + file: params.avatarFile, engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, }); policyDataRef.current = policyData; From 963a05694a061d984cd5245e8a4977686898a8a1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Sep 2025 14:13:52 +0200 Subject: [PATCH 09/16] fix: eslint --- src/pages/iou/request/step/IOURequestEditReportCommon.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index fb08ac5ba15b..7e2347aefa1d 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -61,7 +61,7 @@ function IOURequestEditReportCommon({ const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); const policy = usePolicy(activePolicyID); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); - const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policy) => policy?.id), canBeMissing: false}); + const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policyItem) => policyItem?.id), canBeMissing: false}); const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); const isOwner = selectedReport ? selectedReport.ownerAccountID === currentUserPersonalDetails.accountID : false; From 79abb1b7b2e05f4c60bb623967b4b0ee400e32e1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Sep 2025 14:15:24 +0200 Subject: [PATCH 10/16] fix: eslint --- src/components/WorkspaceConfirmationForm.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/WorkspaceConfirmationForm.tsx b/src/components/WorkspaceConfirmationForm.tsx index 69e9d4d4a9ba..53da5bf9eacf 100644 --- a/src/components/WorkspaceConfirmationForm.tsx +++ b/src/components/WorkspaceConfirmationForm.tsx @@ -155,7 +155,7 @@ function WorkspaceConfirmationForm({onSubmit, policyOwnerEmail = '', onBackButto }) } enabledWhenOffline - addBottomSafeAreaPadding + addBottomSafeAreaPadding={addBottomSafeAreaPadding} > {!isLoadingOnyxValue(metadata) && ( From 4aa77a64714ec7081cf155da6c7fc802b741869f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Sep 2025 14:16:51 +0200 Subject: [PATCH 11/16] fix: typecheck --- src/pages/iou/request/step/IOURequestStepUpgrade.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index 367f891cd54b..c0765a36d2aa 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -48,7 +48,7 @@ function IOURequestStepUpgrade({ policyName: params.name, policyID: params.policyID, currency: params.currency, - file: params.avatarFile, + file: params.avatarFile as File, engagementChoice: CONST.ONBOARDING_CHOICES.TRACK_WORKSPACE, }); policyDataRef.current = policyData; From 8bfc9b0b34420ae02a24d19e55211890706965fe Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 16 Sep 2025 16:27:44 +0200 Subject: [PATCH 12/16] feat: show create expense option --- .../Search/SearchTransactionsChangeReport.tsx | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pages/Search/SearchTransactionsChangeReport.tsx b/src/pages/Search/SearchTransactionsChangeReport.tsx index 959159709756..965a08464a84 100644 --- a/src/pages/Search/SearchTransactionsChangeReport.tsx +++ b/src/pages/Search/SearchTransactionsChangeReport.tsx @@ -3,7 +3,9 @@ import {InteractionManager} from 'react-native'; import {useSession} from '@components/OnyxListItemProvider'; import {useSearchContext} from '@components/Search/SearchContext'; import type {ListItem} from '@components/SelectionList/types'; +import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useOnyx from '@hooks/useOnyx'; +import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport} from '@libs/actions/Transaction'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; @@ -25,6 +27,7 @@ function SearchTransactionsChangeReport() { const isASAPSubmitBetaEnabled = Permissions.isBetaEnabled(CONST.BETAS.ASAP_SUBMIT, allBetas); const session = useSession(); + const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const firstTransactionKey = selectedTransactionsKeys.at(0); const firstTransactionReportID = firstTransactionKey ? selectedTransactions[firstTransactionKey]?.reportID : undefined; @@ -33,6 +36,25 @@ function SearchTransactionsChangeReport() { ? firstTransactionReportID : undefined; + // Get the policy ID from the first transaction + const activePolicyID = firstTransactionKey ? selectedTransactions[firstTransactionKey]?.policyID : undefined; + + const createReport = () => { + const createdReportID = createNewReport(currentUserPersonalDetails, activePolicyID); + const reportNextStep = allReportNextSteps?.[`${ONYXKEYS.COLLECTION.NEXT_STEP}${createdReportID}`]; + changeTransactionsReport( + selectedTransactionsKeys, + createdReportID, + isASAPSubmitBetaEnabled, + session?.accountID ?? CONST.DEFAULT_NUMBER_ID, + session?.email ?? '', + activePolicyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`] : undefined, + reportNextStep, + ); + clearSelectedTransactions(); + Navigation.goBack(); + }; + const selectReport = (item: TransactionGroupListItem) => { if (selectedTransactionsKeys.length === 0) { return; @@ -71,6 +93,7 @@ function SearchTransactionsChangeReport() { selectedReportID={selectedReportID} selectReport={selectReport} removeFromReport={removeFromReport} + createReport={createReport} isEditing /> ); From dc813cd163d130f63258881f5351faa09e7ce164 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 17 Sep 2025 18:03:07 +0200 Subject: [PATCH 13/16] fix: navigate to proper page --- src/components/ReportActionItem/MoneyRequestView.tsx | 2 +- src/pages/iou/request/step/IOURequestStepUpgrade.tsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 9d479d5e8eb9..873dd844608e 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -856,7 +856,7 @@ function MoneyRequestView({ action: CONST.IOU.ACTION.EDIT, transactionID: transaction?.transactionID, reportID: report.reportID, - isReporting: true, + upgradePath: CONST.UPGRADE_PATHS.REPORTS, }), ); return; diff --git a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx index 0955658c3cb2..ba9a1fe8a2ed 100644 --- a/src/pages/iou/request/step/IOURequestStepUpgrade.tsx +++ b/src/pages/iou/request/step/IOURequestStepUpgrade.tsx @@ -171,6 +171,7 @@ function IOURequestStepUpgrade({ buttonDisabled={isOffline} loading={false} isCategorizing={isCategorizing} + isReporting={isReporting} isDistanceRateUpgrade={isDistanceRateUpgrade} /> )} From d9137e447d1c0f7944483bc26f9abb2cf0b40e83 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 19 Sep 2025 17:25:18 +0200 Subject: [PATCH 14/16] fix: apply requested change --- .../iou/request/step/IOURequestEditReportCommon.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx index 7e2347aefa1d..57a091f9eafe 100644 --- a/src/pages/iou/request/step/IOURequestEditReportCommon.tsx +++ b/src/pages/iou/request/step/IOURequestEditReportCommon.tsx @@ -59,7 +59,10 @@ function IOURequestEditReportCommon({ const reportOwnerAccountID = useMemo(() => selectedReport?.ownerAccountID ?? currentUserPersonalDetails.accountID, [selectedReport, currentUserPersonalDetails.accountID]); const reportPolicy = usePolicy(selectedReport?.policyID); const [activePolicyID] = useOnyx(ONYXKEYS.NVP_ACTIVE_POLICY_ID, {canBeMissing: true}); - const policy = usePolicy(activePolicyID); + const [activePolicy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${activePolicyID}`, { + canBeMissing: true, + selector: (policy) => (policy?.type !== CONST.POLICY.TYPE.PERSONAL ? policy : undefined), + }); const [reportNameValuePairs] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS, {canBeMissing: true}); const [allPoliciesID] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: (policies) => mapOnyxCollectionItems(policies, (policyItem) => policyItem?.id), canBeMissing: false}); @@ -152,11 +155,11 @@ function IOURequestEditReportCommon({ ); - }, [createReport, translate, policy]); + }, [createReport, translate, activePolicy]); // eslint-disable-next-line rulesdir/no-negated-variables const shouldShowNotFoundPage = useMemo(() => { From 3f99e7721a82c45d3d887c615b64cf32424b0174 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 19 Sep 2025 18:20:07 +0200 Subject: [PATCH 15/16] fix: do not show move expenses button --- src/pages/Search/SearchPage.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index e5a1dad49226..ed9707d999f4 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -45,6 +45,7 @@ import {navigateToParticipantPage} from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; +import Permissions from '@libs/Permissions'; import {hasVBBA, isPaidGroupPolicy} from '@libs/PolicyUtils'; import {generateReportID, getPolicyExpenseChat} from '@libs/ReportUtils'; import {buildCannedSearchQuery, buildSearchQueryJSON} from '@libs/SearchQueryUtils'; @@ -455,7 +456,10 @@ function SearchPage({route}: SearchPageProps) { }); } - const canAllTransactionsBeMoved = selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); + // TODO: change this condition after the feature is ready to be deployed + const canAllTransactionsBeMoved = Permissions.canUseUnreportedExpense() + ? selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport) && activePolicy?.type !== CONST.POLICY.TYPE.PERSONAL + : selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); if (canAllTransactionsBeMoved) { options.push({ From c00579b4bc4489d1c4cb0d51ff1a279af38efaac Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 19 Sep 2025 18:24:09 +0200 Subject: [PATCH 16/16] fix: minor fix --- src/pages/Search/SearchPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/Search/SearchPage.tsx b/src/pages/Search/SearchPage.tsx index ed9707d999f4..65f143ee771a 100644 --- a/src/pages/Search/SearchPage.tsx +++ b/src/pages/Search/SearchPage.tsx @@ -458,7 +458,7 @@ function SearchPage({route}: SearchPageProps) { // TODO: change this condition after the feature is ready to be deployed const canAllTransactionsBeMoved = Permissions.canUseUnreportedExpense() - ? selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport) && activePolicy?.type !== CONST.POLICY.TYPE.PERSONAL + ? selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport) && !!activePolicy && activePolicy?.type !== CONST.POLICY.TYPE.PERSONAL : selectedTransactionsKeys.every((id) => selectedTransactions[id].canChangeReport); if (canAllTransactionsBeMoved) {