Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add submit action #53641

Merged
merged 16 commits into from
Dec 11, 2024
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 @@ -7149,6 +7149,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 @@ -8781,5 +8792,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
Loading