Skip to content

Commit

Permalink
Merge pull request #53641 from Expensify/cmartins-addSubmitAction
Browse files Browse the repository at this point in the history
Add submit action
  • Loading branch information
luacmartins authored Dec 11, 2024
2 parents 39aff4d + 88caced commit 1584baf
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 10 deletions.
14 changes: 7 additions & 7 deletions src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -520,22 +520,22 @@ function isPolicyFeatureEnabled(policy: OnyxEntry<Policy>, featureName: PolicyFe
return !!policy?.[featureName];
}

function getApprovalWorkflow(policy: OnyxEntry<Policy>): ValueOf<typeof CONST.POLICY.APPROVAL_MODE> {
function getApprovalWorkflow(policy: OnyxEntry<Policy> | SearchPolicy): ValueOf<typeof CONST.POLICY.APPROVAL_MODE> {
if (policy?.type === CONST.POLICY.TYPE.PERSONAL) {
return CONST.POLICY.APPROVAL_MODE.OPTIONAL;
}

return policy?.approvalMode ?? CONST.POLICY.APPROVAL_MODE.ADVANCED;
}

function getDefaultApprover(policy: OnyxEntry<Policy>): string {
function getDefaultApprover(policy: OnyxEntry<Policy> | 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<Policy>, expenseReport: OnyxEntry<Report>): number {
function getSubmitToAccountID(policy: OnyxEntry<Policy> | SearchPolicy, expenseReport: OnyxEntry<Report>): number {
const employeeAccountID = expenseReport?.ownerAccountID ?? -1;
const employeeLogin = getLoginsByAccountIDs([employeeAccountID]).at(0) ?? '';
const defaultApprover = getDefaultApprover(policy);
Expand All @@ -555,8 +555,8 @@ function getSubmitToAccountID(policy: OnyxEntry<Policy>, 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;
}
}

Expand Down Expand Up @@ -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<Policy>, tagName: string) {
const policy = typeof policyOrID === 'string' ? getPolicy(policyOrID) : policyOrID;

const approvalRules = policy?.rules?.approvalRules ?? [];
const approverRule = approvalRules.find((rule) =>
Expand Down
4 changes: 4 additions & 0 deletions src/libs/SearchUIUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
12 changes: 12 additions & 0 deletions src/libs/actions/IOU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7200,6 +7200,17 @@ function canIOUBePaid(
);
}

function canSubmitReport(report: OnyxEntry<OnyxTypes.Report> | SearchReport, policy: OnyxEntry<OnyxTypes.Policy> | 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<OnyxTypes.Report>, excludedIOUReportID: string): OnyxEntry<ReportAction> {
const chatReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`] ?? {};

Expand Down Expand Up @@ -8833,5 +8844,6 @@ export {
getIOUReportActionToApproveOrPay,
getNavigationUrlOnMoneyRequestDelete,
getNavigationUrlAfterTrackExpenseDelete,
canSubmitReport,
};
export type {GPSPoint as GpsPoint, IOURequestType};
41 changes: 39 additions & 2 deletions src/libs/actions/Search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@ 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';
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;
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -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<SearchTransaction>)
: (Object.fromEntries(reportList.map((report) => [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {isActionLoading: isLoading}])) as Partial<SearchReport>),
},
},
];
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[] => [
{
Expand Down Expand Up @@ -369,4 +405,5 @@ export {
payMoneyRequestOnSearch,
approveMoneyRequestOnSearch,
handleActionButtonPress,
submitMoneyRequestOnSearch,
};
17 changes: 16 additions & 1 deletion src/types/onyx/SearchResults.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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 */
Expand Down

0 comments on commit 1584baf

Please sign in to comment.