From 5933ec686e3a0f9e0f3ce5cd41abb82277fd7947 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 5 Dec 2024 10:36:46 -0700 Subject: [PATCH 01/15] create canSubmitReport --- src/libs/SearchUIUtils.ts | 4 ++++ src/libs/actions/IOU.ts | 22 ++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index e100fb885fff..c5a6b90d5b9f 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -299,6 +299,10 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr return CONST.SEARCH.ACTION_TYPES.APPROVE; } + if (IOU.canSubmitReport(report, allReportTransactions, policy)) { + return CONST.SEARCH.ACTION_TYPES.SUBMIT; + } + return CONST.SEARCH.ACTION_TYPES.VIEW; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 438fba5fef94..713de9dbf594 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7063,6 +7063,27 @@ function canIOUBePaid( ); } +function canSubmitReport( + report: OnyxEntry | SearchReport, + allReportTransactions: OnyxEntry | SearchTransaction[], + policy: OnyxEntry | SearchPolicy, +) { + const currentUserAccountID = Report.getCurrentUserAccountID(); + const isOpenExpenseReport = ReportUtils.isOpenExpenseReport(report); + const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); + const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation(allReportTransactions); + const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', moneyRequestReport, policy); + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + + return ( + isOpenExpenseReport && + reimbursableSpend !== 0 && + !hasAllPendingRTERViolations && + !shouldShowBrokenConnectionViolation && + (report?.ownerAccountID === currentUserAccountID || isAdmin || report?.managerID === currentUserAccountID) + ); +} + function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry, excludedIOUReportID: string): OnyxEntry { const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; @@ -8675,5 +8696,6 @@ export { getIOUReportActionToApproveOrPay, getNavigationUrlOnMoneyRequestDelete, getNavigationUrlAfterTrackExpenseDelete, + canSubmitReport, }; export type {GPSPoint as GpsPoint, IOURequestType}; From 5ffa4e9eb67d4b2b6c2f9bd17f7ba3f83fc73986 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 5 Dec 2024 16:20:25 -0700 Subject: [PATCH 02/15] update logic --- src/libs/SearchUIUtils.ts | 3 ++- src/libs/TransactionUtils/index.ts | 13 +++++++++---- src/libs/actions/IOU.ts | 10 +++------- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c5a6b90d5b9f..8f6552a4802a 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -299,7 +299,8 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr return CONST.SEARCH.ACTION_TYPES.APPROVE; } - if (IOU.canSubmitReport(report, allReportTransactions, policy)) { + const allTransactionsViolations = [] as OnyxTypes.TransactionViolations[]; + if (IOU.canSubmitReport(report, policy, allTransactionsViolations)) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 944c431aaad9..6aed060ca9a5 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -714,8 +714,8 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | /** * Check if there is broken connection violation. */ -function hasBrokenConnectionViolation(transactionID: string): boolean { - const violations = getTransactionViolations(transactionID, allTransactionViolations); +function hasBrokenConnectionViolation(transactionID: string, transactionViolations?: TransactionViolations): boolean { + const violations = transactionViolations ?? getTransactionViolations(transactionID, allTransactionViolations); return !!violations?.find( (violation) => violation.name === CONST.VIOLATIONS.RTER && @@ -726,9 +726,14 @@ function hasBrokenConnectionViolation(transactionID: string): boolean { /** * Check if user should see broken connection violation warning. */ -function shouldShowBrokenConnectionViolation(transactionID: string, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean { +function shouldShowBrokenConnectionViolation( + transactionID: string, + report: OnyxEntry | SearchReport, + policy: OnyxEntry | SearchPolicy, + transactionViolations?: TransactionViolations, +): boolean { return ( - hasBrokenConnectionViolation(transactionID) && + hasBrokenConnectionViolation(transactionID, transactionViolations) && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isOpenExpenseReport(report) || (ReportUtils.isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy))) ); } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 713de9dbf594..b207cc42d72d 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7063,16 +7063,12 @@ function canIOUBePaid( ); } -function canSubmitReport( - report: OnyxEntry | SearchReport, - allReportTransactions: OnyxEntry | SearchTransaction[], - policy: OnyxEntry | SearchPolicy, -) { +function canSubmitReport(report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy, transactionViolations: OnyxTypes.TransactionViolations[]) { const currentUserAccountID = Report.getCurrentUserAccountID(); const isOpenExpenseReport = ReportUtils.isOpenExpenseReport(report); const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); - const hasAllPendingRTERViolations = TransactionUtils.allHavePendingRTERViolation(allReportTransactions); - const shouldShowBrokenConnectionViolation = TransactionUtils.shouldShowBrokenConnectionViolation(transaction?.transactionID ?? '-1', moneyRequestReport, policy); + const hasAllPendingRTERViolations = transactionViolations.every((violations) => TransactionUtils.hasPendingRTERViolation(violations)); + const shouldShowBrokenConnectionViolation = transactionViolations.some((violations) => TransactionUtils.shouldShowBrokenConnectionViolation('', report, policy, violations)); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; return ( From 3e643f550d5c83abea702f9dbc2c4daa13ab55ae Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 5 Dec 2024 16:21:21 -0700 Subject: [PATCH 03/15] add comment --- src/libs/SearchUIUtils.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 8f6552a4802a..2fcabf1c8201 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -299,6 +299,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr return CONST.SEARCH.ACTION_TYPES.APPROVE; } + // We're intentionally passing empty violations for now since Auth violations are not ready yet and the risk of blocking submit because of the RTER or Broken connections violations is really small for the current cohort of users. const allTransactionsViolations = [] as OnyxTypes.TransactionViolations[]; if (IOU.canSubmitReport(report, policy, allTransactionsViolations)) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; From 76684ab59298e647a4ec1cbd768ec3cee50b0d5c Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 5 Dec 2024 16:29:32 -0700 Subject: [PATCH 04/15] rm violations code --- src/libs/SearchUIUtils.ts | 3 +-- src/libs/actions/IOU.ts | 14 ++++---------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 2fcabf1c8201..143c7bdf34c2 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -300,8 +300,7 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr } // We're intentionally passing empty violations for now since Auth violations are not ready yet and the risk of blocking submit because of the RTER or Broken connections violations is really small for the current cohort of users. - const allTransactionsViolations = [] as OnyxTypes.TransactionViolations[]; - if (IOU.canSubmitReport(report, policy, allTransactionsViolations)) { + if (IOU.canSubmitReport(report, policy)) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index b207cc42d72d..60df29d41862 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7063,21 +7063,15 @@ function canIOUBePaid( ); } -function canSubmitReport(report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy, transactionViolations: OnyxTypes.TransactionViolations[]) { +function canSubmitReport(report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy) { const currentUserAccountID = Report.getCurrentUserAccountID(); const isOpenExpenseReport = ReportUtils.isOpenExpenseReport(report); const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); - const hasAllPendingRTERViolations = transactionViolations.every((violations) => TransactionUtils.hasPendingRTERViolation(violations)); - const shouldShowBrokenConnectionViolation = transactionViolations.some((violations) => TransactionUtils.shouldShowBrokenConnectionViolation('', report, policy, violations)); const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; - return ( - isOpenExpenseReport && - reimbursableSpend !== 0 && - !hasAllPendingRTERViolations && - !shouldShowBrokenConnectionViolation && - (report?.ownerAccountID === currentUserAccountID || isAdmin || report?.managerID === currentUserAccountID) - ); + // This logic differs from the one in MoneyRequestHeader + // We are intentionally doing this for now because Auth violations are not ready and thus not returned by Search results. Additionally, the risk of a customer having either RTER or Broken connection violation is really small in the current cohort. + return isOpenExpenseReport && reimbursableSpend !== 0 && (report?.ownerAccountID === currentUserAccountID || isAdmin || report?.managerID === currentUserAccountID); } function getIOUReportActionToApproveOrPay(chatReport: OnyxEntry, excludedIOUReportID: string): OnyxEntry { From 179b791310c38a21b6ebd3c203234672fd175e56 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 5 Dec 2024 16:33:33 -0700 Subject: [PATCH 05/15] add api command --- .../SubmitMoneyRequestOnSearchParams.ts | 6 +++++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 ++ src/libs/actions/Search.ts | 23 +++++++++++++++++++ 4 files changed, 32 insertions(+) create mode 100644 src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts diff --git a/src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts b/src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts new file mode 100644 index 000000000000..a1d4ba329067 --- /dev/null +++ b/src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts @@ -0,0 +1,6 @@ +type SubmitMoneyRequestOnSearchParams = { + hash: number; + reportIDList: string[]; +}; + +export default SubmitMoneyRequestOnSearchParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 6a510d074f98..d8b9d4da67e5 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -257,6 +257,7 @@ export type {default as UpgradeToCorporateParams} from './UpgradeToCorporatePara export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyRequestOnSearchParams'; export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams'; export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyRequestOnSearchParams'; +export type {default as SubmitMoneyRequestOnSearchParams} from './SubmitMoneyRequestOnSearchParams'; export type {default as PayMoneyRequestOnSearchParams} from './PayMoneyRequestOnSearchParams'; export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 892bad17928e..a79a7fd193f1 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -316,6 +316,7 @@ const WRITE_COMMANDS = { UPGRADE_TO_CORPORATE: 'UpgradeToCorporate', DELETE_MONEY_REQUEST_ON_SEARCH: 'DeleteMoneyRequestOnSearch', HOLD_MONEY_REQUEST_ON_SEARCH: 'HoldMoneyRequestOnSearch', + SUBMIT_MONEY_REQUEST_ON_SEARCH: 'SubmitMoneyRequestOnSearch', APPROVE_MONEY_REQUEST_ON_SEARCH: 'ApproveMoneyRequestOnSearch', UNHOLD_MONEY_REQUEST_ON_SEARCH: 'UnholdMoneyRequestOnSearch', REQUEST_REFUND: 'User_RefundPurchase', @@ -765,6 +766,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams; + [WRITE_COMMANDS.SUBMIT_MONEY_REQUEST_ON_SEARCH]: Parameters.SubmitMoneyRequestOnSearchParams; [WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH]: Parameters.ApproveMoneyRequestOnSearchParams; [WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.UnholdMoneyRequestOnSearchParams; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index bb64fe10db26..bce6c4e29d3c 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -71,6 +71,9 @@ function handleActionButtonPress(hash: number, item: TransactionListItemType | R case CONST.SEARCH.ACTION_TYPES.APPROVE: approveMoneyRequestOnSearch(hash, [item.reportID], transactionID); return; + case CONST.SEARCH.ACTION_TYPES.SUBMIT: + submitMoneyRequestOnSearch(hash, [item.reportID], transactionID); + return; default: goToItem(); } @@ -243,6 +246,26 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList, comment}, {optimisticData, finallyData}); } +function submitMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[]) { + const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`, + value: { + data: transactionIDList + ? (Object.fromEntries( + transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {isActionLoading: isLoading}]), + ) as Partial) + : (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {isActionLoading: isLoading}])) as Partial), + }, + }, + ]; + const optimisticData: OnyxUpdate[] = createActionLoadingData(true); + const finallyData: OnyxUpdate[] = createActionLoadingData(false); + + API.write(WRITE_COMMANDS.SUBMIT_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); +} + function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[]) { const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [ { From 868e49fc47da0404e81d3f3ec69f35aa78b5d211 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 5 Dec 2024 16:39:06 -0700 Subject: [PATCH 06/15] add bulk action --- src/CONST.ts | 1 + src/components/Search/SearchPageHeader.tsx | 28 ++++++++++++++++++++++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/actions/Search.ts | 1 + 5 files changed, 32 insertions(+) diff --git a/src/CONST.ts b/src/CONST.ts index c28914541113..bccb38fff1b0 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5987,6 +5987,7 @@ const CONST = { HOLD: 'hold', UNHOLD: 'unhold', DELETE: 'delete', + SUBMIT: 'submit', }, TRANSACTION_TYPE: { CASH: 'cash', diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index a78845f126d2..8afbe42812a0 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -83,6 +83,34 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const options: Array> = []; const isAnyTransactionOnHold = Object.values(selectedTransactions).some((transaction) => transaction.isHeld); + const shouldShowSubmitOption = + !isOffline && + !isAnyTransactionOnHold && + (selectedReports.length + ? selectedReports.every((report) => report.action === CONST.SEARCH.ACTION_TYPES.SUBMIT) + : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.SUBMIT)); + + if (shouldShowSubmitOption) { + options.push({ + icon: Expensicons.Plane, + text: translate('search.bulkActions.submit'), + value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT, + shouldCloseModalOnSelect: true, + onSelected: () => { + if (isOffline) { + setIsOfflineModalVisible(true); + return; + } + + const transactionIDList = selectedReports.length ? undefined : Object.keys(selectedTransactions); + const reportIDList = !selectedReports.length + ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) + : selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []; + SearchActions.submitMoneyRequestOnSearch(hash, reportIDList, transactionIDList); + }, + }); + } + const shouldShowApproveOption = !isOffline && !isAnyTransactionOnHold && diff --git a/src/languages/en.ts b/src/languages/en.ts index d79695ed8b48..84c61d83a2f6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4551,6 +4551,7 @@ const translations = { groupedExpenses: 'grouped expenses', bulkActions: { approve: 'Approve', + submit: 'Submit', pay: 'Pay', delete: 'Delete', hold: 'Hold', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5ce47db18d35..99cb86cc956e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4600,6 +4600,7 @@ const translations = { groupedExpenses: 'gastos agrupados', bulkActions: { approve: 'Aprobar', + submit: 'Enviar', pay: 'Pagar', delete: 'Eliminar', hold: 'Retener', diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index bce6c4e29d3c..679747e4bb48 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -399,4 +399,5 @@ export { payMoneyRequestOnSearch, approveMoneyRequestOnSearch, handleActionButtonPress, + submitMoneyRequestOnSearch, }; From e261b1778718447e2a2b5d2fe26eba574f1a8ae0 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Thu, 5 Dec 2024 16:40:57 -0700 Subject: [PATCH 07/15] update icon --- src/components/Search/SearchPageHeader.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index 8afbe42812a0..ee3e0a7d2069 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -92,7 +92,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { if (shouldShowSubmitOption) { options.push({ - icon: Expensicons.Plane, + icon: Expensicons.Send, text: translate('search.bulkActions.submit'), value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT, shouldCloseModalOnSelect: true, From 8b67398107ab8d2843cd5b1ed1cb650ac56971fb Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 6 Dec 2024 10:25:59 -0700 Subject: [PATCH 08/15] rm violations code --- src/libs/SearchUIUtils.ts | 1 - src/libs/TransactionUtils/index.ts | 13 ++++--------- 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 143c7bdf34c2..2d0fa12a6d2c 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -299,7 +299,6 @@ function getAction(data: OnyxTypes.SearchResults['data'], key: string): SearchTr return CONST.SEARCH.ACTION_TYPES.APPROVE; } - // We're intentionally passing empty violations for now since Auth violations are not ready yet and the risk of blocking submit because of the RTER or Broken connections violations is really small for the current cohort of users. if (IOU.canSubmitReport(report, policy)) { return CONST.SEARCH.ACTION_TYPES.SUBMIT; } diff --git a/src/libs/TransactionUtils/index.ts b/src/libs/TransactionUtils/index.ts index 6aed060ca9a5..944c431aaad9 100644 --- a/src/libs/TransactionUtils/index.ts +++ b/src/libs/TransactionUtils/index.ts @@ -714,8 +714,8 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations | /** * Check if there is broken connection violation. */ -function hasBrokenConnectionViolation(transactionID: string, transactionViolations?: TransactionViolations): boolean { - const violations = transactionViolations ?? getTransactionViolations(transactionID, allTransactionViolations); +function hasBrokenConnectionViolation(transactionID: string): boolean { + const violations = getTransactionViolations(transactionID, allTransactionViolations); return !!violations?.find( (violation) => violation.name === CONST.VIOLATIONS.RTER && @@ -726,14 +726,9 @@ function hasBrokenConnectionViolation(transactionID: string, transactionViolatio /** * Check if user should see broken connection violation warning. */ -function shouldShowBrokenConnectionViolation( - transactionID: string, - report: OnyxEntry | SearchReport, - policy: OnyxEntry | SearchPolicy, - transactionViolations?: TransactionViolations, -): boolean { +function shouldShowBrokenConnectionViolation(transactionID: string, report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy): boolean { return ( - hasBrokenConnectionViolation(transactionID, transactionViolations) && + hasBrokenConnectionViolation(transactionID) && (!PolicyUtils.isPolicyAdmin(policy) || ReportUtils.isOpenExpenseReport(report) || (ReportUtils.isProcessingReport(report) && PolicyUtils.isInstantSubmitEnabled(policy))) ); } From e32daf1a3899dec0ef4445c1a216d4669475aff2 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 6 Dec 2024 13:42:36 -0700 Subject: [PATCH 09/15] temporarily call submitreport --- src/components/Search/SearchPageHeader.tsx | 28 ---------------------- src/libs/PolicyUtils.ts | 14 +++++------ src/libs/actions/Search.ts | 25 +++++++++++++------ src/types/onyx/SearchResults.ts | 17 ++++++++++++- 4 files changed, 41 insertions(+), 43 deletions(-) diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader.tsx index ee3e0a7d2069..a78845f126d2 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader.tsx @@ -83,34 +83,6 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const options: Array> = []; const isAnyTransactionOnHold = Object.values(selectedTransactions).some((transaction) => transaction.isHeld); - const shouldShowSubmitOption = - !isOffline && - !isAnyTransactionOnHold && - (selectedReports.length - ? selectedReports.every((report) => report.action === CONST.SEARCH.ACTION_TYPES.SUBMIT) - : selectedTransactionsKeys.every((id) => selectedTransactions[id].action === CONST.SEARCH.ACTION_TYPES.SUBMIT)); - - if (shouldShowSubmitOption) { - options.push({ - icon: Expensicons.Send, - text: translate('search.bulkActions.submit'), - value: CONST.SEARCH.BULK_ACTION_TYPES.SUBMIT, - shouldCloseModalOnSelect: true, - onSelected: () => { - if (isOffline) { - setIsOfflineModalVisible(true); - return; - } - - const transactionIDList = selectedReports.length ? undefined : Object.keys(selectedTransactions); - const reportIDList = !selectedReports.length - ? Object.values(selectedTransactions).map((transaction) => transaction.reportID) - : selectedReports?.filter((report) => !!report).map((report) => report.reportID) ?? []; - SearchActions.submitMoneyRequestOnSearch(hash, reportIDList, transactionIDList); - }, - }); - } - const shouldShowApproveOption = !isOffline && !isAnyTransactionOnHold && diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index f6b277d69d6b..2647c029aa7f 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -520,7 +520,7 @@ function isPolicyFeatureEnabled(policy: OnyxEntry, featureName: PolicyFe return !!policy?.[featureName]; } -function getApprovalWorkflow(policy: OnyxEntry): ValueOf { +function getApprovalWorkflow(policy: OnyxEntry | SearchPolicy): ValueOf { if (policy?.type === CONST.POLICY.TYPE.PERSONAL) { return CONST.POLICY.APPROVAL_MODE.OPTIONAL; } @@ -528,14 +528,14 @@ function getApprovalWorkflow(policy: OnyxEntry): ValueOf): string { +function getDefaultApprover(policy: OnyxEntry | SearchPolicy): string { return policy?.approver ?? policy?.owner ?? ''; } /** * Returns the accountID to whom the given expenseReport submits reports to in the given Policy. */ -function getSubmitToAccountID(policy: OnyxEntry, expenseReport: OnyxEntry): number { +function getSubmitToAccountID(policy: OnyxEntry | SearchPolicy, expenseReport: OnyxEntry): number { const employeeAccountID = expenseReport?.ownerAccountID ?? -1; const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? ''; const defaultApprover = getDefaultApprover(policy); @@ -555,8 +555,8 @@ function getSubmitToAccountID(policy: OnyxEntry, expenseReport: OnyxEntr return getAccountIDsByLogins([categoryAppover]).at(0) ?? -1; } - if (!tagApprover && getTagApproverRule(policy?.id ?? '-1', tag)?.approver) { - tagApprover = getTagApproverRule(policy?.id ?? '-1', tag)?.approver; + if (!tagApprover && getTagApproverRule(policy ?? '-1', tag)?.approver) { + tagApprover = getTagApproverRule(policy ?? '-1', tag)?.approver; } } @@ -1084,8 +1084,8 @@ function hasVBBA(policyID: string) { return !!policy?.achAccount?.bankAccountID; } -function getTagApproverRule(policyID: string, tagName: string) { - const policy = getPolicy(policyID); +function getTagApproverRule(policyOrID: string | SearchPolicy | OnyxEntry, tagName: string) { + const policy = typeof policyOrID === 'string' ? getPolicy(policyOrID) : policyOrID; const approvalRules = policy?.rules?.approvalRules ?? []; const approverRule = approvalRules.find((rule) => diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 679747e4bb48..18983ec71886 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -5,11 +5,13 @@ import type {FormOnyxValues} from '@components/Form/types'; import type {PaymentData, SearchQueryJSON} from '@components/Search/types'; import type {ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import * as API from '@libs/API'; -import type {ExportSearchItemsToCSVParams} from '@libs/API/parameters'; +import type {ExportSearchItemsToCSVParams, SubmitReportParams} from '@libs/API/parameters'; import {SIDE_EFFECT_REQUEST_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ApiUtils from '@libs/ApiUtils'; import fileDownload from '@libs/fileDownload'; import enhanceParameters from '@libs/Network/enhanceParameters'; +import {rand64} from '@libs/NumberUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import {isReportListItemType, isTransactionListItemType} from '@libs/SearchUIUtils'; import playSound, {SOUNDS} from '@libs/Sound'; @@ -17,7 +19,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import FILTER_KEYS from '@src/types/form/SearchAdvancedFiltersForm'; import type {LastPaymentMethod, SearchResults} from '@src/types/onyx'; -import type {SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; +import type {SearchPolicy, SearchReport, SearchTransaction} from '@src/types/onyx/SearchResults'; import * as Report from './Report'; let currentUserEmail: string; @@ -71,9 +73,11 @@ function handleActionButtonPress(hash: number, item: TransactionListItemType | R case CONST.SEARCH.ACTION_TYPES.APPROVE: approveMoneyRequestOnSearch(hash, [item.reportID], transactionID); return; - case CONST.SEARCH.ACTION_TYPES.SUBMIT: - submitMoneyRequestOnSearch(hash, [item.reportID], transactionID); + case CONST.SEARCH.ACTION_TYPES.SUBMIT: { + const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`]; + submitMoneyRequestOnSearch(hash, [item], [policy], transactionID); return; + } default: goToItem(); } @@ -246,7 +250,7 @@ function holdMoneyRequestOnSearch(hash: number, transactionIDList: string[], com API.write(WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH, {hash, transactionIDList, comment}, {optimisticData, finallyData}); } -function submitMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[]) { +function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], policy: SearchPolicy[], transactionIDList?: string[]) { const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [ { onyxMethod: Onyx.METHOD.MERGE, @@ -256,14 +260,21 @@ function submitMoneyRequestOnSearch(hash: number, reportIDList: string[], transa ? (Object.fromEntries( transactionIDList.map((transactionID) => [`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {isActionLoading: isLoading}]), ) as Partial) - : (Object.fromEntries(reportIDList.map((reportID) => [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {isActionLoading: isLoading}])) as Partial), + : (Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {isActionLoading: isLoading}])) as Partial), }, }, ]; const optimisticData: OnyxUpdate[] = createActionLoadingData(true); const finallyData: OnyxUpdate[] = createActionLoadingData(false); - API.write(WRITE_COMMANDS.SUBMIT_MONEY_REQUEST_ON_SEARCH, {hash, reportIDList}, {optimisticData, finallyData}); + const report = (reportList.at(0) ?? {}) as SearchReport; + const parameters: SubmitReportParams = { + reportID: report.reportID, + managerAccountID: PolicyUtils.getSubmitToAccountID(policy.at(0), report) ?? report?.managerID, + reportActionID: rand64(), + }; + + API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, finallyData}); } function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[]) { diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index bd5502e76a5f..e3e2b512ec47 100644 --- a/src/types/onyx/SearchResults.ts +++ b/src/types/onyx/SearchResults.ts @@ -6,7 +6,7 @@ import type TransactionListItem from '@components/SelectionList/Search/Transacti import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types'; import type CONST from '@src/CONST'; import type ONYXKEYS from '@src/ONYXKEYS'; -import type {ACHAccount} from './Policy'; +import type {ACHAccount, ApprovalRule, ExpenseRule} from './Policy'; import type {InvoiceReceiver} from './Report'; import type ReportActionName from './ReportActionName'; import type ReportNameValuePairs from './ReportNameValuePairs'; @@ -226,6 +226,21 @@ type SearchPolicy = { /** Whether the self approval or submitting is enabled */ preventSelfApproval?: boolean; + + /** The email of the policy owner */ + owner: string; + + /** The approver of the policy */ + approver?: string; + + /** A set of rules related to the workpsace */ + rules?: { + /** A set of rules related to the workpsace approvals */ + approvalRules?: ApprovalRule[]; + + /** A set of rules related to the workpsace expenses */ + expenseRules?: ExpenseRule[]; + }; }; /** Model of transaction search result */ From 31a198d017de0b4f21d5e502dc10c584e8749ef3 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 6 Dec 2024 13:45:32 -0700 Subject: [PATCH 10/15] rm unused API command --- src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts | 6 ------ src/libs/API/parameters/index.ts | 1 - src/libs/API/types.ts | 2 -- 3 files changed, 9 deletions(-) delete mode 100644 src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts diff --git a/src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts b/src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts deleted file mode 100644 index a1d4ba329067..000000000000 --- a/src/libs/API/parameters/SubmitMoneyRequestOnSearchParams.ts +++ /dev/null @@ -1,6 +0,0 @@ -type SubmitMoneyRequestOnSearchParams = { - hash: number; - reportIDList: string[]; -}; - -export default SubmitMoneyRequestOnSearchParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index d8b9d4da67e5..6a510d074f98 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -257,7 +257,6 @@ export type {default as UpgradeToCorporateParams} from './UpgradeToCorporatePara export type {default as DeleteMoneyRequestOnSearchParams} from './DeleteMoneyRequestOnSearchParams'; export type {default as HoldMoneyRequestOnSearchParams} from './HoldMoneyRequestOnSearchParams'; export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyRequestOnSearchParams'; -export type {default as SubmitMoneyRequestOnSearchParams} from './SubmitMoneyRequestOnSearchParams'; export type {default as PayMoneyRequestOnSearchParams} from './PayMoneyRequestOnSearchParams'; export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index a79a7fd193f1..892bad17928e 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -316,7 +316,6 @@ const WRITE_COMMANDS = { UPGRADE_TO_CORPORATE: 'UpgradeToCorporate', DELETE_MONEY_REQUEST_ON_SEARCH: 'DeleteMoneyRequestOnSearch', HOLD_MONEY_REQUEST_ON_SEARCH: 'HoldMoneyRequestOnSearch', - SUBMIT_MONEY_REQUEST_ON_SEARCH: 'SubmitMoneyRequestOnSearch', APPROVE_MONEY_REQUEST_ON_SEARCH: 'ApproveMoneyRequestOnSearch', UNHOLD_MONEY_REQUEST_ON_SEARCH: 'UnholdMoneyRequestOnSearch', REQUEST_REFUND: 'User_RefundPurchase', @@ -766,7 +765,6 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DELETE_MONEY_REQUEST_ON_SEARCH]: Parameters.DeleteMoneyRequestOnSearchParams; [WRITE_COMMANDS.HOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.HoldMoneyRequestOnSearchParams; - [WRITE_COMMANDS.SUBMIT_MONEY_REQUEST_ON_SEARCH]: Parameters.SubmitMoneyRequestOnSearchParams; [WRITE_COMMANDS.APPROVE_MONEY_REQUEST_ON_SEARCH]: Parameters.ApproveMoneyRequestOnSearchParams; [WRITE_COMMANDS.UNHOLD_MONEY_REQUEST_ON_SEARCH]: Parameters.UnholdMoneyRequestOnSearchParams; From 63bf8a117b3206c15cf0a8af8121e848399fbdfb Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 6 Dec 2024 13:47:10 -0700 Subject: [PATCH 11/15] add comment --- src/libs/actions/Search.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 18983ec71886..57d5345bfd37 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -274,6 +274,8 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po reportActionID: rand64(), }; + // The SubmitReport command is not 1:1:1 yet, which means creating a separate SubmitMoneyRequestOnSearch command is not feasible until https://github.com/Expensify/Expensify/issues/451223 is done. + // In the meanyime, we'll call SubmitReport which works for a single expense only, so not bulk actions are possible. API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, finallyData}); } From e13a21b5196035fb76afc94216ea2a467b92de11 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 6 Dec 2024 13:48:19 -0700 Subject: [PATCH 12/15] rm unused const --- src/CONST.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index bccb38fff1b0..c28914541113 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5987,7 +5987,6 @@ const CONST = { HOLD: 'hold', UNHOLD: 'unhold', DELETE: 'delete', - SUBMIT: 'submit', }, TRANSACTION_TYPE: { CASH: 'cash', From 0b303fb18e27eb5379be0ca3af8086a3bd815a99 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Fri, 6 Dec 2024 13:48:53 -0700 Subject: [PATCH 13/15] rm more bulk action code --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - 2 files changed, 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 84c61d83a2f6..d79695ed8b48 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4551,7 +4551,6 @@ const translations = { groupedExpenses: 'grouped expenses', bulkActions: { approve: 'Approve', - submit: 'Submit', pay: 'Pay', delete: 'Delete', hold: 'Hold', diff --git a/src/languages/es.ts b/src/languages/es.ts index 99cb86cc956e..5ce47db18d35 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4600,7 +4600,6 @@ const translations = { groupedExpenses: 'gastos agrupados', bulkActions: { approve: 'Aprobar', - submit: 'Enviar', pay: 'Pagar', delete: 'Eliminar', hold: 'Retener', From c2b08d4f014ba4944d02c5b02bb0e579a608e736 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Mon, 9 Dec 2024 17:19:55 -0700 Subject: [PATCH 14/15] Update src/libs/actions/Search.ts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Lucien Akchoté --- src/libs/actions/Search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 57d5345bfd37..a8ce4e00d03d 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -275,7 +275,7 @@ function submitMoneyRequestOnSearch(hash: number, reportList: SearchReport[], po }; // The SubmitReport command is not 1:1:1 yet, which means creating a separate SubmitMoneyRequestOnSearch command is not feasible until https://github.com/Expensify/Expensify/issues/451223 is done. - // In the meanyime, we'll call SubmitReport which works for a single expense only, so not bulk actions are possible. + // In the meantime, we'll call SubmitReport which works for a single expense only, so not bulk actions are possible. API.write(WRITE_COMMANDS.SUBMIT_REPORT, parameters, {optimisticData, finallyData}); } From 88cacedd042a5f199ef929841fb103c2c0112449 Mon Sep 17 00:00:00 2001 From: Carlos Martins Date: Tue, 10 Dec 2024 13:21:30 -0700 Subject: [PATCH 15/15] fix ts --- src/libs/actions/Search.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index 834289994460..65e5cfe62c63 100644 --- a/src/libs/actions/Search.ts +++ b/src/libs/actions/Search.ts @@ -67,7 +67,7 @@ function handleActionButtonPress(hash: number, item: TransactionListItemType | R approveMoneyRequestOnSearch(hash, [item.reportID], transactionID); return; case CONST.SEARCH.ACTION_TYPES.SUBMIT: { - const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`]; + const policy = (allSnapshots?.[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`]?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`] ?? {}) as SearchPolicy; submitMoneyRequestOnSearch(hash, [item], [policy], transactionID); return; }