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 markAsCash button with wired up action for dismissing the rter violation #41835

Merged
merged 12 commits into from
May 23, 2024
12 changes: 6 additions & 6 deletions src/components/MoneyReportHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ function MoneyReportHeader({
const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false);

const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID);
const haveAllPendingRTERViolation = TransactionUtils.haveAllPendingRTERViolation(transactionIDs);
const hasAllPendingRTERViolations = TransactionUtils.hasAllPendingRTERViolations(transactionIDs);

const cancelPayment = useCallback(() => {
if (!chatReport) {
Expand All @@ -121,13 +121,13 @@ function MoneyReportHeader({

const shouldDisableApproveButton = shouldShowApproveButton && !ReportUtils.isAllowedToApproveExpenseReport(moneyRequestReport);

const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(moneyRequestReport) && (shouldShowPayButton || shouldShowApproveButton) && !haveAllPendingRTERViolation;
const shouldShowSettlementButton = !ReportUtils.isInvoiceReport(moneyRequestReport) && (shouldShowPayButton || shouldShowApproveButton) && !hasAllPendingRTERViolations;

const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !haveAllPendingRTERViolation;
const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !hasAllPendingRTERViolations;
const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport);
const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE;
const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep;
const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || hasAllPendingRTERViolations;
const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport);
const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency);
const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy);
Expand Down Expand Up @@ -212,7 +212,7 @@ function MoneyReportHeader({
shouldShowBackButton={shouldUseNarrowLayout}
onBackButtonPress={onBackButtonPress}
// Shows border if no buttons or next steps are showing below the header
shouldShowBorderBottom={!(shouldShowAnyButton && shouldUseNarrowLayout) && !(shouldShowNextStep && !shouldUseNarrowLayout) && !haveAllPendingRTERViolation}
shouldShowBorderBottom={!(shouldShowAnyButton && shouldUseNarrowLayout) && !(shouldShowNextStep && !shouldUseNarrowLayout) && !hasAllPendingRTERViolations}
shouldShowThreeDotsButton
threeDotsMenuItems={threeDotsMenuItems}
threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)}
Expand Down Expand Up @@ -250,7 +250,7 @@ function MoneyReportHeader({
</View>
)}
</HeaderWithBackButton>
{haveAllPendingRTERViolation && (
{hasAllPendingRTERViolations && (
<MoneyRequestHeaderStatusBar
title={
<Icon
Expand Down
18 changes: 17 additions & 1 deletion src/components/MoneyRequestHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@ import * as ReportUtils from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import variables from '@styles/variables';
import * as IOU from '@userActions/IOU';
import * as TransactionActions from '@userActions/Transaction';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {Policy, Report, ReportAction, ReportActions, Session, Transaction, TransactionViolations} from '@src/types/onyx';
import type {OriginalMessageIOU} from '@src/types/onyx/OriginalMessage';
import Button from './Button';
import ConfirmModal from './ConfirmModal';
import HeaderWithBackButton from './HeaderWithBackButton';
import Icon from './Icon';
Expand Down Expand Up @@ -90,6 +92,7 @@ function MoneyRequestHeader({
const isActionOwner = typeof parentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && parentReportAction.actorAccountID === session?.accountID;
const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN;
const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID;
const hasAllPendingRTERViolations = TransactionUtils.hasAllPendingRTERViolations([transaction?.transactionID ?? '']);

const deleteTransaction = useCallback(() => {
if (parentReportAction) {
Expand Down Expand Up @@ -232,7 +235,7 @@ function MoneyRequestHeader({
)
}
description={pendingDescription}
shouldShowBorderBottom={!isOnHold}
shouldShowBorderBottom={!isOnHold && !hasAllPendingRTERViolations}
additionalViewStyle={[styles.mr2]}
/>
)}
Expand All @@ -244,6 +247,19 @@ function MoneyRequestHeader({
danger
/>
)}
{hasAllPendingRTERViolations && (
<View style={[styles.ph5, styles.pb3, styles.borderBottom]}>
<Button
medium
success
text={translate('iou.markAsCash')}
style={[styles.w100, styles.pr0]}
onPress={() => {
TransactionActions.markAsCash(transaction?.transactionID ?? '', transaction?.reportID ?? '');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
TransactionActions.markAsCash(transaction?.transactionID ?? '', transaction?.reportID ?? '');
TransactionActions.markAsCash(transaction?.transactionID ?? '', report?.reportID ?? '');

I think it is better to extract this to a local function because it is used twice and in the last commit only one instance was updated.

}}
/>
</View>
)}
</View>
<ConfirmModal
title={translate('iou.deleteExpense')}
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,7 @@ export default {
deleteReceipt: 'Delete receipt',
pendingMatchWithCreditCard: 'Receipt pending match with credit card.',
pendingMatchWithCreditCardDescription: 'Receipt pending match with credit card. Marks as cash to ignore and request payment.',
markAsCash: 'Mark as cash',
routePending: 'Route pending...',
receiptScanning: 'Receipt scanning…',
receiptScanInProgress: 'Receipt scan in progress.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,7 @@ export default {
deleteReceipt: 'Eliminar recibo',
pendingMatchWithCreditCard: 'Recibo pendiente de conciliar con la tarjeta de crédito.',
pendingMatchWithCreditCardDescription: 'Recibo pendiente de conciliar con tarjeta de crédito. Marcar como efectivo para ignorar y solicitar pago.',
markAsCash: 'Marcar como efectivo',
routePending: 'Ruta pendiente...',
receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`,
fieldPending: 'Pendiente...',
Expand Down
6 changes: 6 additions & 0 deletions src/libs/API/parameters/MarkAsCashParams.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
type MarkAsCashParams = {
transactionID: string;
reportActionID: string;
};

export default MarkAsCashParams;
1 change: 1 addition & 0 deletions src/libs/API/parameters/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -220,3 +220,4 @@ export type {default as LeavePolicyParams} from './LeavePolicyParams';
export type {default as OpenPolicyAccountingPageParams} from './OpenPolicyAccountingPageParams';
export type {default as SearchParams} from './Search';
export type {default as SendInvoiceParams} from './SendInvoiceParams';
export type {default as MarkAsCashParams} from './MarkAsCashParams';
2 changes: 2 additions & 0 deletions src/libs/API/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,7 @@ const WRITE_COMMANDS = {
LEAVE_POLICY: 'LeavePolicy',
ACCEPT_SPOTNANA_TERMS: 'AcceptSpotnanaTerms',
SEND_INVOICE: 'SendInvoice',
MARK_AS_CASH: 'MarkAsCash',
} as const;

type WriteCommand = ValueOf<typeof WRITE_COMMANDS>;
Expand Down Expand Up @@ -432,6 +433,7 @@ type WriteCommandParameters = {
[WRITE_COMMANDS.LEAVE_POLICY]: Parameters.LeavePolicyParams;
[WRITE_COMMANDS.ACCEPT_SPOTNANA_TERMS]: EmptyObject;
[WRITE_COMMANDS.SEND_INVOICE]: Parameters.SendInvoiceParams;
[WRITE_COMMANDS.MARK_AS_CASH]: Parameters.MarkAsCashParams;
};

const READ_COMMANDS = {
Expand Down
38 changes: 38 additions & 0 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import type {
IOUMessage,
OriginalMessageActionName,
OriginalMessageCreated,
OriginalMessageDismissedViolation,
OriginalMessageReimbursementDequeued,
OriginalMessageRenamed,
OriginalMessageRoomChangeLog,
Expand Down Expand Up @@ -255,6 +256,11 @@ type OptimisticClosedReportAction = Pick<
'actionName' | 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'pendingAction' | 'person' | 'reportActionID' | 'shouldShow'
>;

type OptimisticDismissedViolationReportAction = Pick<
ReportAction,
'actionName' | 'actorAccountID' | 'avatar' | 'created' | 'message' | 'originalMessage' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction'
>;

type OptimisticCreatedReportAction = OriginalMessageCreated &
Pick<ReportActionBase, 'actorAccountID' | 'automatic' | 'avatar' | 'created' | 'message' | 'person' | 'reportActionID' | 'shouldShow' | 'pendingAction'>;

Expand Down Expand Up @@ -4650,6 +4656,37 @@ function buildOptimisticClosedReportAction(emailClosingReport: string, policyNam
};
}

/**
* Returns an optimistic Dismissed Violation Report Action. Use the originalMessage customize this to the type of
* violation being dismissed.
*/
function buildOptimisticDismissedViolationReportAction(originalMessage: OriginalMessageDismissedViolation['originalMessage']): OptimisticDismissedViolationReportAction {
return {
actionName: CONST.REPORT.ACTIONS.TYPE.DISMISSED_VIOLATION,
actorAccountID: currentUserAccountID,
avatar: getCurrentUserAvatarOrDefault(),
created: DateUtils.getDBTime(),
message: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'normal',
text: ReportActionsUtils.getDismissedViolationMessageText(originalMessage),
},
],
originalMessage,
pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD,
person: [
{
type: CONST.REPORT.MESSAGE.TYPE.TEXT,
style: 'strong',
text: getCurrentUserDisplayNameOrEmail(),
},
],
reportActionID: NumberUtils.rand64(),
shouldShow: true,
};
}

function buildOptimisticWorkspaceChats(policyID: string, policyName: string, expenseReportId?: string): OptimisticWorkspaceChats {
const announceChatData = buildOptimisticChatReport(
currentUserAccountID ? [currentUserAccountID] : [],
Expand Down Expand Up @@ -6516,6 +6553,7 @@ export {
buildOptimisticChatReport,
buildOptimisticClosedReportAction,
buildOptimisticCreatedReportAction,
buildOptimisticDismissedViolationReportAction,
buildOptimisticEditedTaskFieldReportAction,
buildOptimisticExpenseReport,
buildOptimisticGroupChatReport,
Expand Down
4 changes: 2 additions & 2 deletions src/libs/TransactionUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -524,7 +524,7 @@ function hasPendingRTERViolation(transactionViolations?: TransactionViolations |
/**
* Check if there is pending rter violation in transactionViolations.
*/
function haveAllPendingRTERViolation(transactionIds: string[]): boolean {
function hasAllPendingRTERViolations(transactionIds: string[]): boolean {
const transactionsWithRTERViolations = transactionIds.map((transactionId) => hasPendingRTERViolation(getTransactionViolations(transactionId, allTransactionViolations)));
return transactionsWithRTERViolations.length !== 0 && transactionsWithRTERViolations.every((value) => value === true);
}
Expand Down Expand Up @@ -750,7 +750,7 @@ export {
areRequiredFieldsEmpty,
hasMissingSmartscanFields,
hasPendingRTERViolation,
haveAllPendingRTERViolation,
hasAllPendingRTERViolations,
hasPendingUI,
getWaypointIndex,
waypointHasValidAddress,
Expand Down
63 changes: 59 additions & 4 deletions src/libs/actions/Transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import lodashHas from 'lodash/has';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import * as API from '@libs/API';
import type {GetRouteParams} from '@libs/API/parameters';
import {READ_COMMANDS} from '@libs/API/types';
import type {GetRouteParams, MarkAsCashParams} from '@libs/API/parameters';
import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types';
import * as CollectionUtils from '@libs/CollectionUtils';
import {buildOptimisticDismissedViolationReportAction} from '@libs/ReportUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {RecentWaypoint, Transaction} from '@src/types/onyx';
import type {RecentWaypoint, ReportActions, Transaction, TransactionViolation, TransactionViolations} from '@src/types/onyx';
import type {OnyxData} from '@src/types/onyx/Request';
import type {WaypointCollection} from '@src/types/onyx/Transaction';

Expand All @@ -32,6 +33,12 @@ Onyx.connect({
},
});

let allTransactionViolations: TransactionViolations = [];
Onyx.connect({
key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS,
callback: (val) => (allTransactionViolations = val ?? []),
});

function createInitialWaypoints(transactionID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {
comment: {
Expand Down Expand Up @@ -264,4 +271,52 @@ function clearError(transactionID: string) {
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, {errors: null});
}

export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, updateWaypoints, clearError};
function markAsCash(transactionID: string, transactionThreadReportID: string) {
const optimisticReportAction = buildOptimisticDismissedViolationReportAction({
reason: 'manual',
violationName: CONST.VIOLATIONS.RTER,
});
const optimisticReportActions = {
[optimisticReportAction.reportActionID]: optimisticReportAction,
};
const onyxData: OnyxData = {
optimisticData: [
// Optimistically dismissing the violation, removing it from the list of violations
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
value: allTransactionViolations.filter((violation: TransactionViolation) => violation.name !== CONST.VIOLATIONS.RTER),
},
// Optimistically adding the system message indicating we dismissed the violation
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
value: optimisticReportActions as ReportActions,
},
],
failureData: [
// Rolling back the dismissal of the violation
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`,
value: allTransactionViolations,
},
{
onyxMethod: Onyx.METHOD.MERGE,
key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`,
value: {
[optimisticReportAction.reportActionID]: null,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just want to make sure: which offline pattern do we follow here? Is it B (Optimistic WITH Feedback Pattern)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's from the other branch that has already been merged, but @yuwenmemon probably has more context regarding this action

},
},
],
};

const parameters: MarkAsCashParams = {
transactionID,
reportActionID: optimisticReportAction.reportActionID,
};

return API.write(WRITE_COMMANDS.MARK_AS_CASH, parameters, onyxData);
}

export {addStop, createInitialWaypoints, saveWaypoint, removeWaypoint, getRoute, updateWaypoints, clearError, markAsCash};
24 changes: 0 additions & 24 deletions src/pages/home/report/ReportActionsList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import React, {memo, useCallback, useEffect, useMemo, useRef, useState} from 're
import {DeviceEventEmitter, InteractionManager} from 'react-native';
import type {LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';
import Onyx from 'react-native-onyx';
import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated';
import InvertedFlatList from '@components/InvertedFlatList';
import {AUTOSCROLL_TO_TOP_THRESHOLD} from '@components/InvertedFlatList/BaseInvertedFlatList';
Expand All @@ -27,7 +26,6 @@ import type {CentralPaneNavigatorParamList} from '@navigation/types';
import variables from '@styles/variables';
import * as Report from '@userActions/Report';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';
Expand Down Expand Up @@ -191,28 +189,6 @@ function ReportActionsList({
const lastVisibleActionCreatedRef = useRef(report.lastVisibleActionCreated);
const lastReadTimeRef = useRef(report.lastReadTime);
// Single MoneyRequest
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need this comment?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is showing as a change, because ola removed it in her PR and this branch was based on it

// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}6196867412357270168`, {cardID: 1, merchant: 'single MoneyRequest test'});
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}6196867412357270168`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]);
// Multiple MoneyRequests test
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}1304796714991934480`, {cardID: 1, merchant: 'multiple MoneyRequests test 1'});
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}1304796714991934480`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]);
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}6286508495235425496`, {cardID: 1, merchant: 'multiple MoneyRequests test 2'});
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}6286508495235425496`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]);
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}2150079702326626524`, {cardID: 1, merchant: 'multiple MoneyRequests test 3'});
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}2150079702326626524`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]);
// One-Expense Chat test
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}2438117170083649063`, {cardID: 1, merchant: 'One-Expense Chat test'});
// eslint-disable-next-line rulesdir/prefer-actions-set-data
Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}2438117170083649063`, [{type: 'test', name: CONST.VIOLATIONS.RTER, data: {pendingPattern: true}}]);

const sortedVisibleReportActions = useMemo(
() =>
Expand Down
Loading