From 19429e9c9129702b3bedb03e6277a6d0b5c1ea7e Mon Sep 17 00:00:00 2001 From: tienifr Date: Thu, 13 Jun 2024 05:25:50 +0700 Subject: [PATCH 1/4] feature: hold transaction in one transaction view --- src/components/MoneyReportHeader.tsx | 243 +++++++++++------- src/components/MoneyRequestHeader.tsx | 25 +- .../MoneyRequestHeaderStatusBar.tsx | 20 +- src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/ReportUtils.ts | 6 +- src/libs/TransactionUtils.ts | 6 +- src/pages/iou/SplitBillDetailsPage.tsx | 27 +- 8 files changed, 191 insertions(+), 138 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index d3cf50827cec..5915cff51d95 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -22,6 +22,7 @@ import type {Route} from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; import type {PaymentMethodType} from '@src/types/onyx/OriginalMessage'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type IconAsset from '@src/types/utils/IconAsset'; import Button from './Button'; import ConfirmModal from './ConfirmModal'; import HeaderWithBackButton from './HeaderWithBackButton'; @@ -29,8 +30,10 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MoneyReportHeaderStatusBar from './MoneyReportHeaderStatusBar'; import MoneyRequestHeaderStatusBar from './MoneyRequestHeaderStatusBar'; +import type {MoneyRequestHeaderStatusBarProps} from './MoneyRequestHeaderStatusBar'; import type {ActionHandledType} from './ProcessMoneyReportHoldMenu'; import ProcessMoneyReportHoldMenu from './ProcessMoneyReportHoldMenu'; +import ProcessMoneyRequestHoldMenu from './ProcessMoneyRequestHoldMenu'; import SettlementButton from './SettlementButton'; type MoneyReportHeaderProps = { @@ -59,25 +62,35 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const [nextStep] = useOnyx(`${ONYXKEYS.COLLECTION.NEXT_STEP}${moneyRequestReport.reportID}`); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`); const [session] = useOnyx(ONYXKEYS.SESSION); + const requestParentReportAction = useMemo(() => { + if (!reportActions || !transactionThreadReport?.parentReportActionID) { + return null; + } + return reportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID) as OnyxTypes.ReportAction & OnyxTypes.OriginalMessageIOU; + }, [reportActions, transactionThreadReport?.parentReportActionID]); + const [transactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const [shownHoldUseExplanation] = useOnyx(ONYXKEYS.NVP_HOLD_USE_EXPLAINED, {initWithStoredValues: false}); + const transaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${requestParentReportAction?.originalMessage?.IOUTransactionID ?? 0}`] ?? null; const styles = useThemeStyles(); const theme = useTheme(); const [isDeleteRequestModalVisible, setIsDeleteRequestModalVisible] = useState(false); + const [shouldShowHoldMenu, setShouldShowHoldMenu] = useState(false); const {translate} = useLocalize(); const {windowWidth} = useWindowDimensions(); const {reimbursableSpend} = ReportUtils.getMoneyRequestSpendBreakdown(moneyRequestReport); const isSettled = ReportUtils.isSettled(moneyRequestReport.reportID); - const requestParentReportAction = useMemo(() => { - if (!reportActions || !transactionThreadReport?.parentReportActionID) { - return null; - } - return reportActions.find((action) => action.reportActionID === transactionThreadReport.parentReportActionID); - }, [reportActions, transactionThreadReport?.parentReportActionID]); + const isApproved = ReportUtils.isReportApproved(moneyRequestReport); + const isOnHold = TransactionUtils.isOnHold(transaction); + const isScanning = TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction); const isDeletedParentAction = ReportActionsUtils.isDeletedAction(requestParentReportAction as OnyxTypes.ReportAction); + const canHoldOrUnholdRequest = !isEmptyObject(transaction) && !isSettled && !isApproved && !isDeletedParentAction; // Only the requestor can delete the request, admins can only edit it. const isActionOwner = typeof requestParentReportAction?.actorAccountID === 'number' && typeof session?.accountID === 'number' && requestParentReportAction.actorAccountID === session?.accountID; + const isPolicyAdmin = policy?.role === CONST.POLICY.ROLE.ADMIN; + const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; const canDeleteRequest = isActionOwner && (ReportUtils.canAddOrDeleteTransactions(moneyRequestReport) || ReportUtils.isTrackExpenseReport(transactionThreadReport)) && !isDeletedParentAction; const [isHoldMenuVisible, setIsHoldMenuVisible] = useState(false); @@ -90,8 +103,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); const navigateBackToAfterDelete = useRef(); - const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); - const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((transaction) => transaction.transactionID); + const hasScanningReceipt = ReportUtils.getTransactionsWithReceipts(moneyRequestReport?.reportID).some((t) => TransactionUtils.isReceiptBeingScanned(t)); + const transactionIDs = TransactionUtils.getAllReportTransactions(moneyRequestReport?.reportID).map((t) => t.transactionID); const allHavePendingRTERViolation = TransactionUtils.allHavePendingRTERViolation(transactionIDs); const cancelPayment = useCallback(() => { @@ -110,18 +123,20 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea const shouldShowSettlementButton = (shouldShowPayButton || shouldShowApproveButton) && !allHavePendingRTERViolation; - const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation; + // allTransactions in TransactionUtils might have stale data + const hasOnlyHeldExpenses = ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID, transactions); + const shouldShowSubmitButton = isDraft && reimbursableSpend !== 0 && !allHavePendingRTERViolation && !hasOnlyHeldExpenses; const shouldDisableSubmitButton = shouldShowSubmitButton && !ReportUtils.isAllowedToSubmitDraftExpenseReport(moneyRequestReport); - const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation; + const shouldShowMarkAsCashButton = isDraft && allHavePendingRTERViolation && !hasOnlyHeldExpenses; const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - const shouldShowNextStep = - !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !allHavePendingRTERViolation && !hasScanningReceipt; + const shouldShowStatusBar = allHavePendingRTERViolation || hasOnlyHeldExpenses || hasScanningReceipt; + const shouldShowNextStep = !ReportUtils.isClosedExpenseReportWithNoExpenses(moneyRequestReport) && isFromPaidPolicy && !!nextStep?.message?.length && !shouldShowStatusBar; const shouldShowAnyButton = shouldShowSettlementButton || shouldShowApproveButton || shouldShowSubmitButton || shouldShowNextStep || allHavePendingRTERViolation; const bankAccountRoute = ReportUtils.getBankAccountRoute(chatReport); const formattedAmount = CurrencyUtils.convertToDisplayString(reimbursableSpend, moneyRequestReport.currency); const [nonHeldAmount, fullAmount] = ReportUtils.getNonHeldAndFullAmount(moneyRequestReport, policy); const displayedAmount = ReportUtils.hasHeldExpenses(moneyRequestReport.reportID) && canAllowSettlement ? nonHeldAmount : formattedAmount; - const isMoreContentShown = shouldShowNextStep || hasScanningReceipt || (shouldShowAnyButton && shouldUseNarrowLayout); + const isMoreContentShown = shouldShowNextStep || shouldShowStatusBar || (shouldShowAnyButton && shouldUseNarrowLayout); const confirmPayment = (type?: PaymentMethodType | undefined) => { if (!type || !chatReport) { @@ -170,6 +185,43 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea TransactionActions.markAsCash(iouTransactionID, reportID); }, [requestParentReportAction, transactionThreadReport?.reportID]); + const changeMoneyRequestStatus = () => { + if (!transactionThreadReport) { + return; + } + const transactionID = requestParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? requestParentReportAction.originalMessage?.IOUTransactionID ?? '' : ''; + + if (isOnHold) { + IOU.unholdRequest(transactionID, transactionThreadReport.reportID); + } else { + const activeRoute = encodeURIComponent(Navigation.getActiveRouteWithoutParams()); + Navigation.navigate(ROUTES.MONEY_REQUEST_HOLD_REASON.getRoute(policy?.type ?? CONST.POLICY.TYPE.PERSONAL, transactionID, transactionThreadReport.reportID, activeRoute)); + } + }; + + const getStatusIcon: (src: IconAsset) => React.ReactNode = (src) => ( + + ); + + const getStatusBarProps: () => MoneyRequestHeaderStatusBarProps | undefined = () => { + if (hasOnlyHeldExpenses) { + return {title: translate('iou.hold'), description: translate('iou.expensesOnHold'), danger: true}; + } + if (allHavePendingRTERViolation) { + return {title: getStatusIcon(Expensicons.Hourglass), description: translate('iou.pendingMatchWithCreditCardDescription')}; + } + if (hasScanningReceipt) { + return {title: getStatusIcon(Expensicons.ReceiptScan), description: translate('iou.receiptScanInProgressDescription')}; + } + }; + + const statusBarProps = getStatusBarProps(); + // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on const isWaitingForSubmissionFromCurrentUser = useMemo( () => chatReport?.isOwnPolicyExpenseChat && !policy?.harvesting?.enabled, @@ -177,6 +229,49 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea ); const threeDotsMenuItems = [HeaderUtils.getPinMenuItem(moneyRequestReport)]; + if (canHoldOrUnholdRequest) { + const isRequestIOU = ReportUtils.isIOUReport(chatReport); + const isHoldCreator = ReportUtils.isHoldCreator(transaction, moneyRequestReport?.reportID) && isRequestIOU; + const isTrackExpenseReport = ReportUtils.isTrackExpenseReport(moneyRequestReport); + const canModifyStatus = !isTrackExpenseReport && (isPolicyAdmin || isActionOwner || isApprover); + if (isOnHold && (isHoldCreator || (!isRequestIOU && canModifyStatus))) { + threeDotsMenuItems.push({ + icon: Expensicons.Stopwatch, + text: translate('iou.unholdExpense'), + onSelected: () => changeMoneyRequestStatus(), + }); + } + if (!isOnHold && (isRequestIOU || canModifyStatus) && !isScanning) { + threeDotsMenuItems.push({ + icon: Expensicons.Stopwatch, + text: translate('iou.hold'), + onSelected: () => changeMoneyRequestStatus(), + }); + } + } + + useEffect(() => { + setShouldShowHoldMenu(isOnHold && !shownHoldUseExplanation); + }, [isOnHold, shownHoldUseExplanation]); + + useEffect(() => { + if (!shouldShowHoldMenu) { + return; + } + + if (shouldUseNarrowLayout) { + if (Navigation.getActiveRoute().slice(1) === ROUTES.PROCESS_MONEY_REQUEST_HOLD) { + Navigation.goBack(); + } + } else { + Navigation.navigate(ROUTES.PROCESS_MONEY_REQUEST_HOLD); + } + }, [shouldUseNarrowLayout, shouldShowHoldMenu]); + + const handleHoldRequestClose = () => { + IOU.setShownHoldUseExplanation(); + }; + if (isPayer && isSettled && ReportUtils.isExpenseReport(moneyRequestReport)) { threeDotsMenuItems.push({ icon: Expensicons.Trashcan, @@ -212,8 +307,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea policy={policy} shouldShowBackButton={shouldUseNarrowLayout} onBackButtonPress={onBackButtonPress} - // Shows border if no buttons or next steps are showing below the header - shouldShowBorderBottom={!isMoreContentShown && !allHavePendingRTERViolation} + // Shows border if no buttons or banners are showing below the header + shouldShowBorderBottom={!isMoreContentShown} shouldShowThreeDotsButton threeDotsMenuItems={threeDotsMenuItems} threeDotsAnchorPosition={styles.threeDotsPopoverOffsetNoCloseButton(windowWidth)} @@ -233,7 +328,7 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea shouldShowApproveButton={shouldShowApproveButton} shouldDisableApproveButton={shouldDisableApproveButton} style={[styles.pv2]} - formattedAmount={!ReportUtils.hasOnlyHeldExpenses(moneyRequestReport.reportID) ? displayedAmount : ''} + formattedAmount={!hasOnlyHeldExpenses ? displayedAmount : ''} isDisabled={!canAllowSettlement} /> @@ -262,88 +357,55 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea )} - {allHavePendingRTERViolation && ( - - {shouldShowMarkAsCashButton && shouldUseNarrowLayout && ( - -