diff --git a/src/CONST.ts b/src/CONST.ts index 9aef93f1a1b8..3cd522027cd3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -666,6 +666,7 @@ const CONST = { }, TRANSACTION: { DEFAULT_MERCHANT: 'Request', + UNKNOWN_MERCHANT: 'Unknown Merchant', TYPE: { CUSTOM_UNIT: 'customUnit', }, diff --git a/src/components/MenuItem.js b/src/components/MenuItem.js index e284244f694c..adf3fa0cdd80 100644 --- a/src/components/MenuItem.js +++ b/src/components/MenuItem.js @@ -54,6 +54,7 @@ const defaultProps = { disabled: false, isSelected: false, subtitle: undefined, + subtitleTextStyle: {}, iconType: CONST.ICON_TYPE_ICON, onPress: () => {}, onSecondaryInteraction: undefined, @@ -278,7 +279,7 @@ const MenuItem = React.forwardRef((props, ref) => { {/* Since subtitle can be of type number, we should allow 0 to be shown */} {(props.subtitle || props.subtitle === 0) && ( - {props.subtitle} + {props.subtitle} )} {!_.isEmpty(props.floatRightAvatars) && ( diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 61f82fe120a4..5c834a53a00e 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -34,6 +34,7 @@ import PressableWithFeedback from '../Pressable/PressableWithoutFeedback'; import * as ReceiptUtils from '../../libs/ReceiptUtils'; import ReportActionItemImages from './ReportActionItemImages'; import transactionPropTypes from '../transactionPropTypes'; +import colors from '../../styles/colors'; const propTypes = { /** The active IOUReport, used for Onyx subscription */ @@ -152,6 +153,7 @@ function MoneyRequestPreview(props) { const description = requestComment; const hasReceipt = TransactionUtils.hasReceipt(props.transaction); const isScanning = hasReceipt && TransactionUtils.isReceiptBeingScanned(props.transaction); + const hasFieldErrors = TransactionUtils.hasMissingSmartscanFields(props.transaction); const isDistanceRequest = TransactionUtils.isDistanceRequest(props.transaction); const getSettledMessage = () => { @@ -241,6 +243,12 @@ function MoneyRequestPreview(props) { )} + {hasFieldErrors && ( + + )} Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))} + brickRoadIndicator={hasErrors && transactionAmount === 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + subtitle={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''} + subtitleTextStyle={styles.textLabelError} /> @@ -140,6 +145,9 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, trans shouldShowRightIcon={canEdit} titleStyle={styles.flex1} onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))} + brickRoadIndicator={hasErrors && transactionDate === '' ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + subtitle={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''} + subtitleTextStyle={styles.textLabelError} /> @@ -150,6 +158,9 @@ function MoneyRequestView({report, parentReport, shouldShowHorizontalRule, trans shouldShowRightIcon={canEdit} titleStyle={styles.flex1} onPress={() => Navigation.navigate(ROUTES.getEditRequestRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))} + brickRoadIndicator={hasErrors && transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + subtitle={hasErrors && transactionMerchant === CONST.TRANSACTION.UNKNOWN_MERCHANT ? translate('common.error.enterMerchant') : ''} + subtitleTextStyle={styles.textLabelError} /> {shouldShowHorizontalRule && } diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 3c4153fcc5e0..e962d4ea757b 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -32,6 +32,7 @@ import * as ReceiptUtils from '../../libs/ReceiptUtils'; import * as ReportActionUtils from '../../libs/ReportActionsUtils'; import * as TransactionUtils from '../../libs/TransactionUtils'; import ReportActionItemImages from './ReportActionItemImages'; +import colors from '../../styles/colors'; const propTypes = { /** All the data of the action */ @@ -115,11 +116,12 @@ function ReportPreview(props) { const numberOfScanningReceipts = _.filter(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)).length; const hasReceipts = transactionsWithReceipts.length > 0; const isScanning = hasReceipts && ReportUtils.areAllRequestsBeingSmartScanned(props.iouReportID, props.action); + const hasErrors = hasReceipts && ReportUtils.hasMissingSmartscanFields(props.iouReportID); const lastThreeTransactionsWithReceipts = ReportUtils.getReportPreviewDisplayTransactions(props.action); const hasOnlyOneReceiptRequest = numberOfRequests === 1 && hasReceipts; const previewSubtitle = hasOnlyOneReceiptRequest - ? transactionsWithReceipts[0].merchant + ? TransactionUtils.getMerchant(transactionsWithReceipts[0]) : props.translate('iou.requestCount', { count: numberOfRequests, scanningReceipts: numberOfScanningReceipts, @@ -188,6 +190,12 @@ function ReportPreview(props) { {getPreviewMessage()} + {hasErrors && ( + + )} `Exceeds the maximum length of ${limit} characters`, dateInvalid: 'Please select a valid date', invalidCharacter: 'Invalid character', + enterMerchant: 'Enter a merchant name', + enterAmount: 'Enter an amount', + enterDate: 'Enter a date', }, comma: 'comma', semicolon: 'semicolon', @@ -454,6 +457,7 @@ export default { genericCreateFailureMessage: 'Unexpected error requesting money, please try again later', genericDeleteFailureMessage: 'Unexpected error deleting the money request, please try again later', genericEditFailureMessage: 'Unexpected error editing the money request, please try again later', + genericSmartscanFailureMessage: 'Transaction is missing fields', }, }, notificationPreferencesPage: { diff --git a/src/languages/es.js b/src/languages/es.js index 45e69f82210a..2e7ae7dd09eb 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -106,6 +106,9 @@ export default { characterLimit: ({limit}) => `Supera el límite de ${limit} caracteres`, dateInvalid: 'Por favor, selecciona una fecha válida', invalidCharacter: 'Carácter invalido', + enterMerchant: 'Introduce un comerciante', + enterAmount: 'Introduce un importe', + enterDate: 'Introduce una fecha', }, comma: 'la coma', semicolon: 'el punto y coma', @@ -453,6 +456,7 @@ export default { genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde', genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', + genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', }, }, notificationPreferencesPage: { diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2b39c420511e..a9c319865bbb 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -17,6 +17,7 @@ import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as UserUtils from './UserUtils'; import * as ReportActionUtils from './ReportActionsUtils'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; +import * as ErrorUtils from './ErrorUtils'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -350,11 +351,20 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic function getAllReportErrors(report, reportActions) { const reportErrors = report.errors || {}; const reportErrorFields = report.errorFields || {}; - const reportActionErrors = _.reduce( - reportActions, - (prevReportActionErrors, action) => (!action || _.isEmpty(action.errors) ? prevReportActionErrors : _.extend(prevReportActionErrors, action.errors)), - {}, - ); + const reportActionErrors = {}; + _.each(reportActions, (action) => { + if (action && !_.isEmpty(action.errors)) { + _.extend(reportActionErrors, action.errors); + } else if (ReportActionUtils.isReportPreviewAction(action)) { + const iouReportID = ReportActionUtils.getIOUReportIDFromReportActionPreview(action); + + // Instead of adding all Smartscan errors, let's just add a generic error if there are any. This + // will be more performant and provide the same result in the UI + if (ReportUtils.hasMissingSmartscanFields(iouReportID)) { + _.extend(reportActionErrors, {smartscan: ErrorUtils.getMicroSecondOnyxError('report.genericSmartscanFailureMessage')}); + } + } + }); // All error objects related to the report. Each object in the sources contains error messages keyed by microtime const errorSources = { diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 25569c63f2c8..ff9a5ee14519 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1370,6 +1370,17 @@ function areAllRequestsBeingSmartScanned(iouReportID, reportPreviewAction) { return _.all(transactionsWithReceipts, (transaction) => TransactionUtils.isReceiptBeingScanned(transaction)); } +/** + * Check if any of the transactions in the report has required missing fields + * + * @param {Object|null} iouReportID + * @returns {Boolean} + */ +function hasMissingSmartscanFields(iouReportID) { + const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID); + return _.some(transactionsWithReceipts, (transaction) => TransactionUtils.hasMissingSmartscanFields(transaction)); +} + /** * Given a parent IOU report action get report name for the LHN. * @@ -3593,4 +3604,5 @@ export { areAllRequestsBeingSmartScanned, getReportPreviewDisplayTransactions, getTransactionsWithReceipts, + hasMissingSmartscanFields, }; diff --git a/src/libs/TransactionUtils.js b/src/libs/TransactionUtils.js index 7fb07f216f5f..39ec2082d84a 100644 --- a/src/libs/TransactionUtils.js +++ b/src/libs/TransactionUtils.js @@ -85,6 +85,14 @@ function hasReceipt(transaction) { return lodashGet(transaction, 'receipt.state', '') !== ''; } +/** + * @param {Object} transaction + * @returns {Boolean} + */ +function areModifiedFieldsPopulated(transaction) { + return transaction.modifiedMerchant !== CONST.TRANSACTION.UNKNOWN_MERCHANT && transaction.modifiedAmount !== 0 && transaction.modifiedCreated !== ''; +} + /** * Given the edit made to the money request, return an updated transaction object. * @@ -126,6 +134,7 @@ function getUpdatedTransaction(transaction, transactionChanges, isFromExpenseRep if (shouldStopSmartscan && _.has(transaction, 'receipt') && !_.isEmpty(transaction.receipt) && lodashGet(transaction, 'receipt.state') !== CONST.IOU.RECEIPT_STATE.OPEN) { updatedTransaction.receipt.state = CONST.IOU.RECEIPT_STATE.OPEN; } + updatedTransaction.pendingFields = { ...(_.has(transactionChanges, 'comment') && {comment: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), ...(_.has(transactionChanges, 'created') && {created: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}), @@ -228,6 +237,20 @@ function getCreated(transaction) { return ''; } +function isReceiptBeingScanned(transaction) { + return _.contains([CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING], transaction.receipt.state); +} + +/** + * Check if the transaction has a non-smartscanning receipt and is missing required fields + * + * @param {Object} transaction + * @returns {Boolean} + */ +function hasMissingSmartscanFields(transaction) { + return hasReceipt(transaction) && !isReceiptBeingScanned(transaction) && !areModifiedFieldsPopulated(transaction); +} + /** * Get the transactions related to a report preview with receipts * Get the details linked to the IOU reportAction @@ -244,10 +267,6 @@ function getAllReportTransactions(reportID) { return _.filter(allTransactions, (transaction) => transaction.reportID === reportID); } -function isReceiptBeingScanned(transaction) { - return transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANREADY || transaction.receipt.state === CONST.IOU.RECEIPT_STATE.SCANNING; -} - /** * Verifies that the provided waypoints are valid * @param {Object} waypoints @@ -308,4 +327,5 @@ export { isReceiptBeingScanned, validateWaypoints, isDistanceRequest, + hasMissingSmartscanFields, }; diff --git a/src/styles/styles.js b/src/styles/styles.js index 57a8fab32153..c38da2ecc814 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -1114,6 +1114,12 @@ const styles = { color: themeColors.textSupporting, }, + textLabelError: { + fontFamily: fontFamily.EXP_NEUE, + fontSize: variables.fontSizeLabel, + color: themeColors.textError, + }, + textReceiptUpload: { ...headlineFont, fontSize: variables.fontSizeXLarge,