From c044bf6bcba2b0c62c63174ab81497d0fa4d89f8 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Sat, 14 Jun 2025 01:00:37 +0530 Subject: [PATCH 01/29] Basic setup --- src/CONST.ts | 9 +++++++++ src/ROUTES.ts | 8 ++++++++ src/SCREENS.ts | 5 +++++ src/libs/Navigation/linkingConfig/config.ts | 12 ++++++++++++ src/libs/Navigation/types.ts | 11 +++++++++++ 5 files changed, 45 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index 3b23e2a7314a6..1d070ce75c8e8 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1245,6 +1245,7 @@ const CONST = { DOWNLOAD_CSV: 'downloadCSV', DOWNLOAD_PDF: 'downloadPDF', CHANGE_WORKSPACE: 'changeWorkspace', + CHANGE_APPROVER: 'changeApprover', VIEW_DETAILS: 'viewDetails', DELETE: 'delete', RETRACT: 'retract', @@ -6882,6 +6883,14 @@ const CONST = { description: `workspace.upgrade.approvals.description` as const, icon: 'AdvancedApprovalsSquare', }, + multiApprovalLevels: { + id: 'multiApprovalLevels' as const, + alias: 'multi-approval-levels' as const, + name: 'Multiple approval levels' as const, + title: `workspace.upgrade.multiApprovalLevels.title` as const, + description: `workspace.upgrade.multiApprovalLevels.description` as const, + icon: 'AdvancedApprovalsSquare', + }, glCodes: { id: 'glCodes' as const, alias: 'gl-codes', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 4aaa53a0ef6e3..80119f6ea3cfa 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -513,6 +513,14 @@ const ROUTES = { route: 'r/:reportID/settings/visibility', getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/settings/visibility` as const, backTo), }, + REPORT_CHANGE_APPROVER: { + route: 'r/:reportID/change-approver', + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-approver'` as const, backTo), + }, + REPORT_CHANGE_APPROVER_ADD_APPROVER: { + route: 'r/:reportID/change-approver/add', + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-approver/add` as const, backTo), + }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', getRoute: (reportID: string | undefined, reportActionID: string, backTo?: string) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 9d53fe813d5a1..2a3a2d042f0ee 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -218,6 +218,7 @@ const SCREENS = { DEBUG: 'Debug', ADD_UNREPORTED_EXPENSE: 'AddUnreportedExpense', SCHEDULE_CALL: 'ScheduleCall', + REPORT_CHANGE_APPROVER: 'Report_Change_Approver' }, PUBLIC_CONSOLE_DEBUG: 'Console_Debug', ONBOARDING_MODAL: { @@ -738,6 +739,10 @@ const SCREENS = { BOOK: 'ScheduleCall_Book', CONFIRMATION: 'ScheduleCall_Confirmation', }, + REPORT_CHANGE_APPROVER: { + ROOT: 'Report_Change_Approver_Root', + ADD_APPROVER: 'Report_Change_Approver_Add_Approver', + }, } as const; type Screen = DeepValueOf; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 41b34f11354cd..e6cb8f33a2769 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1655,6 +1655,18 @@ const config: LinkingOptions['config'] = { }, }, }, + [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: { + screens: { + [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: { + path: ROUTES.REPORT_CHANGE_APPROVER.route, + exact: true, + }, + [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: { + path: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, + exact: true, + }, + }, + }, }, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e41895598c7a7..0afd68aad0c79 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1710,6 +1710,7 @@ type RightModalNavigatorParamList = { [SCREENS.MONEY_REQUEST.SPLIT_EXPENSE_EDIT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.ADD_UNREPORTED_EXPENSE]: NavigatorScreenParams<{reportId: string | undefined}>; [SCREENS.RIGHT_MODAL.SCHEDULE_CALL]: NavigatorScreenParams; + [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: NavigatorScreenParams; }; type TravelNavigatorParamList = { @@ -2167,6 +2168,15 @@ type ScheduleCallParamList = { }; }; +type ReportChangeApproverParamList = { + [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: { + reportID: string; + }; + [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: { + reportID: string; + }; +}; + type RootNavigatorParamList = PublicScreensParamList & AuthScreensParamList & SearchFullscreenNavigatorParamList; type OnboardingFlowName = keyof OnboardingModalNavigatorParamList; @@ -2264,4 +2274,5 @@ export type { SplitExpenseParamList, SetParamsAction, WorkspacesTabNavigatorName, + ReportChangeApproverParamList }; From 04c182ba806713c416054e133a18a9ec721f4261 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Sat, 14 Jun 2025 01:01:42 +0530 Subject: [PATCH 02/29] Change approver main page --- src/languages/en.ts | 20 +++ src/languages/es.ts | 20 +++ .../ModalStackNavigators/index.tsx | 7 + .../Navigators/RightModalNavigator.tsx | 4 + src/libs/ReportSecondaryActionUtils.ts | 5 + src/pages/ReportChangeApproverPage.tsx | 125 ++++++++++++++++++ 6 files changed, 181 insertions(+) create mode 100644 src/pages/ReportChangeApproverPage.tsx diff --git a/src/languages/en.ts b/src/languages/en.ts index 05f99434cfae5..cc8fbe0198506 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1285,6 +1285,21 @@ const translations = { rates: 'Rates', submitsTo: ({name}: SubmitsToParams) => `Submits to ${name}`, moveExpenses: () => ({one: 'Move expense', other: 'Move expenses'}), + changeApprover: { + title: 'Change approver', + subtitle: 'Choose an option to change the approver for this report.', + description: 'You can also change the approver permanently for all reports in your ', + workflowSettings: 'workflow settings', + actions: { + addApprover: 'Add approver', + addApproverSubtitle: 'Add an additional approver to the existing workflow.', + bypassApprovers: 'Bypass approvers', + bypassApproversSubtitle: 'Assign yourself as final approver and skip any remaining approvers.', + }, + addApprover: { + subtitle: 'Choose an additional approver for this report before we route through the rest of the approval workflow.', + }, + }, }, share: { shareToExpensify: 'Share to Expensify', @@ -5030,6 +5045,11 @@ const translations = { 'Multi-Level Tags help you track expenses with greater precision. Assign multiple tags to each line item—such as department, client, or cost center—to capture the full context of every expense. This enables more detailed reporting, approval workflows, and accounting exports.', onlyAvailableOnPlan: 'Multi-level tags are only available on the Control plan, starting at ', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: 'Multiple approval levels', + description: 'Multiple aproval levels is a workflow tool for companies that require more than one person to approve a report before it can be reimbursed.', + onlyAvailableOnPlan: 'Multiple aproval levels are only available on the Control plan, starting at ', + }, pricing: { perActiveMember: 'per active member per month.', perMember: 'per member per month.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 27e5f0874eedf..d8809abb5bda5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1283,6 +1283,21 @@ const translations = { rates: 'Tasas', submitsTo: ({name}: SubmitsToParams) => `Se envía a ${name}`, moveExpenses: () => ({one: 'Mover gasto', other: 'Mover gastos'}), + changeApprover: { + title: 'Change approver', + subtitle: 'Choose an option to change the approver for this report.', + description: 'You can also change the approver permanently for all reports in your ', + workflowSettings: 'workflow settings', + actions: { + addApprover: 'Add approver', + addApproverSubtitle: 'Add an additional approver to the existing workflow.', + bypassApprovers: 'Bypass approvers', + bypassApproversSubtitle: 'Assign yourself as final approver and skip any remaining approvers.', + }, + addApprover: { + subtitle: 'Choose an additional approver for this report before we route through the rest of the approval workflow', + }, + }, }, share: { shareToExpensify: 'Compartir para Expensify', @@ -5099,6 +5114,11 @@ const translations = { 'Las etiquetas multinivel te ayudan a llevar un control más preciso de los gastos. Asigna múltiples etiquetas a cada partida, como departamento, cliente o centro de costos, para capturar el contexto completo de cada gasto. Esto permite informes más detallados, flujos de aprobación y exportaciones contables.', onlyAvailableOnPlan: 'Las etiquetas multinivel solo están disponibles en el plan Control, a partir de ', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: 'Multiple approval levels', + description: 'Multiple aproval levels is a workflow tool for companies that require more than one person to approve a report before it can be reimbursed.', + onlyAvailableOnPlan: 'Multiple aproval levels are only available on the Control plan, starting at ', + }, note: { upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o', learnMore: 'más información', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 9d7a925e64387..932c231360f4f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -20,6 +20,7 @@ import type { PrivateNotesNavigatorParamList, ProfileNavigatorParamList, ReferralDetailsNavigatorParamList, + ReportChangeApproverParamList, ReportChangeWorkspaceNavigatorParamList, ReportDescriptionNavigatorParamList, ReportDetailsNavigatorParamList, @@ -162,6 +163,11 @@ const ReportChangeWorkspaceModalStackNavigator = createModalStackNavigator require('../../../../pages/ReportChangeWorkspacePage').default, }); +const ReportChangeApproverModalStackNavigator = createModalStackNavigator({ + [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: () => require('../../../../pages/ReportChangeApproverPage').default, + [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: () => require('../../../../pages/ReportAddApproverPage').default, +}); + const ReportSettingsModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default, [SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default, @@ -800,6 +806,7 @@ export { ReportDescriptionModalStackNavigator, ReportDetailsModalStackNavigator, ReportChangeWorkspaceModalStackNavigator, + ReportChangeApproverModalStackNavigator, ReportParticipantsModalStackNavigator, ReportSettingsModalStackNavigator, RoomMembersModalStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index 366131799dfad..bf2dbd30c3f31 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -104,6 +104,10 @@ function RightModalNavigator({navigation, route}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.REPORT_CHANGE_WORKSPACE} component={ModalStackNavigators.ReportChangeWorkspaceModalStackNavigator} /> + ; + +function ReportChangeApproverPage({report, policies}: ReportChangeApproverPageProps) { + const reportID = report?.reportID; + const {translate} = useLocalize(); + const styles = useThemeStyles(); + + const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); + const reportPolicy = policies && Object.values(policies).find((policy) => policy?.id === report?.policyID); + const [selectedApproverType, setSelectedApproverType] = useState(); + + const changeApprover = useCallback(() => { + if (!selectedApproverType) { + return; + } + if (selectedApproverType === 'addApprover') { + if (reportPolicy && !isControlPolicy(reportPolicy)) { + Navigation.navigate( + ROUTES.WORKSPACE_UPGRADE.getRoute( + reportPolicy.id, + CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.alias, + ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.getRoute(report.reportID), + ), + ); + return; + } + Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.getRoute(report.reportID)); + return; + } + + if (!isPolicyAdmin(reportPolicy) || !reportPolicy || !session?.accountID) { + return; + } + changeReportApprover(reportID, reportPolicy.id, session.accountID); + Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); + }, [selectedApproverType, reportPolicy, reportID, session?.accountID, report.reportID]); + + const sections = useMemo(() => { + const data = [ + { + text: translate('iou.changeApprover.actions.addApprover'), + keyForList: 'addApprover', + value: 'addApprover', + alternateText: translate('iou.changeApprover.actions.addApproverSubtitle'), + isSelected: selectedApproverType === 'addApprover', + }, + ]; + + if (!isUserPolicyAdmin(reportPolicy, getManagerAccountEmail(reportPolicy, report))) { + data.push({ + text: translate('iou.changeApprover.actions.bypassApprovers'), + keyForList: 'bypassApprover', + value: 'bypassApprover', + alternateText: translate('iou.changeApprover.actions.bypassApproversSubtitle'), + isSelected: selectedApproverType === 'bypassApprover', + }); + } + + return [{data}]; + }, [report, reportPolicy, selectedApproverType, translate]); + + if (!isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report)) { + return ; + } + + return ( + + + setSelectedApproverType(option.keyForList)} + showConfirmButton + confirmButtonText={translate('iou.changeApprover.title')} + onConfirm={changeApprover} + customListHeader={ + <> + {translate('iou.changeApprover.subtitle')} + + {translate('iou.changeApprover.description')} + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(reportPolicy?.id))}>{translate('iou.changeApprover.workflowSettings')} + . + + + } + /> + + ); +} + +ReportChangeApproverPage.displayName = 'ReportChangeApproverPage'; + +export default withReportOrNotFound()(ReportChangeApproverPage); From 4f00e0164546f4d5f4347d33974c28f31c8455de Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Sat, 14 Jun 2025 01:01:57 +0530 Subject: [PATCH 03/29] Add change approver secondary action --- src/components/MoneyReportHeader.tsx | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index a07c28609187d..fa430cc614baf 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -792,6 +792,18 @@ function MoneyReportHeader({ Navigation.navigate(ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.getRoute(moneyRequestReport.reportID)); }, }, + [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER]: { + text: translate('iou.changeApprover.title'), + icon: Expensicons.Workflows, + value: CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER, + onSelected: () => { + console.debug('[MoneyReportHeader] Change Approver selected', moneyRequestReport); + if (!moneyRequestReport) { + return; + } + Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER.getRoute(moneyRequestReport.reportID, ' ')); + }, + }, [CONST.REPORT.SECONDARY_ACTIONS.DELETE]: { text: translate('common.delete'), icon: Expensicons.Trashcan, From d1f20d6c43f4fb22e54c3fcc008e72f10746cad7 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Sat, 14 Jun 2025 01:03:05 +0530 Subject: [PATCH 04/29] App approver page and basic action --- src/libs/actions/Report.ts | 10 ++++++++++ src/pages/ReportAddApproverPage.tsx | 15 +++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 src/pages/ReportAddApproverPage.tsx diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 178000941a8d7..4396b7d1fe88d 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -5599,6 +5599,15 @@ function changeReportPolicy(reportID: string, policyID: string) { } } +/** + * Changes the policy of a report and all its child reports, and moves the report to the new policy's expense chat. + */ +function changeReportApprover(reportID: string, policyID: string, memberAccountID: number) { + if (!reportID || !policyID) { + return; + } +} + export type {Video, GuidedSetupData, TaskForParameters, IntroSelected}; export { @@ -5707,4 +5716,5 @@ export { changeReportPolicy, removeFailedReport, openUnreportedExpense, + changeReportApprover, }; diff --git a/src/pages/ReportAddApproverPage.tsx b/src/pages/ReportAddApproverPage.tsx new file mode 100644 index 0000000000000..743e0d7376871 --- /dev/null +++ b/src/pages/ReportAddApproverPage.tsx @@ -0,0 +1,15 @@ +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; +import type SCREENS from '@src/SCREENS'; +import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; +import withReportOrNotFound from './home/report/withReportOrNotFound'; + +type ReportAddApproverPageProps = WithReportOrNotFoundProps & PlatformStackScreenProps; + +function ReportAddApproverPage({report}: ReportAddApproverPageProps) { + return null; +} + +ReportAddApproverPage.displayName = 'ReportAddApproverPage'; + +export default withReportOrNotFound()(ReportAddApproverPage); From 53f4c721101be958956b2dd6ccd861d0f049ac7c Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Mon, 16 Jun 2025 14:23:19 +0530 Subject: [PATCH 05/29] Add add Approver page. --- src/pages/ReportAddApproverPage.tsx | 181 +++++++++++++++++++++++++++- 1 file changed, 179 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportAddApproverPage.tsx b/src/pages/ReportAddApproverPage.tsx index 743e0d7376871..c91c922c36d35 100644 --- a/src/pages/ReportAddApproverPage.tsx +++ b/src/pages/ReportAddApproverPage.tsx @@ -1,13 +1,190 @@ +import debounce from 'lodash/debounce'; +import React, {useCallback, useMemo, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import Badge from '@components/Badge'; +import BlockingView from '@components/BlockingViews/BlockingView'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {FallbackAvatar} from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; +import Text from '@components/Text'; +import type {SelectionListApprover} from '@components/WorkspaceMembersSelectionList'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; +import {getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; +import {getManagerAccountEmail, getMemberAccountIDsForWorkspace, isPolicyAdmin} from '@libs/PolicyUtils'; +import {isMoneyRequestReport, isMoneyRequestReportPendingDeletion} from '@libs/ReportUtils'; +import tokenizedSearch from '@libs/tokenizedSearch'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import NotFoundPage from './ErrorPage/NotFoundPage'; import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; type ReportAddApproverPageProps = WithReportOrNotFoundProps & PlatformStackScreenProps; -function ReportAddApproverPage({report}: ReportAddApproverPageProps) { - return null; +function ReportAddApproverPage({report, isLoadingReportData, policies}: ReportAddApproverPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [selectedApproverEmail, setSelectedApproverEmail] = useState(undefined); + const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); + + const [allApprovers, setAllApprovers] = useState([]); + const shouldShowTextInput = allApprovers?.length >= CONST.STANDARD_LIST_ITEM_LIMIT; + + const policy = policies && Object.values(policies).find((policyData) => policyData?.id === report?.policyID); + + const employeeList = policy?.employeeList; + const sections = useMemo(() => { + const approvers: SelectionListApprover[] = []; + + if (employeeList) { + const availableApprovers = Object.values(employeeList) + .map((employee): SelectionListApprover | null => { + const isAdmin = employee?.role === CONST.REPORT.ROLE.ADMIN; + const email = employee.email; + + if (!email) { + return null; + } + + if (getManagerAccountEmail(policy, report) === email) { + return null; + } + + const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(employeeList); + const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); + const {avatar, displayName = email} = personalDetails?.[accountID] ?? {}; + + return { + text: displayName, + alternateText: email, + keyForList: email, + isSelected: selectedApproverEmail === email, + login: email, + icons: [{source: avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], + rightElement: isAdmin ? : undefined, + }; + }) + .filter((approver): approver is SelectionListApprover => !!approver); + + approvers.push(...availableApprovers); + // eslint-disable-next-line react-compiler/react-compiler + setAllApprovers(approvers); + } + + const filteredApprovers = + debouncedSearchTerm !== '' ? tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm), (option) => [option.text ?? '', option.login ?? '']) : approvers; + + const data = sortAlphabetically(filteredApprovers, 'text'); + return [ + { + title: undefined, + data, + shouldShow: true, + }, + ]; + }, [employeeList, debouncedSearchTerm, policy, report, personalDetails, selectedApproverEmail, translate]); + + const shouldShowListEmptyContent = !debouncedSearchTerm && !sections.at(0)?.data.length; + + const addApprover = useCallback(() => {}, []); + + const button = useMemo(() => { + return ( + + ); + }, [addApprover, selectedApproverEmail, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate]); + + const toggleApprover = useCallback( + (approver: SelectionListApprover) => + debounce(() => { + if (selectedApproverEmail === approver.login) { + setSelectedApproverEmail(undefined); + return; + } + setSelectedApproverEmail(approver.login); + }, 200)(), + [selectedApproverEmail], + ); + + const headerMessage = useMemo(() => (searchTerm && !sections.at(0)?.data?.length ? translate('common.noResultsFound') : ''), [searchTerm, sections, translate]); + + const listEmptyContent = useMemo( + () => ( + + ), + [translate, styles.textSupporting, styles.pb10], + ); + + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || !isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report); + + if (shouldShowNotFoundView) { + return ; + } + + return ( + + { + Navigation.goBack(ROUTES.REPORT_CHANGE_APPROVER.getRoute(report.reportID)); + }} + /> + {translate('iou.changeApprover.addApprover.subtitle')} + + + ); } ReportAddApproverPage.displayName = 'ReportAddApproverPage'; From 4b900d425d1889ff9013647012ceb0abaf5bbc48 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Mon, 16 Jun 2025 14:23:31 +0530 Subject: [PATCH 06/29] Update not found page logic --- src/pages/ReportChangeApproverPage.tsx | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 84fcee76f44b4..afa4c244ff199 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -18,13 +18,14 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import NotFoundPage from './ErrorPage/NotFoundPage'; import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; import withReportOrNotFound from './home/report/withReportOrNotFound'; type ReportChangeApproverPageProps = WithReportOrNotFoundProps & PlatformStackScreenProps; -function ReportChangeApproverPage({report, policies}: ReportChangeApproverPageProps) { +function ReportChangeApproverPage({report, policies, isLoadingReportData}: ReportChangeApproverPageProps) { const reportID = report?.reportID; const {translate} = useLocalize(); const styles = useThemeStyles(); @@ -83,7 +84,11 @@ function ReportChangeApproverPage({report, policies}: ReportChangeApproverPagePr return [{data}]; }, [report, reportPolicy, selectedApproverType, translate]); - if (!isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report)) { + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundView = + (isEmptyObject(reportPolicy) && !isLoadingReportData) || !isPolicyAdmin(reportPolicy) || !isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report); + + if (shouldShowNotFoundView) { return ; } From 899f341bf9f269c783414435d7153e5b12a1a031 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 22 Jul 2025 21:08:03 +0530 Subject: [PATCH 07/29] Revert Add approver changes for next PR --- src/pages/ReportAddApproverPage.tsx | 192 ------------------------- src/pages/ReportChangeApproverPage.tsx | 10 +- 2 files changed, 1 insertion(+), 201 deletions(-) delete mode 100644 src/pages/ReportAddApproverPage.tsx diff --git a/src/pages/ReportAddApproverPage.tsx b/src/pages/ReportAddApproverPage.tsx deleted file mode 100644 index c91c922c36d35..0000000000000 --- a/src/pages/ReportAddApproverPage.tsx +++ /dev/null @@ -1,192 +0,0 @@ -import debounce from 'lodash/debounce'; -import React, {useCallback, useMemo, useState} from 'react'; -import {useOnyx} from 'react-native-onyx'; -import Badge from '@components/Badge'; -import BlockingView from '@components/BlockingViews/BlockingView'; -import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {FallbackAvatar} from '@components/Icon/Expensicons'; -import * as Illustrations from '@components/Icon/Illustrations'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import InviteMemberListItem from '@components/SelectionList/InviteMemberListItem'; -import Text from '@components/Text'; -import type {SelectionListApprover} from '@components/WorkspaceMembersSelectionList'; -import useDebouncedState from '@hooks/useDebouncedState'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import {canUseTouchScreen} from '@libs/DeviceCapabilities'; -import Navigation from '@libs/Navigation/Navigation'; -import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; -import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; -import {getSearchValueForPhoneOrEmail, sortAlphabetically} from '@libs/OptionsListUtils'; -import {getManagerAccountEmail, getMemberAccountIDsForWorkspace, isPolicyAdmin} from '@libs/PolicyUtils'; -import {isMoneyRequestReport, isMoneyRequestReportPendingDeletion} from '@libs/ReportUtils'; -import tokenizedSearch from '@libs/tokenizedSearch'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import NotFoundPage from './ErrorPage/NotFoundPage'; -import type {WithReportOrNotFoundProps} from './home/report/withReportOrNotFound'; -import withReportOrNotFound from './home/report/withReportOrNotFound'; - -type ReportAddApproverPageProps = WithReportOrNotFoundProps & PlatformStackScreenProps; - -function ReportAddApproverPage({report, isLoadingReportData, policies}: ReportAddApproverPageProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); - const [selectedApproverEmail, setSelectedApproverEmail] = useState(undefined); - const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST); - - const [allApprovers, setAllApprovers] = useState([]); - const shouldShowTextInput = allApprovers?.length >= CONST.STANDARD_LIST_ITEM_LIMIT; - - const policy = policies && Object.values(policies).find((policyData) => policyData?.id === report?.policyID); - - const employeeList = policy?.employeeList; - const sections = useMemo(() => { - const approvers: SelectionListApprover[] = []; - - if (employeeList) { - const availableApprovers = Object.values(employeeList) - .map((employee): SelectionListApprover | null => { - const isAdmin = employee?.role === CONST.REPORT.ROLE.ADMIN; - const email = employee.email; - - if (!email) { - return null; - } - - if (getManagerAccountEmail(policy, report) === email) { - return null; - } - - const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(employeeList); - const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); - const {avatar, displayName = email} = personalDetails?.[accountID] ?? {}; - - return { - text: displayName, - alternateText: email, - keyForList: email, - isSelected: selectedApproverEmail === email, - login: email, - icons: [{source: avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], - rightElement: isAdmin ? : undefined, - }; - }) - .filter((approver): approver is SelectionListApprover => !!approver); - - approvers.push(...availableApprovers); - // eslint-disable-next-line react-compiler/react-compiler - setAllApprovers(approvers); - } - - const filteredApprovers = - debouncedSearchTerm !== '' ? tokenizedSearch(approvers, getSearchValueForPhoneOrEmail(debouncedSearchTerm), (option) => [option.text ?? '', option.login ?? '']) : approvers; - - const data = sortAlphabetically(filteredApprovers, 'text'); - return [ - { - title: undefined, - data, - shouldShow: true, - }, - ]; - }, [employeeList, debouncedSearchTerm, policy, report, personalDetails, selectedApproverEmail, translate]); - - const shouldShowListEmptyContent = !debouncedSearchTerm && !sections.at(0)?.data.length; - - const addApprover = useCallback(() => {}, []); - - const button = useMemo(() => { - return ( - - ); - }, [addApprover, selectedApproverEmail, styles.flexBasisAuto, styles.flexGrow0, styles.flexReset, styles.flexShrink0, translate]); - - const toggleApprover = useCallback( - (approver: SelectionListApprover) => - debounce(() => { - if (selectedApproverEmail === approver.login) { - setSelectedApproverEmail(undefined); - return; - } - setSelectedApproverEmail(approver.login); - }, 200)(), - [selectedApproverEmail], - ); - - const headerMessage = useMemo(() => (searchTerm && !sections.at(0)?.data?.length ? translate('common.noResultsFound') : ''), [searchTerm, sections, translate]); - - const listEmptyContent = useMemo( - () => ( - - ), - [translate, styles.textSupporting, styles.pb10], - ); - - // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || !isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report); - - if (shouldShowNotFoundView) { - return ; - } - - return ( - - { - Navigation.goBack(ROUTES.REPORT_CHANGE_APPROVER.getRoute(report.reportID)); - }} - /> - {translate('iou.changeApprover.addApprover.subtitle')} - - - ); -} - -ReportAddApproverPage.displayName = 'ReportAddApproverPage'; - -export default withReportOrNotFound()(ReportAddApproverPage); diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index afa4c244ff199..980f8009c8ca3 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -61,15 +61,7 @@ function ReportChangeApproverPage({report, policies, isLoadingReportData}: Repor }, [selectedApproverType, reportPolicy, reportID, session?.accountID, report.reportID]); const sections = useMemo(() => { - const data = [ - { - text: translate('iou.changeApprover.actions.addApprover'), - keyForList: 'addApprover', - value: 'addApprover', - alternateText: translate('iou.changeApprover.actions.addApproverSubtitle'), - isSelected: selectedApproverType === 'addApprover', - }, - ]; + const data = []; if (!isUserPolicyAdmin(reportPolicy, getManagerAccountEmail(reportPolicy, report))) { data.push({ From 5cd6f09c5a0f5816b02dd7650c862401a48d03e8 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 22 Jul 2025 21:44:42 +0530 Subject: [PATCH 08/29] Revert Add approver changes --- src/pages/ReportChangeApproverPage.tsx | 36 +++++++------------------- 1 file changed, 9 insertions(+), 27 deletions(-) diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 980f8009c8ca3..11baef89dde24 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -12,9 +12,8 @@ import {changeReportApprover} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; -import {getManagerAccountEmail, isControlPolicy, isPolicyAdmin, isUserPolicyAdmin} from '@libs/PolicyUtils'; +import {getManagerAccountEmail, isPolicyAdmin, isUserPolicyAdmin} from '@libs/PolicyUtils'; import {isMoneyRequestReport, isMoneyRequestReportPendingDeletion} from '@libs/ReportUtils'; -import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -25,45 +24,30 @@ import withReportOrNotFound from './home/report/withReportOrNotFound'; type ReportChangeApproverPageProps = WithReportOrNotFoundProps & PlatformStackScreenProps; -function ReportChangeApproverPage({report, policies, isLoadingReportData}: ReportChangeApproverPageProps) { +function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportChangeApproverPageProps) { const reportID = report?.reportID; const {translate} = useLocalize(); const styles = useThemeStyles(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); - const reportPolicy = policies && Object.values(policies).find((policy) => policy?.id === report?.policyID); const [selectedApproverType, setSelectedApproverType] = useState(); const changeApprover = useCallback(() => { if (!selectedApproverType) { return; } - if (selectedApproverType === 'addApprover') { - if (reportPolicy && !isControlPolicy(reportPolicy)) { - Navigation.navigate( - ROUTES.WORKSPACE_UPGRADE.getRoute( - reportPolicy.id, - CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.alias, - ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.getRoute(report.reportID), - ), - ); - return; - } - Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.getRoute(report.reportID)); - return; - } - if (!isPolicyAdmin(reportPolicy) || !reportPolicy || !session?.accountID) { + if (!isPolicyAdmin(policy) || !policy || !session?.accountID) { return; } - changeReportApprover(reportID, reportPolicy.id, session.accountID); + changeReportApprover(reportID, policy.id, session.accountID); Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - }, [selectedApproverType, reportPolicy, reportID, session?.accountID, report.reportID]); + }, [selectedApproverType, policy, reportID, session?.accountID]); const sections = useMemo(() => { const data = []; - if (!isUserPolicyAdmin(reportPolicy, getManagerAccountEmail(reportPolicy, report))) { + if (!isUserPolicyAdmin(policy, getManagerAccountEmail(policy, report))) { data.push({ text: translate('iou.changeApprover.actions.bypassApprovers'), keyForList: 'bypassApprover', @@ -74,11 +58,10 @@ function ReportChangeApproverPage({report, policies, isLoadingReportData}: Repor } return [{data}]; - }, [report, reportPolicy, selectedApproverType, translate]); + }, [report, policy, selectedApproverType, translate]); // eslint-disable-next-line rulesdir/no-negated-variables - const shouldShowNotFoundView = - (isEmptyObject(reportPolicy) && !isLoadingReportData) || !isPolicyAdmin(reportPolicy) || !isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report); + const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || !isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report); if (shouldShowNotFoundView) { return ; @@ -107,8 +90,7 @@ function ReportChangeApproverPage({report, policies, isLoadingReportData}: Repor {translate('iou.changeApprover.subtitle')} {translate('iou.changeApprover.description')} - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(reportPolicy?.id))}>{translate('iou.changeApprover.workflowSettings')} - . + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policy?.id))}>{translate('iou.changeApprover.workflowSettings')}. } From f0068aa56b88f76b0ff047c4f2d14632bd1cc864 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 22 Jul 2025 21:45:04 +0530 Subject: [PATCH 09/29] Fix issues --- src/pages/ReportChangeApproverPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 11baef89dde24..f3191201975bf 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -1,5 +1,4 @@ import React, {useCallback, useMemo, useState} from 'react'; -import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; @@ -7,6 +6,7 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; import {changeReportApprover} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; From 07b222e06139372ff3e8a636752a2cfb011ca075 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 22 Jul 2025 21:48:12 +0530 Subject: [PATCH 10/29] Remove Extra screen --- src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index c94acaf42be4b..4565d599d10f6 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -167,7 +167,6 @@ const ReportChangeWorkspaceModalStackNavigator = createModalStackNavigator({ [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: () => require('../../../../pages/ReportChangeApproverPage').default, - [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: () => require('../../../../pages/ReportAddApproverPage').default, }); const ReportSettingsModalStackNavigator = createModalStackNavigator({ From 520b0e9eaa3e4a11af0fbbae6a50df33b2441b56 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 02:29:18 +0530 Subject: [PATCH 11/29] Navigation fixes --- src/ROUTES.ts | 2 +- src/components/MoneyReportHeader.tsx | 2 +- src/libs/Navigation/linkingConfig/config.ts | 10 ++-------- src/pages/home/report/withReportOrNotFound.tsx | 4 +++- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b5a9ef59ddc32..77449024e2a82 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -509,7 +509,7 @@ const ROUTES = { }, REPORT_CHANGE_APPROVER: { route: 'r/:reportID/change-approver', - getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-approver'` as const, backTo), + getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-approver` as const, backTo), }, REPORT_CHANGE_APPROVER_ADD_APPROVER: { route: 'r/:reportID/change-approver/add', diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d0b2137097c28..878d2c8656e85 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -928,7 +928,7 @@ function MoneyReportHeader({ if (!moneyRequestReport) { return; } - Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER.getRoute(moneyRequestReport.reportID, ' ')); + Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute())); }, }, [CONST.REPORT.SECONDARY_ACTIONS.DELETE]: { diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 566fa96146387..13c35bde57a7f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1599,14 +1599,8 @@ const config: LinkingOptions['config'] = { }, [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: { screens: { - [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: { - path: ROUTES.REPORT_CHANGE_APPROVER.route, - exact: true, - }, - [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: { - path: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, - exact: true, - }, + [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: ROUTES.REPORT_CHANGE_APPROVER.route, + [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, }, }, }, diff --git a/src/pages/home/report/withReportOrNotFound.tsx b/src/pages/home/report/withReportOrNotFound.tsx index 3070876741f50..94e997465e393 100644 --- a/src/pages/home/report/withReportOrNotFound.tsx +++ b/src/pages/home/report/withReportOrNotFound.tsx @@ -13,6 +13,7 @@ import {canAccessReport} from '@libs/ReportUtils'; import type { ParticipantsNavigatorParamList, PrivateNotesNavigatorParamList, + ReportChangeApproverParamList, ReportChangeWorkspaceNavigatorParamList, ReportDescriptionNavigatorParamList, ReportDetailsNavigatorParamList, @@ -52,7 +53,8 @@ type ScreenProps = | PlatformStackScreenProps | PlatformStackScreenProps | PlatformStackScreenProps - | PlatformStackScreenProps; + | PlatformStackScreenProps + | PlatformStackScreenProps; type WithReportOrNotFoundProps = WithReportOrNotFoundOnyxProps & { route: ScreenProps['route']; From 9418aca49d2bbceaf35acef5eee7db542247c880 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 02:55:14 +0530 Subject: [PATCH 12/29] Enable bypass approver option only when manager is not admin --- src/libs/PolicyUtils.ts | 6 ++++++ src/libs/ReportSecondaryActionUtils.ts | 5 ++++- src/pages/ReportChangeApproverPage.tsx | 4 ++-- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index e77a1e1e46cde..a26d7f527a1bd 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1513,6 +1513,11 @@ function isUserInvitedToWorkspace(): boolean { ); } +function isMemberPolicyAdmin(policy: OnyxEntry, memberEmail: string): boolean { + const admins = getAdminEmployees(policy); + return admins.some((admin) => admin.email === memberEmail); +} + export { canEditTaxRate, escapeTagName, @@ -1664,6 +1669,7 @@ export { hasIndependentTags, getLengthOfTag, getPolicyEmployeeAccountIDs, + isMemberPolicyAdmin, }; export type {MemberEmailsToAccountIDs}; diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index ac7cbc1a1705e..9c5ef326ee50a 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -9,10 +9,12 @@ import { arePaymentsEnabled as arePaymentsEnabledUtils, getConnectedIntegration, getCorrectedAutoReportingFrequency, + getManagerAccountEmail, getSubmitToAccountID, getValidConnectedIntegration, hasIntegrationAutoSync, isInstantSubmitEnabled, + isMemberPolicyAdmin, isPolicyAdmin, isPolicyMember, isPreferredExporter, @@ -668,7 +670,8 @@ function getSecondaryReportActions({ options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); } - if (isExpenseReportUtils(report) && isProcessingReportUtils(report) && isPolicyAdmin(policy)) { + // When report manager is not the policy admin and current user is policy admin, allow changing the approver + if (!isMemberPolicyAdmin(policy, getManagerAccountEmail(policy, report)) && isExpenseReportUtils(report) && isProcessingReportUtils(report) && isPolicyAdmin(policy)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER); } diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index f3191201975bf..59cd259919727 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -12,7 +12,7 @@ import {changeReportApprover} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; -import {getManagerAccountEmail, isPolicyAdmin, isUserPolicyAdmin} from '@libs/PolicyUtils'; +import {getManagerAccountEmail, isMemberPolicyAdmin, isPolicyAdmin} from '@libs/PolicyUtils'; import {isMoneyRequestReport, isMoneyRequestReportPendingDeletion} from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -47,7 +47,7 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC const sections = useMemo(() => { const data = []; - if (!isUserPolicyAdmin(policy, getManagerAccountEmail(policy, report))) { + if (!isMemberPolicyAdmin(policy, getManagerAccountEmail(policy, report))) { data.push({ text: translate('iou.changeApprover.actions.bypassApprovers'), keyForList: 'bypassApprover', From bfd96991df1f5362bdbae07823ad90125bf2579c Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 03:01:22 +0530 Subject: [PATCH 13/29] Add Assign Report to me action --- .../API/parameters/AssignReportToMeParams.ts | 6 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/ReportUtils.ts | 33 +++++++ src/libs/actions/Report.ts | 99 ++++++++++++++++++- src/pages/ReportChangeApproverPage.tsx | 6 +- 6 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 src/libs/API/parameters/AssignReportToMeParams.ts diff --git a/src/libs/API/parameters/AssignReportToMeParams.ts b/src/libs/API/parameters/AssignReportToMeParams.ts new file mode 100644 index 0000000000000..6669185bcead1 --- /dev/null +++ b/src/libs/API/parameters/AssignReportToMeParams.ts @@ -0,0 +1,6 @@ +type AssignReportToMeParams = { + /** Expense reportID */ + reportID: string; +}; + +export default AssignReportToMeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 9e726ef807da8..6a7a5973136b1 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -414,3 +414,4 @@ export type {default as ReopenReportParams} from './ReopenReportParams'; export type {default as OpenUnreportedExpensesPageParams} from './OpenUnreportedExpensesPageParams'; export type {default as VerifyTestDriveRecipientParams} from './VerifyTestDriveRecipientParams'; export type {default as ExportSearchWithTemplateParams} from './ExportSearchWithTemplateParams'; +export type {default as AssignReportToMeParams} from './AssignReportToMeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 4eb7e4e0db755..a47758c670ea9 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -494,6 +494,7 @@ const WRITE_COMMANDS = { TRAVEL_SIGNUP_REQUEST: 'RequestTravelAccess', DELETE_VACATION_DELEGATE: 'DeleteVacationDelegate', IMPORT_PLAID_ACCOUNTS: 'ImportPlaidAccounts', + ASSIGN_REPORT_TO_ME: 'AssignReportToMe', } as const; type WriteCommand = ValueOf; @@ -1007,6 +1008,7 @@ type WriteCommandParameters = { // Change transaction report [WRITE_COMMANDS.CHANGE_TRANSACTIONS_REPORT]: Parameters.ChangeTransactionsReportParams; [WRITE_COMMANDS.TRAVEL_SIGNUP_REQUEST]: null; + [WRITE_COMMANDS.ASSIGN_REPORT_TO_ME]: Parameters.AssignReportToMeParams; }; const READ_COMMANDS = { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 74614d6a16a53..5fcdd93400df3 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -725,6 +725,12 @@ type OptimisticIOUReport = Pick< | 'fieldList' | 'parentReportActionID' >; + +type OptimisticChangedApproverReportAction = Pick< + ReportAction, + 'actionName' | 'actorAccountID' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction' +>; + type DisplayNameWithTooltips = Array>; type CustomIcon = { @@ -7553,6 +7559,32 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio }; } +function buildOptimisticChangeApproverReportAction(managerID: number): OptimisticChangedApproverReportAction { + return { + actionName: managerID === currentUserAccountID ? CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL : CONST.REPORT.ACTIONS.TYPE.ACTION_REROUTE, + actorAccountID: currentUserAccountID, + avatar: getCurrentUserAvatar(), + created: DateUtils.getDBTime(), + message: [ + { + type: CONST.REPORT.MESSAGE.TYPE.COMMENT, + text: `changed the approver to ${getDisplayNameForParticipant({accountID: managerID})}`, + html: `changed the approver to `, + }, + ], + person: [ + { + type: CONST.REPORT.MESSAGE.TYPE.TEXT, + style: 'strong', + text: getCurrentUserDisplayNameOrEmail(), + }, + ], + shouldShow: false, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + reportActionID: rand64(), + }; +} + function buildOptimisticAnnounceChat(policyID: string, accountIDs: number[]): OptimisticAnnounceChat { const announceReport = getRoom(CONST.REPORT.CHAT_TYPE.POLICY_ANNOUNCE, policyID); // This will be fixed as part of https://github.com/Expensify/Expensify/issues/507850 @@ -11385,6 +11417,7 @@ export { buildOptimisticDetachReceipt, buildParticipantsFromAccountIDs, buildReportNameFromParticipantNames, + buildOptimisticChangeApproverReportAction, buildTransactionThread, canAccessReport, isReportNotFound, diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 8788d23c99688..88ae84fa37cdc 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -99,6 +99,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {OptimisticAddCommentReportAction, OptimisticChatReport, SelfDMParameters} from '@libs/ReportUtils'; import { buildOptimisticAddCommentReportAction, + buildOptimisticChangeApproverReportAction, buildOptimisticChangeFieldAction, buildOptimisticChangePolicyReportAction, buildOptimisticChatReport, @@ -196,6 +197,7 @@ import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; +import {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {clearByKey} from './CachedPDFPaths'; import {setDownload} from './Download'; @@ -5831,10 +5833,102 @@ function resolveConciergeCategoryOptions(reportID: string | undefined, actionRep /** * Changes the policy of a report and all its child reports, and moves the report to the new policy's expense chat. */ -function changeReportApprover(reportID: string, policyID: string, memberAccountID: number) { - if (!reportID || !policyID) { +function changeReportApprover(report: OnyxEntry, memberAccountID: number) { + if (!report?.reportID) { return; } + + const takeControlReportAction = buildOptimisticChangeApproverReportAction(memberAccountID); + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: { + managerID: currentUserAccountID, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: { + [takeControlReportAction.reportActionID]: takeControlReportAction, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: { + managerID: report.managerID, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: { + [takeControlReportAction.reportActionID]: null, + }, + }, + ], + }; + + const memberPersonalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs({accountIDs: [memberAccountID], currentUserAccountID}); + const params = { + reportID: report.reportID, + newApproverEmail: memberPersonalDetails?.at(0)?.login ?? '', + }; + + API.write(WRITE_COMMANDS.REROUTE_REPORT, params, onyxData); +} + +function assignCurrentUserAsApprover(report: OnyxEntry) { + if (!report?.reportID) { + return; + } + + const takeControlReportAction = buildOptimisticChangeApproverReportAction(currentUserAccountID); + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: { + managerID: currentUserAccountID, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: { + [takeControlReportAction.reportActionID]: takeControlReportAction, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: { + managerID: report.managerID, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: { + [takeControlReportAction.reportActionID]: null, + }, + }, + ], + }; + + const params = { + reportID: report.reportID, + }; + + API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); } export type {Video, GuidedSetupData, TaskForParameters, IntroSelected}; @@ -5950,4 +6044,5 @@ export { removeFailedReport, openUnreportedExpense, changeReportApprover, + assignCurrentUserAsApprover, }; diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 59cd259919727..a4c4f1704a736 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -8,7 +8,7 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {changeReportApprover} from '@libs/actions/Report'; +import {assignCurrentUserAsApprover, changeReportApprover} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; @@ -40,9 +40,9 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC if (!isPolicyAdmin(policy) || !policy || !session?.accountID) { return; } - changeReportApprover(reportID, policy.id, session.accountID); + assignCurrentUserAsApprover(report); Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - }, [selectedApproverType, policy, reportID, session?.accountID]); + }, [selectedApproverType, reportPolicy, session?.accountID, report, reportID]); const sections = useMemo(() => { const data = []; From 42d338371a50001c4eb0909694d2fce95571f9df Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 03:28:25 +0530 Subject: [PATCH 14/29] Add assignCurrentUserAsApprover action --- src/CONST/index.ts | 1 + src/libs/actions/Report.ts | 53 ++------------------------ src/pages/ReportChangeApproverPage.tsx | 2 +- 3 files changed, 6 insertions(+), 50 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index ad2c09a24f2af..f60356f4b2e8c 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1186,6 +1186,7 @@ const CONST = { RETRACTED: 'RETRACTED', REOPENED: 'REOPENED', REPORT_PREVIEW: 'REPORTPREVIEW', + ACTION_REROUTE: 'ACTIONREROUTE', SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action SHARE: 'SHARE', // OldDot Action STRIPE_PAID: 'STRIPEPAID', // OldDot Action diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 88ae84fa37cdc..46cc2bf41caf8 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -197,7 +197,7 @@ import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; -import {OnyxData} from '@src/types/onyx/Request'; +import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {clearByKey} from './CachedPDFPaths'; import {setDownload} from './Download'; @@ -5830,15 +5830,12 @@ function resolveConciergeCategoryOptions(reportID: string | undefined, actionRep } as Partial); } -/** - * Changes the policy of a report and all its child reports, and moves the report to the new policy's expense chat. - */ -function changeReportApprover(report: OnyxEntry, memberAccountID: number) { +function assignCurrentUserAsApprover(report: OnyxEntry) { if (!report?.reportID) { return; } - const takeControlReportAction = buildOptimisticChangeApproverReportAction(memberAccountID); + const takeControlReportAction = buildOptimisticChangeApproverReportAction(currentUserAccountID); const onyxData: OnyxData = { optimisticData: [ { @@ -5856,14 +5853,7 @@ function changeReportApprover(report: OnyxEntry, memberAccountID: number }, }, ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, - value: { - managerID: report.managerID, - }, - }, + successData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, @@ -5872,40 +5862,6 @@ function changeReportApprover(report: OnyxEntry, memberAccountID: number }, }, ], - }; - - const memberPersonalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs({accountIDs: [memberAccountID], currentUserAccountID}); - const params = { - reportID: report.reportID, - newApproverEmail: memberPersonalDetails?.at(0)?.login ?? '', - }; - - API.write(WRITE_COMMANDS.REROUTE_REPORT, params, onyxData); -} - -function assignCurrentUserAsApprover(report: OnyxEntry) { - if (!report?.reportID) { - return; - } - - const takeControlReportAction = buildOptimisticChangeApproverReportAction(currentUserAccountID); - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, - value: { - managerID: currentUserAccountID, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, - value: { - [takeControlReportAction.reportActionID]: takeControlReportAction, - }, - }, - ], failureData: [ { onyxMethod: Onyx.METHOD.MERGE, @@ -6043,6 +5999,5 @@ export { changeReportPolicyAndInviteSubmitter, removeFailedReport, openUnreportedExpense, - changeReportApprover, assignCurrentUserAsApprover, }; diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index a4c4f1704a736..17685cb3565ab 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -42,7 +42,7 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC } assignCurrentUserAsApprover(report); Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); - }, [selectedApproverType, reportPolicy, session?.accountID, report, reportID]); + }, [selectedApproverType, policy, session?.accountID, report, reportID]); const sections = useMemo(() => { const data = []; From ff8538f16a28a0586ce161dc371ad7106fa1bdfc Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 15:08:41 +0530 Subject: [PATCH 15/29] Update the Action preview --- src/languages/en.ts | 2 ++ src/languages/params.ts | 3 +++ src/libs/OptionsListUtils.ts | 3 +++ src/libs/ReportActionsUtils.ts | 11 ++++++++++- src/libs/ReportUtils.ts | 5 +++++ src/libs/SidebarUtils.ts | 3 +++ src/libs/actions/Report.ts | 8 ++++++-- .../report/ContextMenu/ContextMenuActions.tsx | 3 +++ src/pages/home/report/PureReportActionItem.tsx | 7 +++++++ src/types/onyx/OriginalMessage.ts | 15 ++++++++++++++- 10 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 14f18dd85be3d..b3412e3c52f52 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -46,6 +46,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -1349,6 +1350,7 @@ const translations = { subtitle: 'Choose an option to change the approver for this report.', description: 'You can also change the approver permanently for all reports in your ', workflowSettings: 'workflow settings', + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `changed the approver to `, actions: { addApprover: 'Add approver', addApproverSubtitle: 'Add an additional approver to the existing workflow.', diff --git a/src/languages/params.ts b/src/languages/params.ts index bfa7f92dca651..0e40d3c7517fa 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -849,6 +849,8 @@ type MergeFailureDescriptionGenericParams = { email: string; }; +type ChangedApproverMessageParams = {managerID: number}; + export type { ContactMethodsRouteParams, ContactMethodParams, @@ -1138,4 +1140,5 @@ export type { MergeSuccessDescriptionParams, MergeFailureUncreatedAccountDescriptionParams, MergeFailureDescriptionGenericParams, + ChangedApproverMessageParams, }; diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts index 5c83d764202c4..2435cb4ad2e08 100644 --- a/src/libs/OptionsListUtils.ts +++ b/src/libs/OptionsListUtils.ts @@ -61,6 +61,7 @@ import { isUserInvitedToWorkspace, } from './PolicyUtils'; import { + getChangedApproverActionMessage, getCombinedReportActions, getExportIntegrationLastMessageText, getIOUReportIDFromReportActionPreview, @@ -833,6 +834,8 @@ function getLastMessageTextForReport(report: OnyxEntry, lastActorDetails lastMessageTextFromReport = getRenamedAction(lastReportAction, isExpenseReport(report)); } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.DELETED_TRANSACTION)) { lastMessageTextFromReport = getDeletedTransactionMessage(lastReportAction); + } else if (isActionOfType(lastReportAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { + lastMessageTextFromReport = getChangedApproverActionMessage(lastReportAction); } // we do not want to show report closed in LHN for non archived report so use getReportLastMessage as fallback instead of lastMessageText from report diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index fd34ce56feaab..4226e3386f571 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -1489,7 +1489,6 @@ function isOldDotReportAction(action: ReportAction | OldDotReportAction) { CONST.REPORT.ACTIONS.TYPE.SELECTED_FOR_RANDOM_AUDIT, CONST.REPORT.ACTIONS.TYPE.SHARE, CONST.REPORT.ACTIONS.TYPE.STRIPE_PAID, - CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, CONST.REPORT.ACTIONS.TYPE.UNSHARE, CONST.REPORT.ACTIONS.TYPE.DELETED_ACCOUNT, CONST.REPORT.ACTIONS.TYPE.DONATION, @@ -2803,6 +2802,15 @@ function getUpdatedManualApprovalThresholdMessage(reportAction: OnyxEntry(reportAction: OnyxEntry) { + const {mentionedAccountIDs} = getOriginalMessage(reportAction as ReportAction) ?? {}; + + if (!mentionedAccountIDs?.length) { + return ''; + } + return translateLocal('iou.changeApprover.changedApproverMessage', {managerID: mentionedAccountIDs.at(0) ?? CONST.DEFAULT_NUMBER_ID}); +} + function isCardIssuedAction( reportAction: OnyxEntry, ): reportAction is ReportAction< @@ -3132,6 +3140,7 @@ export { getVacationer, getSubmittedTo, getReceiptScanFailedMessage, + getChangedApproverActionMessage, }; export type {LastVisibleMessage}; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5fcdd93400df3..321da1647cfa2 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -140,6 +140,7 @@ import { getActionableJoinRequestPendingReportAction, getAllReportActions, getCardIssuedMessage, + getChangedApproverActionMessage, getDismissedViolationMessageText, getExportIntegrationLastMessageText, getIntegrationSyncFailedMessage, @@ -5219,6 +5220,10 @@ function getReportNameInternal({ return getPolicyChangeMessage(parentReportAction); } + if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { + return getChangedApproverActionMessage(parentReportAction); + } + if (isMoneyRequestAction(parentReportAction)) { const originalMessage = getOriginalMessage(parentReportAction); const reportPolicy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index b9a78f64d3fae..9280865557b5a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -25,6 +25,7 @@ import { getAddedApprovalRuleMessage, getAddedConnectionMessage, getCardIssuedMessage, + getChangedApproverActionMessage, getDeletedApprovalRuleMessage, getIntegrationSyncFailedMessage, getLastVisibleMessage, @@ -784,6 +785,8 @@ function getOptionData({ result.alternateText = getReopenedMessage(); } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE)) { result.alternateText = getTravelUpdateMessage(lastAction); + } else if (isActionOfType(lastAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { + result.alternateText = getChangedApproverActionMessage(lastAction); } else { result.alternateText = lastMessageTextFromReport.length > 0 diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 46cc2bf41caf8..208dbca717da6 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -5858,7 +5858,9 @@ function assignCurrentUserAsApprover(report: OnyxEntry) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, value: { - [takeControlReportAction.reportActionID]: null, + [takeControlReportAction.reportActionID]: { + pendingAction: null, + }, }, }, ], @@ -5874,7 +5876,9 @@ function assignCurrentUserAsApprover(report: OnyxEntry) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, value: { - [takeControlReportAction.reportActionID]: null, + [takeControlReportAction.reportActionID]: { + pendingAction: null, + }, }, }, ], diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx index 34735c274a5bb..083c58196f245 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx @@ -26,6 +26,7 @@ import { getAddedApprovalRuleMessage, getAddedConnectionMessage, getCardIssuedMessage, + getChangedApproverActionMessage, getDeletedApprovalRuleMessage, getExportIntegrationMessageHTML, getIntegrationSyncFailedMessage, @@ -660,6 +661,8 @@ const ContextMenuActions: ContextMenuAction[] = [ setClipboardMessage(getUpdatedApprovalRuleMessage(reportAction)); } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD)) { setClipboardMessage(getUpdatedManualApprovalThresholdMessage(reportAction)); + } else if (isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { + setClipboardMessage(getChangedApproverActionMessage(reportAction)); } else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY) { const displayMessage = getPolicyChangeMessage(reportAction); Clipboard.setString(displayMessage); diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 55a96bf4e594c..60c4aaeb003a4 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -59,6 +59,7 @@ import { extractLinksFromMessageHtml, getAddedApprovalRuleMessage, getAddedConnectionMessage, + getChangedApproverActionMessage, getDeletedApprovalRuleMessage, getDemotedFromWorkspaceMessage, getDismissedViolationMessageText, @@ -1207,6 +1208,12 @@ function PureReportActionItem({ children = ; } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_MANUAL_APPROVAL_THRESHOLD)) { children = ; + } else if (isActionOfType(action, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) { + children = ( + + ${getChangedApproverActionMessage(action)}`} /> + + ); } else { const hasBeenFlagged = ![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && !isPendingRemove(action); diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index 48aff18b4d018..7aa1b8d3cb906 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -856,6 +856,18 @@ type OriginalMessageIntegrationMessage = { }; }; +/** + * Model of Take Control action original message + */ +type OriginalMessageTakeControl = { + /** Whether the action was taken on newDot or oldDot */ + isNewDot: boolean; + /** Time the action was created */ + lastModified: string; + /** Tagged account IDs of new approvers */ + mentionedAccountIDs: number[]; +}; + /** * Original message for CARD_ISSUED, CARD_MISSING_ADDRESS, CARD_ASSIGNED and CARD_ISSUED_VIRTUAL actions */ @@ -923,7 +935,7 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.TASK_COMPLETED]: never; [CONST.REPORT.ACTIONS.TYPE.TASK_EDITED]: never; [CONST.REPORT.ACTIONS.TYPE.TASK_REOPENED]: never; - [CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL]: never; + [CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL]: OriginalMessageTakeControl; [CONST.REPORT.ACTIONS.TYPE.TRAVEL_UPDATE]: OriginalMessageTravelUpdate; [CONST.REPORT.ACTIONS.TYPE.UNAPPROVED]: OriginalMessageUnapproved; [CONST.REPORT.ACTIONS.TYPE.UNHOLD]: never; @@ -948,6 +960,7 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.RETRACTED]: never; [CONST.REPORT.ACTIONS.TYPE.REOPENED]: never; [CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED]: never; + [CONST.REPORT.ACTIONS.TYPE.ACTION_REROUTE]: never; } & OldDotOriginalMessageMap & { [T in ValueOf]: OriginalMessagePolicyChangeLog; } & { From cdb218830938eadb562716c4025115542d722f50 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 15:13:53 +0530 Subject: [PATCH 16/29] Fix spellings --- src/languages/en.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b3412e3c52f52..1fae43a8f4bf6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5432,8 +5432,8 @@ const translations = { }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { title: 'Multiple approval levels', - description: 'Multiple aproval levels is a workflow tool for companies that require more than one person to approve a report before it can be reimbursed.', - onlyAvailableOnPlan: 'Multiple aproval levels are only available on the Control plan, starting at ', + description: 'Multiple approval levels is a workflow tool for companies that require more than one person to approve a report before it can be reimbursed.', + onlyAvailableOnPlan: 'Multiple approval levels are only available on the Control plan, starting at ', }, pricing: { perActiveMember: 'per active member per month.', From 1b6448dd81bde4f8ae3c5b0416dda43b6a2becb8 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 16:10:31 +0530 Subject: [PATCH 17/29] translations --- src/languages/de.ts | 24 ++++++++++++++++++++++ src/languages/en.ts | 5 +++-- src/languages/es.ts | 28 +++++++++++++++----------- src/languages/fr.ts | 24 ++++++++++++++++++++++ src/languages/it.ts | 24 ++++++++++++++++++++++ src/languages/ja.ts | 23 +++++++++++++++++++++ src/languages/nl.ts | 23 +++++++++++++++++++++ src/languages/params.ts | 2 ++ src/languages/pl.ts | 24 ++++++++++++++++++++++ src/languages/pt-BR.ts | 24 ++++++++++++++++++++++ src/languages/zh-hans.ts | 22 ++++++++++++++++++++ src/pages/ReportChangeApproverPage.tsx | 14 +++++++------ 12 files changed, 217 insertions(+), 20 deletions(-) diff --git a/src/languages/de.ts b/src/languages/de.ts index c9f6569c80479..04dc1ee7b7aa3 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1360,6 +1362,22 @@ const translations = { rates: 'Preise', submitsTo: ({name}: SubmitsToParams) => `Übermittelt an ${name}`, moveExpenses: () => ({one: 'Ausgabe verschieben', other: 'Ausgaben verschieben'}), + changeApprover: { + title: 'Genehmiger ändern', + subtitle: 'Wählen Sie eine Option, um den Genehmiger für diesen Bericht zu ändern.', + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `Sie können den Genehmiger auch dauerhaft für alle Berichte in Ihren Workflow-Einstellungen ändern.`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `änderte den Genehmiger zu `, + actions: { + addApprover: 'Genehmiger hinzufügen', + addApproverSubtitle: 'Fügen Sie dem bestehenden Workflow einen zusätzlichen Genehmiger hinzu.', + bypassApprovers: 'Genehmiger umgehen', + bypassApproversSubtitle: 'Weisen Sie sich selbst als endgültigen Genehmiger zu und überspringen Sie alle verbleibenden Genehmiger.', + }, + addApprover: { + subtitle: 'Wählen Sie einen zusätzlichen Genehmiger für diesen Bericht, bevor wir ihn durch den Rest des Genehmigungs-Workflows leiten.', + }, + }, }, transactionMerge: { listPage: { @@ -5436,6 +5454,12 @@ const translations = { 'Mehrstufige Tags helfen Ihnen, Ausgaben präziser zu verfolgen. Weisen Sie jedem Posten mehrere Tags zu – wie Abteilung, Kunde oder Kostenstelle – um den vollständigen Kontext jeder Ausgabe zu erfassen. Dies ermöglicht detailliertere Berichte, Genehmigungs-Workflows und Buchhaltungsexporte.', onlyAvailableOnPlan: 'Mehrstufige Tags sind nur im Control-Plan verfügbar, beginnend bei', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: 'Mehrere Genehmigungsstufen', + description: + 'Mehrere Genehmigungsstufen ist ein Workflow-Tool für Unternehmen, die mehr als eine Person benötigen, um einen Bericht zu genehmigen, bevor er erstattet werden kann.', + onlyAvailableOnPlan: 'Mehrere Genehmigungsstufen sind nur im Control-Plan verfügbar, beginnend bei ', + }, pricing: { perActiveMember: 'pro aktivem Mitglied pro Monat.', perMember: 'pro Mitglied pro Monat.', diff --git a/src/languages/en.ts b/src/languages/en.ts index 1fae43a8f4bf6..df260ba7938c5 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -275,6 +275,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1348,8 +1349,8 @@ const translations = { changeApprover: { title: 'Change approver', subtitle: 'Choose an option to change the approver for this report.', - description: 'You can also change the approver permanently for all reports in your ', - workflowSettings: 'workflow settings', + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `You can also change the approver permanently for all reports in your workflow settings.`, changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `changed the approver to `, actions: { addApprover: 'Add approver', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8d9d7d7da7b89..c33de4536f5f1 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -44,6 +44,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -273,6 +274,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1342,18 +1344,19 @@ const translations = { submitsTo: ({name}: SubmitsToParams) => `Se envía a ${name}`, moveExpenses: () => ({one: 'Mover gasto', other: 'Mover gastos'}), changeApprover: { - title: 'Change approver', - subtitle: 'Choose an option to change the approver for this report.', - description: 'You can also change the approver permanently for all reports in your ', - workflowSettings: 'workflow settings', + title: 'Cambiar aprobador', + subtitle: 'Elige una opción para cambiar el aprobador de este informe.', + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `También puedes cambiar el aprobador de forma permanente para todos los informes en tu configuración de flujo de trabajo.`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `cambió el aprobador a `, actions: { - addApprover: 'Add approver', - addApproverSubtitle: 'Add an additional approver to the existing workflow.', - bypassApprovers: 'Bypass approvers', - bypassApproversSubtitle: 'Assign yourself as final approver and skip any remaining approvers.', + addApprover: 'Añadir aprobador', + addApproverSubtitle: 'Añade un aprobador adicional al flujo de trabajo existente.', + bypassApprovers: 'Omitir aprobadores', + bypassApproversSubtitle: 'Asígnate como aprobador final y omite a los aprobadores restantes.', }, addApprover: { - subtitle: 'Choose an additional approver for this report before we route through the rest of the approval workflow', + subtitle: 'Elige un aprobador adicional para este informe antes de que lo enviemos por el resto del flujo de aprobación.', }, }, }, @@ -5462,9 +5465,10 @@ const translations = { onlyAvailableOnPlan: 'Las etiquetas multinivel solo están disponibles en el plan Control, a partir de ', }, [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { - title: 'Multiple approval levels', - description: 'Multiple aproval levels is a workflow tool for companies that require more than one person to approve a report before it can be reimbursed.', - onlyAvailableOnPlan: 'Multiple aproval levels are only available on the Control plan, starting at ', + title: 'Múltiples niveles de aprobación', + description: + 'Los múltiples niveles de aprobación son una herramienta de flujo de trabajo para empresas que requieren que más de una persona apruebe un informe antes de que pueda ser reembolsado.', + onlyAvailableOnPlan: 'Los múltiples niveles de aprobación solo están disponibles en el plan Controlar, a partir de ', }, note: { upgradeWorkspace: 'Mejore su espacio de trabajo para acceder a esta función, o', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 169aa040d5926..ad7760d5d738f 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1363,6 +1365,22 @@ const translations = { rates: 'Tarifs', submitsTo: ({name}: SubmitsToParams) => `Soumet à ${name}`, moveExpenses: () => ({one: 'Déplacer la dépense', other: 'Déplacer les dépenses'}), + changeApprover: { + title: "Modifier l'approbateur", + subtitle: "Choisissez une option pour modifier l'approbateur de ce rapport.", + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `Vous pouvez également modifier l'approbateur de manière permanente pour tous les rapports dans vos paramètres de flux de travail.`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `a changé l'approbateur en `, + actions: { + addApprover: 'Ajouter un approbateur', + addApproverSubtitle: 'Ajouter un approbateur supplémentaire au flux de travail existant.', + bypassApprovers: 'Contourner les approbateurs', + bypassApproversSubtitle: 'Vous désigner comme approbateur final et ignorer les autres approbateurs.', + }, + addApprover: { + subtitle: "Choisissez un approbateur supplémentaire pour ce rapport avant de le faire passer par le reste du flux de travail d'approbation.", + }, + }, }, transactionMerge: { listPage: { @@ -5449,6 +5467,12 @@ const translations = { "Les balises multi-niveaux vous aident à suivre les dépenses avec plus de précision. Assignez plusieurs balises à chaque poste—comme le département, le client ou le centre de coût—pour capturer le contexte complet de chaque dépense. Cela permet des rapports plus détaillés, des flux de travail d'approbation et des exportations comptables.", onlyAvailableOnPlan: 'Les balises multi-niveaux sont uniquement disponibles sur le plan Control, à partir de', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: "Niveaux d'approbation multiples", + description: + "Les niveaux d'approbation multiples sont un outil de flux de travail pour les entreprises qui exigent que plus d'une personne approuve un rapport avant qu'il ne puisse être remboursé.", + onlyAvailableOnPlan: "Les niveaux d'approbation multiples sont uniquement disponibles sur le plan Control, à partir de ", + }, pricing: { perActiveMember: 'par membre actif par mois.', perMember: 'par membre par mois.', diff --git a/src/languages/it.ts b/src/languages/it.ts index 5f0b012c870b4..5986220cc7102 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1356,6 +1358,22 @@ const translations = { rates: 'Tariffe', submitsTo: ({name}: SubmitsToParams) => `Invia a ${name}`, moveExpenses: () => ({one: 'Sposta spesa', other: 'Sposta spese'}), + changeApprover: { + title: 'Cambia approvatore', + subtitle: "Scegli un'opzione per cambiare l'approvatore di questo report.", + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `Puoi anche cambiare l'approvatore in modo permanente per tutti i report nelle tue impostazioni del flusso di lavoro.`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `ha cambiato l'approvatore in `, + actions: { + addApprover: 'Aggiungi approvatore', + addApproverSubtitle: 'Aggiungi un approvatore aggiuntivo al flusso di lavoro esistente.', + bypassApprovers: 'Ignora approvatori', + bypassApproversSubtitle: 'Assegna te stesso come approvatore finale e salta gli approvatori rimanenti.', + }, + addApprover: { + subtitle: 'Scegli un approvatore aggiuntivo per questo report prima di instradarlo attraverso il resto del flusso di lavoro di approvazione.', + }, + }, }, transactionMerge: { listPage: { @@ -5448,6 +5466,12 @@ const translations = { 'I tag multilivello ti aiutano a monitorare le spese con maggiore precisione. Assegna più tag a ciascuna voce, come reparto, cliente o centro di costo, per catturare il contesto completo di ogni spesa. Questo consente report più dettagliati, flussi di lavoro di approvazione ed esportazioni contabili.', onlyAvailableOnPlan: 'I tag multilivello sono disponibili solo nel piano Control, a partire da', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: 'Livelli di approvazione multipli', + description: + 'I livelli di approvazione multipli sono uno strumento di flusso di lavoro per le aziende che richiedono a più di una persona di approvare un report prima che possa essere rimborsato.', + onlyAvailableOnPlan: 'I livelli di approvazione multipli sono disponibili solo nel piano Control, a partire da ', + }, pricing: { perActiveMember: 'per membro attivo al mese.', perMember: 'per membro al mese.', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 2ec0368d31445..c3e4e31f65fb3 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1357,6 +1359,22 @@ const translations = { rates: '料金', submitsTo: ({name}: SubmitsToParams) => `${name}に送信`, moveExpenses: () => ({one: '経費を移動', other: '経費を移動'}), + changeApprover: { + title: '承認者を変更', + subtitle: 'このレポートの承認者を変更するオプションを選択してください。', + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `ワークフロー設定で、すべてのレポートの承認者を恒久的に変更することもできます。`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => ` に承認者を変更しました`, + actions: { + addApprover: '承認者を追加', + addApproverSubtitle: '既存のワークフローに承認者を追加します。', + bypassApprovers: '承認者をバイパス', + bypassApproversSubtitle: '最終承認者として自分自身を割り当て、残りの承認者をスキップします。', + }, + addApprover: { + subtitle: '承認ワークフローの残りの部分を経由する前に、このレポートの追加の承認者を選択してください。', + }, + }, }, transactionMerge: { listPage: { @@ -5418,6 +5436,11 @@ const translations = { 'マルチレベルタグは、経費をより正確に追跡するのに役立ちます。各項目に部門、クライアント、コストセンターなどの複数のタグを割り当てることで、すべての経費の完全なコンテキストを把握できます。これにより、より詳細なレポート作成、承認ワークフロー、および会計エクスポートが可能になります。', onlyAvailableOnPlan: 'マルチレベルタグは、Controlプランでのみ利用可能です。開始価格は', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: '複数の承認レベル', + description: '複数の承認レベルは、払い戻しが行われる前に複数の人がレポートを承認する必要がある企業向けのワークフローツールです。', + onlyAvailableOnPlan: '複数の承認レベルは、Controlプランでのみ利用可能です。料金は ', + }, pricing: { perActiveMember: 'アクティブメンバー1人あたり月額。', perMember: 'メンバーごとに月額。', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 860478854fdb1..93e84bbcec387 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1358,6 +1360,22 @@ const translations = { rates: 'Tarieven', submitsTo: ({name}: SubmitsToParams) => `Dient in bij ${name}`, moveExpenses: () => ({one: 'Verplaats uitgave', other: 'Verplaats uitgaven'}), + changeApprover: { + title: 'Goedkeurder wijzigen', + subtitle: 'Kies een optie om de goedkeurder voor dit rapport te wijzigen.', + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `U kunt de goedkeurder ook permanent wijzigen voor alle rapporten in uw workflow-instellingen.`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `wijzigde de goedkeurder naar `, + actions: { + addApprover: 'Goedkeurder toevoegen', + addApproverSubtitle: 'Voeg een extra goedkeurder toe aan de bestaande workflow.', + bypassApprovers: 'Goedkeurders omzeilen', + bypassApproversSubtitle: 'Wijs uzelf toe als definitieve goedkeurder en sla de resterende goedkeurders over.', + }, + addApprover: { + subtitle: 'Kies een extra goedkeurder voor dit rapport voordat we het via de rest van de goedkeuringsworkflow sturen.', + }, + }, }, transactionMerge: { listPage: { @@ -5446,6 +5464,11 @@ const translations = { 'Multi-Level Tags helpen je om uitgaven met grotere precisie bij te houden. Ken meerdere tags toe aan elk regelitem—zoals afdeling, klant of kostenplaats—om de volledige context van elke uitgave vast te leggen. Dit maakt gedetailleerdere rapportage, goedkeuringsworkflows en boekhouduitvoer mogelijk.', onlyAvailableOnPlan: 'Multi-level tags zijn alleen beschikbaar op het Control-plan, beginnend bij', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: 'Meerdere goedkeuringsniveaus', + description: 'Meerdere goedkeuringsniveaus is een workflowtool voor bedrijven die vereisen dat meer dan één persoon een rapport goedkeurt voordat het kan worden vergoed.', + onlyAvailableOnPlan: 'Meerdere goedkeuringsniveaus zijn alleen beschikbaar op het Control-plan, vanaf ', + }, pricing: { perActiveMember: 'per actief lid per maand.', perMember: 'per lid per maand.', diff --git a/src/languages/params.ts b/src/languages/params.ts index 0e40d3c7517fa..edd30d9696ca2 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -850,6 +850,7 @@ type MergeFailureDescriptionGenericParams = { }; type ChangedApproverMessageParams = {managerID: number}; +type WorkflowSettingsParam = {workflowSettingLink: string}; export type { ContactMethodsRouteParams, @@ -1141,4 +1142,5 @@ export type { MergeFailureUncreatedAccountDescriptionParams, MergeFailureDescriptionGenericParams, ChangedApproverMessageParams, + WorkflowSettingsParam, }; diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 442fcc821d757..2a85091c0b179 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1355,6 +1357,22 @@ const translations = { rates: 'Stawki', submitsTo: ({name}: SubmitsToParams) => `Przesyła do ${name}`, moveExpenses: () => ({one: 'Przenieś wydatek', other: 'Przenieś wydatki'}), + changeApprover: { + title: 'Zmień zatwierdzającego', + subtitle: 'Wybierz opcję, aby zmienić zatwierdzającego dla tego raportu.', + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `Możesz również trwale zmienić zatwierdzającego dla wszystkich raportów w swoich ustawieniach przepływu pracy.`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `zmieniono zatwierdzającego na `, + actions: { + addApprover: 'Dodaj zatwierdzającego', + addApproverSubtitle: 'Dodaj dodatkowego zatwierdzającego do istniejącego przepływu pracy.', + bypassApprovers: 'Pomiń zatwierdzających', + bypassApproversSubtitle: 'Przypisz siebie jako ostatecznego zatwierdzającego i pomiń pozostałych zatwierdzających.', + }, + addApprover: { + subtitle: 'Wybierz dodatkowego zatwierdzającego dla tego raportu, zanim poprowadzimy go przez resztę przepływu pracy zatwierdzania.', + }, + }, }, transactionMerge: { listPage: { @@ -5434,6 +5452,12 @@ const translations = { 'Wielopoziomowe tagi pomagają śledzić wydatki z większą precyzją. Przypisz wiele tagów do każdej pozycji, takich jak dział, klient czy centrum kosztów, aby uchwycić pełny kontekst każdego wydatku. Umożliwia to bardziej szczegółowe raportowanie, przepływy pracy związane z zatwierdzaniem oraz eksporty księgowe.', onlyAvailableOnPlan: 'Wielopoziomowe tagi są dostępne tylko w planie Control, zaczynając od', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: 'Wiele poziomów zatwierdzania', + description: + 'Wiele poziomów zatwierdzania to narzędzie workflow dla firm, które wymagają zatwierdzenia raportu przez więcej niż jedną osobę, zanim będzie mógł zostać zrefundowany.', + onlyAvailableOnPlan: 'Wiele poziomów zatwierdzania jest dostępnych tylko w planie Control, zaczynając od ', + }, pricing: { perActiveMember: 'na aktywnego członka miesięcznie.', perMember: 'za członka miesięcznie.', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 7d88357402d68..23812e2c183d4 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1356,6 +1358,22 @@ const translations = { rates: 'Taxas', submitsTo: ({name}: SubmitsToParams) => `Envia para ${name}`, moveExpenses: () => ({one: 'Mover despesa', other: 'Mover despesas'}), + changeApprover: { + title: 'Alterar aprovador', + subtitle: 'Escolha uma opção para alterar o aprovador deste relatório.', + description: ({workflowSettingLink}: WorkflowSettingsParam) => + `Você também pode alterar o aprovador permanentemente para todos os relatórios em suas configurações de fluxo de trabalho.`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `alterou o aprovador para `, + actions: { + addApprover: 'Adicionar aprovador', + addApproverSubtitle: 'Adicionar um aprovador adicional ao fluxo de trabalho existente.', + bypassApprovers: 'Ignorar aprovadores', + bypassApproversSubtitle: 'Atribua-se como aprovador final e pule quaisquer aprovadores restantes.', + }, + addApprover: { + subtitle: 'Escolha um aprovador adicional para este relatório antes de o encaminharmos através do restante do fluxo de trabalho de aprovação.', + }, + }, }, transactionMerge: { listPage: { @@ -5444,6 +5462,12 @@ const translations = { 'As Tags de Múltiplos Níveis ajudam você a rastrear despesas com maior precisão. Atribua várias tags a cada item de linha — como departamento, cliente ou centro de custo — para capturar o contexto completo de cada despesa. Isso permite relatórios mais detalhados, fluxos de trabalho de aprovação e exportações contábeis.', onlyAvailableOnPlan: 'As tags de múltiplos níveis estão disponíveis apenas no plano Control, a partir de', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: 'Vários níveis de aprovação', + description: + 'Vários níveis de aprovação são uma ferramenta de fluxo de trabalho para empresas que exigem que mais de uma pessoa aprove um relatório antes que ele possa ser reembolsado.', + onlyAvailableOnPlan: 'Vários níveis de aprovação estão disponíveis apenas no plano Control, a partir de ', + }, pricing: { perActiveMember: 'por membro ativo por mês.', perMember: 'por membro por mês.', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 6706e2d847b61..843a80e7ddeac 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -57,6 +57,7 @@ import type { CardInfoParams, CardNextPaymentParams, CategoryNameParams, + ChangedApproverMessageParams, ChangeFieldParams, ChangeOwnerDuplicateSubscriptionParams, ChangeOwnerHasFailedSettlementsParams, @@ -285,6 +286,7 @@ import type { WeSentYouMagicSignInLinkParams, WorkEmailMergingBlockedParams, WorkEmailResendCodeParams, + WorkflowSettingsParam, WorkspaceLockedPlanTypeParams, WorkspaceMemberList, WorkspaceOwnerWillNeedToAddOrUpdatePaymentCardParams, @@ -1342,6 +1344,21 @@ const translations = { rates: '费率', submitsTo: ({name}: SubmitsToParams) => `提交给${name}`, moveExpenses: () => ({one: '移动费用', other: '移动费用'}), + changeApprover: { + title: '更改审批人', + subtitle: '选择一个选项来更改此报告的审批人。', + description: ({workflowSettingLink}: WorkflowSettingsParam) => `您也可以在[工作流设置中永久更改所有报告的审批人。`, + changedApproverMessage: ({managerID}: ChangedApproverMessageParams) => `将审批人更改为 `, + actions: { + addApprover: '添加审批人', + addApproverSubtitle: '为现有工作流添加一个额外的审批人。', + bypassApprovers: '跳过审批人', + bypassApproversSubtitle: '将自己指定为最终审批人并跳过任何剩余的审批人。', + }, + addApprover: { + subtitle: '在我们将此报告路由到其余审批工作流之前,为此报告选择一个额外的审批人。', + }, + }, }, transactionMerge: { listPage: { @@ -5351,6 +5368,11 @@ const translations = { description: '多级标签帮助您更精确地跟踪费用。为每个项目分配多个标签,例如部门、客户或成本中心,以捕获每笔费用的完整上下文。这使得更详细的报告、审批流程和会计导出成为可能。', onlyAvailableOnPlan: '多级标签仅在Control计划中提供,起价为', }, + [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: { + title: '多级审批', + description: '多级审批是一种工作流工具,适用于要求一人以上审批报销单后才能进行报销的公司。', + onlyAvailableOnPlan: '多级审批仅在 Control 套餐上提供,起价为 ', + }, pricing: { perActiveMember: '每位活跃成员每月。', perMember: '每位成员每月。', diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 17685cb3565ab..5b33504cf4bdc 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -1,14 +1,16 @@ import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; -import TextLink from '@components/TextLink'; +import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {assignCurrentUserAsApprover, changeReportApprover} from '@libs/actions/Report'; +import {assignCurrentUserAsApprover} from '@libs/actions/Report'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; @@ -28,6 +30,7 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC const reportID = report?.reportID; const {translate} = useLocalize(); const styles = useThemeStyles(); + const {environmentURL} = useEnvironment(); const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false}); const [selectedApproverType, setSelectedApproverType] = useState(); @@ -88,10 +91,9 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC customListHeader={ <> {translate('iou.changeApprover.subtitle')} - - {translate('iou.changeApprover.description')} - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policy?.id))}>{translate('iou.changeApprover.workflowSettings')}. - + + + } /> From 047d2fb3fd95107c78d3f2207fcabe8806213a85 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 16:14:08 +0530 Subject: [PATCH 18/29] Remove comment --- src/components/MoneyReportHeader.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 878d2c8656e85..abb8989dfa932 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -924,7 +924,6 @@ function MoneyReportHeader({ icon: Expensicons.Workflows, value: CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER, onSelected: () => { - console.debug('[MoneyReportHeader] Change Approver selected', moneyRequestReport); if (!moneyRequestReport) { return; } From 4467066237b42c076bf2d04fb1e62e55572902e3 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 12 Aug 2025 16:44:56 +0530 Subject: [PATCH 19/29] Fix rendering on native --- src/libs/ReportUtils.ts | 6 ++++++ src/pages/ReportChangeApproverPage.tsx | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 321da1647cfa2..e6fbd23104e5d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7565,6 +7565,7 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio } function buildOptimisticChangeApproverReportAction(managerID: number): OptimisticChangedApproverReportAction { + const created = DateUtils.getDBTime(); return { actionName: managerID === currentUserAccountID ? CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL : CONST.REPORT.ACTIONS.TYPE.ACTION_REROUTE, actorAccountID: currentUserAccountID, @@ -7584,6 +7585,11 @@ function buildOptimisticChangeApproverReportAction(managerID: number): Optimisti text: getCurrentUserDisplayNameOrEmail(), }, ], + originalMessage: { + isNewDot: true, + lastModified: created, + mentionedAccountIDs: [managerID], + }, shouldShow: false, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, reportActionID: rand64(), diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 5b33504cf4bdc..4ec8d7f9750a6 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -91,7 +91,7 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC customListHeader={ <> {translate('iou.changeApprover.subtitle')} - + From ee5aaa8a0e7be251906f4725a79740bd5b2f774b Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 13 Aug 2025 17:34:43 +0530 Subject: [PATCH 20/29] Updates --- src/libs/API/parameters/AssignReportToMeParams.ts | 3 +++ src/libs/ReportSecondaryActionUtils.ts | 1 + src/libs/actions/Report.ts | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/libs/API/parameters/AssignReportToMeParams.ts b/src/libs/API/parameters/AssignReportToMeParams.ts index 6669185bcead1..920b5310e6c30 100644 --- a/src/libs/API/parameters/AssignReportToMeParams.ts +++ b/src/libs/API/parameters/AssignReportToMeParams.ts @@ -1,6 +1,9 @@ type AssignReportToMeParams = { /** Expense reportID */ reportID: string; + + /** Action ID for optimistic took control action */ + reportActionID?: string; }; export default AssignReportToMeParams; diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index 9c5ef326ee50a..b20f018270c8f 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -670,6 +670,7 @@ function getSecondaryReportActions({ options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); } + // @todo we will remove checking whether currrent manager is admin in PR #68353 // When report manager is not the policy admin and current user is policy admin, allow changing the approver if (!isMemberPolicyAdmin(policy, getManagerAccountEmail(policy, report)) && isExpenseReportUtils(report) && isProcessingReportUtils(report) && isPolicyAdmin(policy)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER); diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 208dbca717da6..d7e94321c07d3 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -15,6 +15,7 @@ import type { AddCommentOrAttachmentParams, AddEmojiReactionParams, AddWorkspaceRoomParams, + AssignReportToMeParams, CompleteGuidedSetupParams, DeleteAppReportParams, DeleteCommentParams, @@ -5884,8 +5885,9 @@ function assignCurrentUserAsApprover(report: OnyxEntry) { ], }; - const params = { + const params: AssignReportToMeParams = { reportID: report.reportID, + reportActionID: takeControlReportAction.reportActionID, }; API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); From f3f202c22cf8b90017932d86c1a98f13aaf07a80 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 13 Aug 2025 17:43:39 +0530 Subject: [PATCH 21/29] Fix spell --- src/libs/ReportSecondaryActionUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index a87e836c6a71a..5a589ab4bc65c 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -682,7 +682,7 @@ function getSecondaryReportActions({ options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE); } - // @todo we will remove checking whether currrent manager is admin in PR #68353 + // @todo we will remove checking whether current manager is admin in PR #68353 // When report manager is not the policy admin and current user is policy admin, allow changing the approver if (!isMemberPolicyAdmin(policy, getManagerAccountEmail(policy, report)) && isExpenseReportUtils(report) && isProcessingReportUtils(report) && isPolicyAdmin(policy)) { options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER); From 37eab51f2589717f127b2b1d223ec00ee8c9e19d Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 19 Aug 2025 00:10:11 +0530 Subject: [PATCH 22/29] Refactor and cleanup --- src/components/MoneyReportHeader.tsx | 2 ++ src/libs/API/parameters/AssignReportToMeParams.ts | 2 +- src/libs/actions/Report.ts | 6 +++--- src/pages/ReportChangeApproverPage.tsx | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index 44fda82ce87b6..53ae70f3f2c9f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -23,6 +23,7 @@ import {deleteAppReport, downloadReportPDF, exportReportToCSV, exportReportToPDF import {queueExportSearchWithTemplate} from '@libs/actions/Search'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import getPlatform from '@libs/getPlatform'; +import Log from '@libs/Log'; import {getThreadReportIDsForTransactions, getTotalAmountForIOUReportPreviewButton} from '@libs/MoneyRequestReportUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types'; @@ -967,6 +968,7 @@ function MoneyReportHeader({ value: CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER, onSelected: () => { if (!moneyRequestReport) { + Log.warn('Change approver secondary action triggered without moneyRequestReport data.'); return; } Navigation.navigate(ROUTES.REPORT_CHANGE_APPROVER.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute())); diff --git a/src/libs/API/parameters/AssignReportToMeParams.ts b/src/libs/API/parameters/AssignReportToMeParams.ts index 920b5310e6c30..5c9b46821c88d 100644 --- a/src/libs/API/parameters/AssignReportToMeParams.ts +++ b/src/libs/API/parameters/AssignReportToMeParams.ts @@ -3,7 +3,7 @@ type AssignReportToMeParams = { reportID: string; /** Action ID for optimistic took control action */ - reportActionID?: string; + reportActionID: string; }; export default AssignReportToMeParams; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 3b5ff291f8c5d..6b82af64d3dc5 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -5888,19 +5888,19 @@ function resolveConciergeCategoryOptions(reportID: string | undefined, actionRep } as Partial); } -function assignCurrentUserAsApprover(report: OnyxEntry) { +function assignCurrentUserAsApprover(report: OnyxEntry, accountID: number) { if (!report?.reportID) { return; } - const takeControlReportAction = buildOptimisticChangeApproverReportAction(currentUserAccountID); + const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID); const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, value: { - managerID: currentUserAccountID, + managerID: accountID, }, }, { diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 4ec8d7f9750a6..5180e982f8048 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -43,7 +43,7 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC if (!isPolicyAdmin(policy) || !policy || !session?.accountID) { return; } - assignCurrentUserAsApprover(report); + assignCurrentUserAsApprover(report, session.accountID); Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); }, [selectedApproverType, policy, session?.accountID, report, reportID]); From 5e530b3ae74de7b505d1baad9f04cf19a8e73a91 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 19 Aug 2025 00:11:56 +0530 Subject: [PATCH 23/29] Remove ACTION_REROUTE --- src/CONST/index.ts | 1 - src/libs/ReportActionsUtils.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/types/onyx/OriginalMessage.ts | 1 - 4 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index d90f04fd9d411..7b9bd8d0ce021 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -1188,7 +1188,6 @@ const CONST = { RETRACTED: 'RETRACTED', REOPENED: 'REOPENED', REPORT_PREVIEW: 'REPORTPREVIEW', - ACTION_REROUTE: 'ACTIONREROUTE', SELECTED_FOR_RANDOM_AUDIT: 'SELECTEDFORRANDOMAUDIT', // OldDot Action SHARE: 'SHARE', // OldDot Action STRIPE_PAID: 'STRIPEPAID', // OldDot Action diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 35db4439d4484..077935aad6738 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -2802,7 +2802,7 @@ function getUpdatedManualApprovalThresholdMessage(reportAction: OnyxEntry(reportAction: OnyxEntry) { +function getChangedApproverActionMessage(reportAction: OnyxEntry) { const {mentionedAccountIDs} = getOriginalMessage(reportAction as ReportAction) ?? {}; if (!mentionedAccountIDs?.length) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index da9127015d86d..549ea9ee3c21b 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7579,7 +7579,7 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio function buildOptimisticChangeApproverReportAction(managerID: number): OptimisticChangedApproverReportAction { const created = DateUtils.getDBTime(); return { - actionName: managerID === currentUserAccountID ? CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL : CONST.REPORT.ACTIONS.TYPE.ACTION_REROUTE, + actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, actorAccountID: currentUserAccountID, avatar: getCurrentUserAvatar(), created: DateUtils.getDBTime(), diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts index a7f3cb7cbae5e..05eb81f3ba73e 100644 --- a/src/types/onyx/OriginalMessage.ts +++ b/src/types/onyx/OriginalMessage.ts @@ -966,7 +966,6 @@ type OriginalMessageMap = { [CONST.REPORT.ACTIONS.TYPE.RETRACTED]: never; [CONST.REPORT.ACTIONS.TYPE.REOPENED]: never; [CONST.REPORT.ACTIONS.TYPE.RECEIPT_SCAN_FAILED]: never; - [CONST.REPORT.ACTIONS.TYPE.ACTION_REROUTE]: never; } & OldDotOriginalMessageMap & { [T in ValueOf]: OriginalMessagePolicyChangeLog; } & { From 720a622020d4b5178ab125bebba98a8212cb6f7c Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 19 Aug 2025 00:59:27 +0530 Subject: [PATCH 24/29] Add optimistic next step --- src/libs/actions/IOU.ts | 78 ++++++++++++++++++++++++++ src/libs/actions/Report.ts | 66 ---------------------- src/pages/ReportChangeApproverPage.tsx | 2 +- 3 files changed, 79 insertions(+), 67 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 7786798360854..797f01470a05c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -11,6 +11,7 @@ import type {SearchQueryJSON} from '@components/Search/types'; import * as API from '@libs/API'; import type { ApproveMoneyRequestParams, + AssignReportToMeParams, CategorizeTrackedExpenseParams as CategorizeTrackedExpenseApiParams, CompleteSplitBillParams, CreateDistanceRequestParams, @@ -109,6 +110,7 @@ import { buildOptimisticAddCommentReportAction, buildOptimisticApprovedReportAction, buildOptimisticCancelPaymentReportAction, + buildOptimisticChangeApproverReportAction, buildOptimisticChatReport, buildOptimisticCreatedReportAction, buildOptimisticDetachReceipt, @@ -12178,6 +12180,81 @@ function saveSplitTransactions(draftTransaction: OnyxEntry, accountID: number) { + if (!report?.reportID) { + return; + } + + const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID); + const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${report?.reportID}`] ?? null; + const optimisticNextStep = buildNextStep({...report, managerID: accountID}, CONST.REPORT.STATUS_NUM.SUBMITTED, false, true); + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: { + managerID: accountID, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: { + [takeControlReportAction.reportActionID]: takeControlReportAction, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${report.reportID}`, + value: optimisticNextStep, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: { + [takeControlReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, + value: { + managerID: report.managerID, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, + value: { + [takeControlReportAction.reportActionID]: { + pendingAction: null, + }, + }, + }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.NEXT_STEP}${report?.reportID}`, + value: currentNextStep, + }, + ], + }; + + const params: AssignReportToMeParams = { + reportID: report.reportID, + reportActionID: takeControlReportAction.reportActionID, + }; + + API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); +} + export { adjustRemainingSplitShares, approveMoneyRequest, @@ -12284,5 +12361,6 @@ export { reopenReport, retractReport, startDistanceRequest, + assignCurrentUserAsApprover, }; export type {GPSPoint as GpsPoint, IOURequestType, StartSplitBilActionParams, CreateTrackExpenseParams, RequestMoneyInformation, ReplaceReceipt}; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 6b82af64d3dc5..f20705aa5d2ac 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -15,7 +15,6 @@ import type { AddCommentOrAttachmentParams, AddEmojiReactionParams, AddWorkspaceRoomParams, - AssignReportToMeParams, CompleteGuidedSetupParams, DeleteAppReportParams, DeleteCommentParams, @@ -100,7 +99,6 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import type {OptimisticAddCommentReportAction, OptimisticChatReport, SelfDMParameters} from '@libs/ReportUtils'; import { buildOptimisticAddCommentReportAction, - buildOptimisticChangeApproverReportAction, buildOptimisticChangeFieldAction, buildOptimisticChangePolicyReportAction, buildOptimisticChatReport, @@ -203,7 +201,6 @@ import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {ConnectionName} from '@src/types/onyx/Policy'; import type {NotificationPreference, Participants, Participant as ReportParticipant, RoomVisibility, WriteCapability} from '@src/types/onyx/Report'; import type {Message, ReportActions} from '@src/types/onyx/ReportAction'; -import type {OnyxData} from '@src/types/onyx/Request'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import {clearByKey} from './CachedPDFPaths'; import {setDownload} from './Download'; @@ -5888,68 +5885,6 @@ function resolveConciergeCategoryOptions(reportID: string | undefined, actionRep } as Partial); } -function assignCurrentUserAsApprover(report: OnyxEntry, accountID: number) { - if (!report?.reportID) { - return; - } - - const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID); - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, - value: { - managerID: accountID, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, - value: { - [takeControlReportAction.reportActionID]: takeControlReportAction, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, - value: { - [takeControlReportAction.reportActionID]: { - pendingAction: null, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, - value: { - managerID: report.managerID, - }, - }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, - value: { - [takeControlReportAction.reportActionID]: { - pendingAction: null, - }, - }, - }, - ], - }; - - const params: AssignReportToMeParams = { - reportID: report.reportID, - reportActionID: takeControlReportAction.reportActionID, - }; - - API.write(WRITE_COMMANDS.ASSIGN_REPORT_TO_ME, params, onyxData); -} - export type {Video, GuidedSetupData, TaskForParameters, IntroSelected}; export { @@ -6062,5 +5997,4 @@ export { changeReportPolicyAndInviteSubmitter, removeFailedReport, openUnreportedExpense, - assignCurrentUserAsApprover, }; diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 5180e982f8048..4e9ea76b1b81b 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -10,7 +10,7 @@ import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {assignCurrentUserAsApprover} from '@libs/actions/Report'; +import {assignCurrentUserAsApprover} from '@libs/actions/IOU'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; From a5f437507322247844a9e59256d89afc0ad6c7d1 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 19 Aug 2025 01:20:59 +0530 Subject: [PATCH 25/29] Remove extra route --- src/ROUTES.ts | 4 ---- src/SCREENS.ts | 1 - src/libs/Navigation/linkingConfig/config.ts | 1 - 3 files changed, 6 deletions(-) diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b651dbfdf1855..dd17d02407141 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -516,10 +516,6 @@ const ROUTES = { route: 'r/:reportID/change-approver', getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-approver` as const, backTo), }, - REPORT_CHANGE_APPROVER_ADD_APPROVER: { - route: 'r/:reportID/change-approver/add', - getRoute: (reportID: string, backTo?: string) => getUrlWithBackToParam(`r/${reportID}/change-approver/add` as const, backTo), - }, SPLIT_BILL_DETAILS: { route: 'r/:reportID/split/:reportActionID', getRoute: (reportID: string | undefined, reportActionID: string, backTo?: string) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index cc012c958a83f..31045e7e61ed0 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -765,7 +765,6 @@ const SCREENS = { }, REPORT_CHANGE_APPROVER: { ROOT: 'Report_Change_Approver_Root', - ADD_APPROVER: 'Report_Change_Approver_Add_Approver', }, TEST_TOOLS_MODAL: { ROOT: 'TestToolsModal_Root', diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index add012baf65ba..2600b7aa87680 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1604,7 +1604,6 @@ const config: LinkingOptions['config'] = { [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: { screens: { [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: ROUTES.REPORT_CHANGE_APPROVER.route, - [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: ROUTES.REPORT_CHANGE_APPROVER_ADD_APPROVER.route, }, }, }, From 83467982859b1cb4a49ecc2b5e7bcedb4e7e330a Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 19 Aug 2025 01:25:18 +0530 Subject: [PATCH 26/29] Remove external data dependency on build optimistic action function --- src/libs/ReportUtils.ts | 4 ++-- src/libs/actions/IOU.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 549ea9ee3c21b..26b217a7bf020 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -7576,11 +7576,11 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio }; } -function buildOptimisticChangeApproverReportAction(managerID: number): OptimisticChangedApproverReportAction { +function buildOptimisticChangeApproverReportAction(managerID: number, actorAccountID: number): OptimisticChangedApproverReportAction { const created = DateUtils.getDBTime(); return { actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL, - actorAccountID: currentUserAccountID, + actorAccountID, avatar: getCurrentUserAvatar(), created: DateUtils.getDBTime(), message: [ diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 797f01470a05c..9414a3ca403aa 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12185,7 +12185,7 @@ function assignCurrentUserAsApprover(report: OnyxEntry, accoun return; } - const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID); + const takeControlReportAction = buildOptimisticChangeApproverReportAction(accountID, accountID); const currentNextStep = allNextSteps[`${ONYXKEYS.COLLECTION.NEXT_STEP}${report?.reportID}`] ?? null; const optimisticNextStep = buildNextStep({...report, managerID: accountID}, CONST.REPORT.STATUS_NUM.SUBMITTED, false, true); From 8b343d7ce39738963f3f9b0728209724433f57e2 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 19 Aug 2025 13:59:40 +0530 Subject: [PATCH 27/29] Check correct manager of the report --- src/libs/PolicyUtils.ts | 5 ++++- src/libs/ReportSecondaryActionUtils.ts | 9 +++++++-- src/pages/ReportChangeApproverPage.tsx | 6 ++++-- 3 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index f08e97488d08c..bd43b880cd90d 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1514,7 +1514,10 @@ function isUserInvitedToWorkspace(): boolean { ); } -function isMemberPolicyAdmin(policy: OnyxEntry, memberEmail: string): boolean { +function isMemberPolicyAdmin(policy: OnyxEntry, memberEmail: string | undefined): boolean { + if (!policy || !memberEmail) { + return false; + } const admins = getAdminEmployees(policy); return admins.some((admin) => admin.email === memberEmail); } diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts index b1f4ade9e4a41..2878d7036c111 100644 --- a/src/libs/ReportSecondaryActionUtils.ts +++ b/src/libs/ReportSecondaryActionUtils.ts @@ -5,11 +5,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {ExportTemplate, Policy, Report, ReportAction, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; import {isApprover as isApproverUtils} from './actions/Policy/Member'; import {getCurrentUserAccountID, getCurrentUserEmail} from './actions/Report'; +import {getLoginByAccountID} from './PersonalDetailsUtils'; import { arePaymentsEnabled as arePaymentsEnabledUtils, getConnectedIntegration, getCorrectedAutoReportingFrequency, - getManagerAccountEmail, getSubmitToAccountID, getValidConnectedIntegration, hasIntegrationAutoSync, @@ -683,7 +683,12 @@ function getSecondaryReportActions({ // @todo we will remove checking whether current manager is admin in PR #68353 // When report manager is not the policy admin and current user is policy admin, allow changing the approver - if (!isMemberPolicyAdmin(policy, getManagerAccountEmail(policy, report)) && isExpenseReportUtils(report) && isProcessingReportUtils(report) && isPolicyAdmin(policy)) { + if ( + !isMemberPolicyAdmin(policy, getLoginByAccountID(report.managerID ?? CONST.DEFAULT_NUMBER_ID)) && + isExpenseReportUtils(report) && + isProcessingReportUtils(report) && + isPolicyAdmin(policy) + ) { options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER); } diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 4e9ea76b1b81b..457e87050cab4 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -14,8 +14,10 @@ import {assignCurrentUserAsApprover} from '@libs/actions/IOU'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; -import {getManagerAccountEmail, isMemberPolicyAdmin, isPolicyAdmin} from '@libs/PolicyUtils'; +import {getLoginByAccountID} from '@libs/PersonalDetailsUtils'; +import {isMemberPolicyAdmin, isPolicyAdmin} from '@libs/PolicyUtils'; import {isMoneyRequestReport, isMoneyRequestReportPendingDeletion} from '@libs/ReportUtils'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -50,7 +52,7 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC const sections = useMemo(() => { const data = []; - if (!isMemberPolicyAdmin(policy, getManagerAccountEmail(policy, report))) { + if (!isMemberPolicyAdmin(policy, getLoginByAccountID(report.managerID ?? CONST.DEFAULT_NUMBER_ID))) { data.push({ text: translate('iou.changeApprover.actions.bypassApprovers'), keyForList: 'bypassApprover', From d22cce196419eb83681fd165edd89b75315cc46e Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Tue, 19 Aug 2025 14:11:55 +0530 Subject: [PATCH 28/29] Fix types --- src/libs/Navigation/types.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e294a3d0cabef..4eaf29f2dcbe4 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -2271,9 +2271,6 @@ type ReportChangeApproverParamList = { [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: { reportID: string; }; - [SCREENS.REPORT_CHANGE_APPROVER.ADD_APPROVER]: { - reportID: string; - }; }; type TestToolsModalModalNavigatorParamList = { From a5567b288840c4e8caf7e11d60ebbcc2d24e0137 Mon Sep 17 00:00:00 2001 From: Rajat Parashar Date: Wed, 20 Aug 2025 02:28:14 +0530 Subject: [PATCH 29/29] Rename action --- src/libs/actions/IOU.ts | 4 ++-- src/pages/ReportChangeApproverPage.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 623161272d5ae..7baf6a68d41fb 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -12391,7 +12391,7 @@ function saveSplitTransactions(draftTransaction: OnyxEntry, accountID: number) { +function assignReportToMe(report: OnyxEntry, accountID: number) { if (!report?.reportID) { return; } @@ -12574,7 +12574,7 @@ export { reopenReport, retractReport, startDistanceRequest, - assignCurrentUserAsApprover, + assignReportToMe, clearSplitTransactionDraftErrors, }; export type {GPSPoint as GpsPoint, IOURequestType, StartSplitBilActionParams, CreateTrackExpenseParams, RequestMoneyInformation, ReplaceReceipt}; diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx index 457e87050cab4..afe0cff9ada4e 100644 --- a/src/pages/ReportChangeApproverPage.tsx +++ b/src/pages/ReportChangeApproverPage.tsx @@ -10,7 +10,7 @@ import useEnvironment from '@hooks/useEnvironment'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; import useThemeStyles from '@hooks/useThemeStyles'; -import {assignCurrentUserAsApprover} from '@libs/actions/IOU'; +import {assignReportToMe} from '@libs/actions/IOU'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {ReportChangeApproverParamList} from '@libs/Navigation/types'; @@ -45,7 +45,7 @@ function ReportChangeApproverPage({report, policy, isLoadingReportData}: ReportC if (!isPolicyAdmin(policy) || !policy || !session?.accountID) { return; } - assignCurrentUserAsApprover(report, session.accountID); + assignReportToMe(report, session.accountID); Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID)); }, [selectedApproverType, policy, session?.accountID, report, reportID]);