diff --git a/src/CONST/index.ts b/src/CONST/index.ts
index 0c80e2ae5747c..8c19cbb6e3cc7 100755
--- a/src/CONST/index.ts
+++ b/src/CONST/index.ts
@@ -1083,6 +1083,7 @@ const CONST = {
HOLD: 'hold',
DOWNLOAD_PDF: 'downloadPDF',
CHANGE_WORKSPACE: 'changeWorkspace',
+ CHANGE_APPROVER: 'changeApprover',
VIEW_DETAILS: 'viewDetails',
DELETE: 'delete',
RETRACT: 'retract',
@@ -6712,6 +6713,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 733f55e84e5b4..fc327c404852f 100644
--- a/src/ROUTES.ts
+++ b/src/ROUTES.ts
@@ -509,6 +509,10 @@ 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),
+ },
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 6d786f4263c8c..4d9043da65a09 100644
--- a/src/SCREENS.ts
+++ b/src/SCREENS.ts
@@ -228,6 +228,7 @@ const SCREENS = {
DEBUG: 'Debug',
ADD_UNREPORTED_EXPENSE: 'AddUnreportedExpense',
SCHEDULE_CALL: 'ScheduleCall',
+ REPORT_CHANGE_APPROVER: 'Report_Change_Approver',
MERGE_TRANSACTION: 'MergeTransaction',
},
PUBLIC_CONSOLE_DEBUG: 'Console_Debug',
@@ -767,7 +768,9 @@ const SCREENS = {
BOOK: 'ScheduleCall_Book',
CONFIRMATION: 'ScheduleCall_Confirmation',
},
-
+ REPORT_CHANGE_APPROVER: {
+ ROOT: 'Report_Change_Approver_Root',
+ },
TEST_TOOLS_MODAL: {
ROOT: 'TestToolsModal_Root',
},
diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index fbbe13c5740d9..976d1c632b714 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, {navigationRef} from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
@@ -969,6 +970,18 @@ function MoneyReportHeader({
Navigation.navigate(ROUTES.REPORT_WITH_ID_CHANGE_WORKSPACE.getRoute(moneyRequestReport.reportID, Navigation.getActiveRoute()));
},
},
+ [CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER]: {
+ text: translate('iou.changeApprover.title'),
+ icon: Expensicons.Workflows,
+ 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()));
+ },
+ },
[CONST.REPORT.SECONDARY_ACTIONS.DELETE]: {
text: translate('common.delete'),
icon: Expensicons.Trashcan,
diff --git a/src/languages/de.ts b/src/languages/de.ts
index 7e0b0ab0b9328..5c0fe3af7ba2b 100644
--- a/src/languages/de.ts
+++ b/src/languages/de.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1385,6 +1387,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: {
@@ -5485,6 +5503,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 d284b72324256..f1dd43658dfb5 100755
--- a/src/languages/en.ts
+++ b/src/languages/en.ts
@@ -47,6 +47,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -281,6 +282,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1369,6 +1371,22 @@ 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: ({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',
+ 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.',
+ },
+ },
},
transactionMerge: {
listPage: {
@@ -5463,6 +5481,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 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.',
perMember: 'per member per month.',
diff --git a/src/languages/es.ts b/src/languages/es.ts
index 63366806843c4..082eb66a269f7 100644
--- a/src/languages/es.ts
+++ b/src/languages/es.ts
@@ -45,6 +45,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -280,6 +281,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1366,6 +1368,22 @@ const translations = {
rates: 'Tasas',
submitsTo: ({name}: SubmitsToParams) => `Se envía a ${name}`,
moveExpenses: () => ({one: 'Mover gasto', other: 'Mover gastos'}),
+ changeApprover: {
+ 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: '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: 'Elige un aprobador adicional para este informe antes de que lo enviemos por el resto del flujo de aprobación.',
+ },
+ },
},
transactionMerge: {
listPage: {
@@ -5496,6 +5514,12 @@ 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: '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',
learnMore: 'más información',
diff --git a/src/languages/fr.ts b/src/languages/fr.ts
index 4070f5ed023e9..aef6f5e511866 100644
--- a/src/languages/fr.ts
+++ b/src/languages/fr.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1388,6 +1390,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: {
@@ -5499,6 +5517,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 30d1bf50b7224..f3e31b20fa10a 100644
--- a/src/languages/it.ts
+++ b/src/languages/it.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1381,6 +1383,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: {
@@ -5498,6 +5516,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 a9427ceced914..50d094fe98214 100644
--- a/src/languages/ja.ts
+++ b/src/languages/ja.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1382,6 +1384,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: {
@@ -5468,6 +5486,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 05c2daa8a988a..aabcb2c71b101 100644
--- a/src/languages/nl.ts
+++ b/src/languages/nl.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1383,6 +1385,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: {
@@ -5496,6 +5514,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 948875a640859..3b8a6051d206a 100644
--- a/src/languages/params.ts
+++ b/src/languages/params.ts
@@ -859,6 +859,10 @@ type MergeFailureDescriptionGenericParams = {
email: string;
};
+type ChangedApproverMessageParams = {managerID: number};
+
+type WorkflowSettingsParam = {workflowSettingLink: string};
+
type IndividualExpenseRulesSubtitleParams = {
categoriesPageLink: string;
tagsPageLink: string;
@@ -1179,6 +1183,8 @@ export type {
MergeSuccessDescriptionParams,
MergeFailureUncreatedAccountDescriptionParams,
MergeFailureDescriptionGenericParams,
+ ChangedApproverMessageParams,
+ WorkflowSettingsParam,
MovedActionParams,
IndividualExpenseRulesSubtitleParams,
BillableDefaultDescriptionParams,
diff --git a/src/languages/pl.ts b/src/languages/pl.ts
index ba7fb2cb5a52a..d1acfbcd4ad87 100644
--- a/src/languages/pl.ts
+++ b/src/languages/pl.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1380,6 +1382,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: {
@@ -5484,6 +5502,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 f8a24f5cadf78..593093dcf978a 100644
--- a/src/languages/pt-BR.ts
+++ b/src/languages/pt-BR.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1381,6 +1383,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: {
@@ -5494,6 +5512,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 bff500a0be9c3..20f9408013821 100644
--- a/src/languages/zh-hans.ts
+++ b/src/languages/zh-hans.ts
@@ -58,6 +58,7 @@ import type {
CardInfoParams,
CardNextPaymentParams,
CategoryNameParams,
+ ChangedApproverMessageParams,
ChangeFieldParams,
ChangeOwnerDuplicateSubscriptionParams,
ChangeOwnerHasFailedSettlementsParams,
@@ -292,6 +293,7 @@ import type {
WeSentYouMagicSignInLinkParams,
WorkEmailMergingBlockedParams,
WorkEmailResendCodeParams,
+ WorkflowSettingsParam,
WorkspaceLockedPlanTypeParams,
WorkspaceMemberList,
WorkspaceMembersCountParams,
@@ -1366,6 +1368,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: {
@@ -5400,6 +5417,11 @@ const translations = {
description: '多级标签帮助您更精确地跟踪费用。为每个项目分配多个标签,例如部门、客户或成本中心,以捕获每笔费用的完整上下文。这使得更详细的报告、审批流程和会计导出成为可能。',
onlyAvailableOnPlan: '多级标签仅在Control计划中提供,起价为',
},
+ [CONST.UPGRADE_FEATURE_INTRO_MAPPING.multiApprovalLevels.id]: {
+ title: '多级审批',
+ description: '多级审批是一种工作流工具,适用于要求一人以上审批报销单后才能进行报销的公司。',
+ onlyAvailableOnPlan: '多级审批仅在 Control 套餐上提供,起价为 ',
+ },
pricing: {
perActiveMember: '每位活跃成员每月。',
perMember: '每位成员每月。',
diff --git a/src/libs/API/parameters/AssignReportToMeParams.ts b/src/libs/API/parameters/AssignReportToMeParams.ts
new file mode 100644
index 0000000000000..5c9b46821c88d
--- /dev/null
+++ b/src/libs/API/parameters/AssignReportToMeParams.ts
@@ -0,0 +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/API/parameters/index.ts b/src/libs/API/parameters/index.ts
index 348bc86a6320e..335dedcbedf02 100644
--- a/src/libs/API/parameters/index.ts
+++ b/src/libs/API/parameters/index.ts
@@ -420,4 +420,5 @@ 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';
export type {default as SaveReportDraftCommentParams} from './SaveReportDraftCommentParams';
diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts
index 4434ef7f3da9d..9f01ed916b9c5 100644
--- a/src/libs/API/types.ts
+++ b/src/libs/API/types.ts
@@ -501,6 +501,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;
@@ -1021,6 +1022,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/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
index 438470e170cb4..529a488538250 100644
--- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
+++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx
@@ -21,6 +21,7 @@ import type {
PrivateNotesNavigatorParamList,
ProfileNavigatorParamList,
ReferralDetailsNavigatorParamList,
+ ReportChangeApproverParamList,
ReportChangeWorkspaceNavigatorParamList,
ReportDescriptionNavigatorParamList,
ReportDetailsNavigatorParamList,
@@ -169,6 +170,10 @@ const ReportChangeWorkspaceModalStackNavigator = createModalStackNavigator require('../../../../pages/ReportChangeWorkspacePage').default,
});
+const ReportChangeApproverModalStackNavigator = createModalStackNavigator({
+ [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: () => require('../../../../pages/ReportChangeApproverPage').default,
+});
+
const ReportSettingsModalStackNavigator = createModalStackNavigator({
[SCREENS.REPORT_SETTINGS.ROOT]: () => require('../../../../pages/settings/Report/ReportSettingsPage').default,
[SCREENS.REPORT_SETTINGS.NAME]: () => require('../../../../pages/settings/Report/NamePage').default,
@@ -835,6 +840,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 df2f877090925..6dc900dd2fb07 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}
/>
+
['config'] = {
},
},
},
+ [SCREENS.RIGHT_MODAL.REPORT_CHANGE_APPROVER]: {
+ screens: {
+ [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: ROUTES.REPORT_CHANGE_APPROVER.route,
+ },
+ },
},
},
diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts
index 8c5e969a238aa..eaa7219e0f00a 100644
--- a/src/libs/Navigation/types.ts
+++ b/src/libs/Navigation/types.ts
@@ -1799,6 +1799,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;
[SCREENS.RIGHT_MODAL.MERGE_TRANSACTION]: NavigatorScreenParams;
};
@@ -2279,6 +2280,12 @@ type ScheduleCallParamList = {
};
};
+type ReportChangeApproverParamList = {
+ [SCREENS.REPORT_CHANGE_APPROVER.ROOT]: {
+ reportID: string;
+ };
+};
+
type TestToolsModalModalNavigatorParamList = {
[SCREENS.TEST_TOOLS_MODAL.ROOT]: {
backTo?: Routes;
@@ -2383,6 +2390,7 @@ export type {
SplitExpenseParamList,
SetParamsAction,
WorkspacesTabNavigatorName,
+ ReportChangeApproverParamList,
TestToolsModalModalNavigatorParamList,
MergeTransactionNavigatorParamList,
};
diff --git a/src/libs/OptionsListUtils.ts b/src/libs/OptionsListUtils.ts
index 10299518d656a..f1e5b63f96308 100644
--- a/src/libs/OptionsListUtils.ts
+++ b/src/libs/OptionsListUtils.ts
@@ -61,6 +61,7 @@ import {
isUserInvitedToWorkspace,
} from './PolicyUtils';
import {
+ getChangedApproverActionMessage,
getCombinedReportActions,
getExportIntegrationLastMessageText,
getIOUReportIDFromReportActionPreview,
@@ -839,6 +840,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);
} else if (isMovedAction(lastReportAction)) {
lastMessageTextFromReport = getMovedActionMessage(lastReportAction, report);
}
diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts
index a1766b4d7ce0a..7a9861428db36 100644
--- a/src/libs/PolicyUtils.ts
+++ b/src/libs/PolicyUtils.ts
@@ -1526,6 +1526,14 @@ function isUserInvitedToWorkspace(): 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);
+}
+
export {
canEditTaxRate,
escapeTagName,
@@ -1678,6 +1686,7 @@ export {
getLengthOfTag,
isPolicyMemberWithoutPendingDelete,
getPolicyEmployeeAccountIDs,
+ isMemberPolicyAdmin,
};
export type {MemberEmailsToAccountIDs};
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 79d15194ba59b..182671bf9f3be 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -1529,7 +1529,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,
@@ -2856,6 +2855,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<
@@ -3210,6 +3218,7 @@ export {
getVacationer,
getSubmittedTo,
getReceiptScanFailedMessage,
+ getChangedApproverActionMessage,
getDelegateAccountIDFromReportAction,
};
diff --git a/src/libs/ReportSecondaryActionUtils.ts b/src/libs/ReportSecondaryActionUtils.ts
index 772bb919fb6b1..adccdcbbf26d1 100644
--- a/src/libs/ReportSecondaryActionUtils.ts
+++ b/src/libs/ReportSecondaryActionUtils.ts
@@ -5,6 +5,7 @@ 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,
@@ -13,6 +14,8 @@ import {
getValidConnectedIntegration,
hasIntegrationAutoSync,
isInstantSubmitEnabled,
+ isMemberPolicyAdmin,
+ isPolicyAdmin,
isPolicyMember,
isPreferredExporter,
isSubmitAndClose,
@@ -678,6 +681,17 @@ function getSecondaryReportActions({
options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_WORKSPACE);
}
+ // @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, getLoginByAccountID(report.managerID ?? CONST.DEFAULT_NUMBER_ID)) &&
+ isExpenseReportUtils(report) &&
+ isProcessingReportUtils(report) &&
+ isPolicyAdmin(policy)
+ ) {
+ options.push(CONST.REPORT.SECONDARY_ACTIONS.CHANGE_APPROVER);
+ }
+
options.push(CONST.REPORT.SECONDARY_ACTIONS.VIEW_DETAILS);
if (isDeleteAction(report, reportTransactions, reportActions ?? [], policy)) {
diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts
index 56e0b988a5b75..b1369a48260e5 100644
--- a/src/libs/ReportUtils.ts
+++ b/src/libs/ReportUtils.ts
@@ -143,6 +143,7 @@ import {
getActionableJoinRequestPendingReportAction,
getAllReportActions,
getCardIssuedMessage,
+ getChangedApproverActionMessage,
getDismissedViolationMessageText,
getExportIntegrationLastMessageText,
getIntegrationSyncFailedMessage,
@@ -734,6 +735,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 = {
@@ -5259,6 +5266,10 @@ function getReportNameInternal({
return getPolicyChangeMessage(parentReportAction);
}
+ if (isActionOfType(parentReportAction, CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL)) {
+ return getChangedApproverActionMessage(parentReportAction);
+ }
+
if (parentReportAction?.actionName && isTagModificationAction(parentReportAction?.actionName)) {
return getCleanedTagName(getWorkspaceTagUpdateMessage(parentReportAction) ?? '');
}
@@ -7635,6 +7646,38 @@ function buildOptimisticResolvedDuplicatesReportAction(): OptimisticDismissedVio
};
}
+function buildOptimisticChangeApproverReportAction(managerID: number, actorAccountID: number): OptimisticChangedApproverReportAction {
+ const created = DateUtils.getDBTime();
+ return {
+ actionName: CONST.REPORT.ACTIONS.TYPE.TAKE_CONTROL,
+ actorAccountID,
+ 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(),
+ },
+ ],
+ originalMessage: {
+ isNewDot: true,
+ lastModified: created,
+ mentionedAccountIDs: [managerID],
+ },
+ 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
@@ -11496,6 +11539,7 @@ export {
buildOptimisticDetachReceipt,
buildParticipantsFromAccountIDs,
buildReportNameFromParticipantNames,
+ buildOptimisticChangeApproverReportAction,
buildTransactionThread,
canAccessReport,
isReportNotFound,
diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts
index 89a758ec38f94..bc7f55c65ba50 100644
--- a/src/libs/SidebarUtils.ts
+++ b/src/libs/SidebarUtils.ts
@@ -34,6 +34,7 @@ import {
getAddedApprovalRuleMessage,
getAddedConnectionMessage,
getCardIssuedMessage,
+ getChangedApproverActionMessage,
getDeletedApprovalRuleMessage,
getIntegrationSyncFailedMessage,
getLastVisibleMessage,
@@ -893,6 +894,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/IOU.ts b/src/libs/actions/IOU.ts
index 58fb04cd9d41a..34b522dc39ca2 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,
@@ -110,6 +111,7 @@ import {
buildOptimisticAddCommentReportAction,
buildOptimisticApprovedReportAction,
buildOptimisticCancelPaymentReportAction,
+ buildOptimisticChangeApproverReportAction,
buildOptimisticChatReport,
buildOptimisticCreatedReportAction,
buildOptimisticDetachReceipt,
@@ -12380,6 +12382,81 @@ function saveSplitTransactions(draftTransaction: OnyxEntry, accountID: number) {
+ if (!report?.reportID) {
+ return;
+ }
+
+ 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);
+
+ 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,
@@ -12488,6 +12565,7 @@ export {
reopenReport,
retractReport,
startDistanceRequest,
+ assignReportToMe,
getPerDiemExpenseInformation,
getSendInvoiceInformation,
};
diff --git a/src/pages/ReportChangeApproverPage.tsx b/src/pages/ReportChangeApproverPage.tsx
new file mode 100644
index 0000000000000..afe0cff9ada4e
--- /dev/null
+++ b/src/pages/ReportChangeApproverPage.tsx
@@ -0,0 +1,108 @@
+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 useEnvironment from '@hooks/useEnvironment';
+import useLocalize from '@hooks/useLocalize';
+import useOnyx from '@hooks/useOnyx';
+import useThemeStyles from '@hooks/useThemeStyles';
+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';
+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';
+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, policy, isLoadingReportData}: ReportChangeApproverPageProps) {
+ const reportID = report?.reportID;
+ const {translate} = useLocalize();
+ const styles = useThemeStyles();
+ const {environmentURL} = useEnvironment();
+
+ const [session] = useOnyx(ONYXKEYS.SESSION, {canBeMissing: false});
+ const [selectedApproverType, setSelectedApproverType] = useState();
+
+ const changeApprover = useCallback(() => {
+ if (!selectedApproverType) {
+ return;
+ }
+
+ if (!isPolicyAdmin(policy) || !policy || !session?.accountID) {
+ return;
+ }
+ assignReportToMe(report, session.accountID);
+ Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(reportID));
+ }, [selectedApproverType, policy, session?.accountID, report, reportID]);
+
+ const sections = useMemo(() => {
+ const data = [];
+
+ if (!isMemberPolicyAdmin(policy, getLoginByAccountID(report.managerID ?? CONST.DEFAULT_NUMBER_ID))) {
+ data.push({
+ text: translate('iou.changeApprover.actions.bypassApprovers'),
+ keyForList: 'bypassApprover',
+ value: 'bypassApprover',
+ alternateText: translate('iou.changeApprover.actions.bypassApproversSubtitle'),
+ isSelected: selectedApproverType === 'bypassApprover',
+ });
+ }
+
+ return [{data}];
+ }, [report, policy, selectedApproverType, translate]);
+
+ // eslint-disable-next-line rulesdir/no-negated-variables
+ const shouldShowNotFoundView = (isEmptyObject(policy) && !isLoadingReportData) || !isPolicyAdmin(policy) || !isMoneyRequestReport(report) || isMoneyRequestReportPendingDeletion(report);
+
+ if (shouldShowNotFoundView) {
+ return ;
+ }
+
+ return (
+
+
+ setSelectedApproverType(option.keyForList)}
+ showConfirmButton
+ confirmButtonText={translate('iou.changeApprover.title')}
+ onConfirm={changeApprover}
+ customListHeader={
+ <>
+ {translate('iou.changeApprover.subtitle')}
+
+
+
+ >
+ }
+ />
+
+ );
+}
+
+ReportChangeApproverPage.displayName = 'ReportChangeApproverPage';
+
+export default withReportOrNotFound()(ReportChangeApproverPage);
diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.tsx b/src/pages/home/report/ContextMenu/ContextMenuActions.tsx
index 3831aef198331..dddf6422fcf97 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,
@@ -674,6 +675,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 (isMovedAction(reportAction)) {
setClipboardMessage(getMovedActionMessage(reportAction, originalReport));
} else if (reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHANGE_POLICY) {
diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx
index 0cae1c2d1a3a8..0201b1a8cd77e 100644
--- a/src/pages/home/report/PureReportActionItem.tsx
+++ b/src/pages/home/report/PureReportActionItem.tsx
@@ -60,6 +60,7 @@ import {
extractLinksFromMessageHtml,
getAddedApprovalRuleMessage,
getAddedConnectionMessage,
+ getChangedApproverActionMessage,
getDeletedApprovalRuleMessage,
getDemotedFromWorkspaceMessage,
getDismissedViolationMessageText,
@@ -1264,6 +1265,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/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'];
diff --git a/src/types/onyx/OriginalMessage.ts b/src/types/onyx/OriginalMessage.ts
index 0b04b5c750d07..6ec4c2b650f9d 100644
--- a/src/types/onyx/OriginalMessage.ts
+++ b/src/types/onyx/OriginalMessage.ts
@@ -895,6 +895,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
*/
@@ -964,7 +976,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;