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/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index c51eadf2c637..86f7aaaecce9 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, 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 3df4b7d0c15f..6b45283421ea 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -7200,6 +7200,17 @@ function canIOUBePaid( ); } +function canSubmitReport(report: OnyxEntry | SearchReport, policy: OnyxEntry | SearchPolicy) { + const currentUserAccountID = Report.getCurrentUserAccountID(); + const isOpenExpenseReport = ReportUtils.isOpenExpenseReport(report); + const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(report); + const isAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + + // 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 { const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {}; @@ -8833,5 +8844,6 @@ export { getIOUReportActionToApproveOrPay, getNavigationUrlOnMoneyRequestDelete, getNavigationUrlAfterTrackExpenseDelete, + canSubmitReport, }; export type {GPSPoint as GpsPoint, IOURequestType}; diff --git a/src/libs/actions/Search.ts b/src/libs/actions/Search.ts index c08a83088180..65e5cfe62c63 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; @@ -64,6 +66,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: { + const policy = (allSnapshots?.[`${ONYXKEYS.COLLECTION.SNAPSHOT}${hash}`]?.data?.[`${ONYXKEYS.COLLECTION.POLICY}${item.policyID}`] ?? {}) as SearchPolicy; + submitMoneyRequestOnSearch(hash, [item], [policy], transactionID); + return; + } default: goToItem(); } @@ -236,6 +243,35 @@ 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, reportList: SearchReport[], policy: SearchPolicy[], 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(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {isActionLoading: isLoading}])) as Partial), + }, + }, + ]; + const optimisticData: OnyxUpdate[] = createActionLoadingData(true); + const finallyData: OnyxUpdate[] = createActionLoadingData(false); + + const report = (reportList.at(0) ?? {}) as SearchReport; + const parameters: SubmitReportParams = { + reportID: report.reportID, + managerAccountID: PolicyUtils.getSubmitToAccountID(policy.at(0), report) ?? report?.managerID, + 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 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}); +} + function approveMoneyRequestOnSearch(hash: number, reportIDList: string[], transactionIDList?: string[]) { const createActionLoadingData = (isLoading: boolean): OnyxUpdate[] => [ { @@ -369,4 +405,5 @@ export { payMoneyRequestOnSearch, approveMoneyRequestOnSearch, handleActionButtonPress, + submitMoneyRequestOnSearch, }; diff --git a/src/types/onyx/SearchResults.ts b/src/types/onyx/SearchResults.ts index a47c0c5cab49..7cee219d3b04 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'; @@ -230,6 +230,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 */