Skip to content

Commit

Permalink
Merge pull request Expensify#32594 from infinitered/money-request-fields
Browse files Browse the repository at this point in the history
  • Loading branch information
cead22 authored Jan 4, 2024
2 parents b9ef51f + 8057dc4 commit 8c19e21
Show file tree
Hide file tree
Showing 13 changed files with 505 additions and 192 deletions.
36 changes: 36 additions & 0 deletions src/CONST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3054,6 +3054,42 @@ const CONST = {
CAROUSEL: 3,
},

VIOLATIONS: {
ALL_TAG_LEVELS_REQUIRED: 'allTagLevelsRequired',
AUTO_REPORTED_REJECTED_EXPENSE: 'autoReportedRejectedExpense',
BILLABLE_EXPENSE: 'billableExpense',
CASH_EXPENSE_WITH_NO_RECEIPT: 'cashExpenseWithNoReceipt',
CATEGORY_OUT_OF_POLICY: 'categoryOutOfPolicy',
CONVERSION_SURCHARGE: 'conversionSurcharge',
CUSTOM_UNIT_OUT_OF_POLICY: 'customUnitOutOfPolicy',
DUPLICATED_TRANSACTION: 'duplicatedTransaction',
FIELD_REQUIRED: 'fieldRequired',
FUTURE_DATE: 'futureDate',
INVOICE_MARKUP: 'invoiceMarkup',
MAX_AGE: 'maxAge',
MISSING_CATEGORY: 'missingCategory',
MISSING_COMMENT: 'missingComment',
MISSING_TAG: 'missingTag',
MODIFIED_AMOUNT: 'modifiedAmount',
MODIFIED_DATE: 'modifiedDate',
NON_EXPENSIWORKS_EXPENSE: 'nonExpensiworksExpense',
OVER_AUTO_APPROVAL_LIMIT: 'overAutoApprovalLimit',
OVER_CATEGORY_LIMIT: 'overCategoryLimit',
OVER_LIMIT: 'overLimit',
OVER_LIMIT_ATTENDEE: 'overLimitAttendee',
PER_DAY_LIMIT: 'perDayLimit',
RECEIPT_NOT_SMART_SCANNED: 'receiptNotSmartScanned',
RECEIPT_REQUIRED: 'receiptRequired',
RTER: 'rter',
SMARTSCAN_FAILED: 'smartscanFailed',
SOME_TAG_LEVELS_REQUIRED: 'someTagLevelsRequired',
TAG_OUT_OF_POLICY: 'tagOutOfPolicy',
TAX_AMOUNT_CHANGED: 'taxAmountChanged',
TAX_OUT_OF_POLICY: 'taxOutOfPolicy',
TAX_RATE_CHANGED: 'taxRateChanged',
TAX_REQUIRED: 'taxRequired',
},

/** Context menu types */
CONTEXT_MENU_TYPES: {
LINK: 'LINK',
Expand Down
90 changes: 77 additions & 13 deletions src/components/ReportActionItem/MoneyRequestView.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import lodashGet from 'lodash/get';
import lodashValues from 'lodash/values';
import PropTypes from 'prop-types';
import React, {useMemo} from 'react';
import React, {useCallback, useMemo} from 'react';
import {View} from 'react-native';
import {withOnyx} from 'react-native-onyx';
import categoryPropTypes from '@components/categoryPropTypes';
Expand All @@ -14,12 +14,14 @@ import Switch from '@components/Switch';
import tagPropTypes from '@components/tagPropTypes';
import Text from '@components/Text';
import transactionPropTypes from '@components/transactionPropTypes';
import ViolationMessages from '@components/ViolationMessages';
import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails';
import useLocalize from '@hooks/useLocalize';
import usePermissions from '@hooks/usePermissions';
import useStyleUtils from '@hooks/useStyleUtils';
import useTheme from '@hooks/useTheme';
import useThemeStyles from '@hooks/useThemeStyles';
import useViolations from '@hooks/useViolations';
import useWindowDimensions from '@hooks/useWindowDimensions';
import * as CardUtils from '@libs/CardUtils';
import compose from '@libs/compose';
Expand All @@ -41,6 +43,32 @@ import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import ReportActionItemImage from './ReportActionItemImage';

const violationNames = lodashValues(CONST.VIOLATIONS);

const transactionViolationPropType = PropTypes.shape({
type: PropTypes.string.isRequired,
name: PropTypes.oneOf(violationNames).isRequired,
data: PropTypes.shape({
rejectedBy: PropTypes.string,
rejectReason: PropTypes.string,
amount: PropTypes.string,
surcharge: PropTypes.number,
invoiceMarkup: PropTypes.number,
maxAge: PropTypes.number,
tagName: PropTypes.string,
formattedLimitAmount: PropTypes.string,
categoryLimit: PropTypes.string,
limit: PropTypes.string,
category: PropTypes.string,
brokenBankConnection: PropTypes.bool,
isAdmin: PropTypes.bool,
email: PropTypes.string,
isTransactionOlderThan7Days: PropTypes.bool,
member: PropTypes.string,
taxName: PropTypes.string,
}),
});

const propTypes = {
/** The report currently being looked at */
report: reportPropTypes.isRequired,
Expand All @@ -61,6 +89,9 @@ const propTypes = {
/** The transaction associated with the transactionThread */
transaction: transactionPropTypes,

/** Violations detected in this transaction */
transactionViolations: PropTypes.arrayOf(transactionViolationPropType),

/** Collection of tags attached to a policy */
policyTags: tagPropTypes,

Expand All @@ -76,10 +107,11 @@ const defaultProps = {
currency: CONST.CURRENCY.USD,
comment: {comment: ''},
},
transactionViolations: [],
policyTags: {},
};

function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy}) {
function MoneyRequestView({report, parentReport, parentReportActions, policyCategories, shouldShowHorizontalRule, transaction, policyTags, policy, transactionViolations}) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
Expand Down Expand Up @@ -131,6 +163,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
const shouldShowTag = isPolicyExpenseChat && (transactionTag || OptionsListUtils.hasEnabledOptions(lodashValues(policyTagsList)));
const shouldShowBillable = isPolicyExpenseChat && (transactionBillable || !lodashGet(policy, 'disabledFields.defaultBillable', true));

const {getViolationsForField} = useViolations(transactionViolations);
const hasViolations = useCallback((field) => canUseViolations && getViolationsForField(field).length > 0, [canUseViolations, getViolationsForField]);

let amountDescription = `${translate('iou.amount')}`;

if (isCardTransaction) {
Expand Down Expand Up @@ -198,6 +233,7 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT))}
/>
)}
{canUseViolations && <ViolationMessages violations={getViolationsForField('receipt')} />}
<OfflineWithFeedback pendingAction={getPendingFieldAction('pendingFields.amount')}>
<MenuItemWithTopDescription
title={formattedTransactionAmount ? formattedTransactionAmount.toString() : ''}
Expand All @@ -208,9 +244,10 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
interactive={canEditAmount}
shouldShowRightIcon={canEditAmount}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.AMOUNT))}
brickRoadIndicator={hasErrors && transactionAmount === 0 ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
brickRoadIndicator={hasViolations('amount') || (hasErrors && transactionAmount === 0) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
error={hasErrors && transactionAmount === 0 ? translate('common.error.enterAmount') : ''}
/>
{canUseViolations && <ViolationMessages violations={getViolationsForField('amount')} />}
</OfflineWithFeedback>
<OfflineWithFeedback pendingAction={getPendingFieldAction('pendingFields.comment')}>
<MenuItemWithTopDescription
Expand All @@ -222,8 +259,10 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DESCRIPTION))}
wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]}
brickRoadIndicator={hasViolations('comment') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
numberOfLinesTitle={0}
/>
{canUseViolations && <ViolationMessages violations={getViolationsForField('comment')} />}
</OfflineWithFeedback>
{isDistanceRequest ? (
<OfflineWithFeedback pendingAction={lodashGet(transaction, 'pendingFields.waypoints') || lodashGet(transaction, 'pendingAction')}>
Expand All @@ -245,9 +284,10 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.MERCHANT))}
brickRoadIndicator={hasErrors && isEmptyMerchant ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
brickRoadIndicator={hasViolations('merchant') || (hasErrors && isEmptyMerchant) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
error={hasErrors && isEmptyMerchant ? translate('common.error.enterMerchant') : ''}
/>
{canUseViolations && <ViolationMessages violations={getViolationsForField('merchant')} />}
</OfflineWithFeedback>
)}
<OfflineWithFeedback pendingAction={getPendingFieldAction('pendingFields.created')}>
Expand All @@ -258,9 +298,10 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit && !isSettled}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DATE))}
brickRoadIndicator={hasErrors && transactionDate === '' ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
brickRoadIndicator={hasViolations('date') || (hasErrors && transactionDate === '') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
error={hasErrors && transactionDate === '' ? translate('common.error.enterDate') : ''}
/>
{canUseViolations && <ViolationMessages violations={getViolationsForField('date')} />}
</OfflineWithFeedback>
{shouldShowCategory && (
<OfflineWithFeedback pendingAction={lodashGet(transaction, 'pendingFields.category') || lodashGet(transaction, 'pendingAction')}>
Expand All @@ -271,7 +312,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.CATEGORY))}
brickRoadIndicator={hasViolations('category') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
/>
{canUseViolations && <ViolationMessages violations={getViolationsForField('category')} />}
</OfflineWithFeedback>
)}
{shouldShowTag && (
Expand All @@ -283,7 +326,9 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
shouldShowRightIcon={canEdit}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.TAG))}
brickRoadIndicator={hasViolations('tag') ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''}
/>
{canUseViolations && <ViolationMessages violations={getViolationsForField('tag')} />}
</OfflineWithFeedback>
)}
{isCardTransaction && (
Expand All @@ -295,15 +340,24 @@ function MoneyRequestView({report, parentReport, parentReportActions, policyCate
/>
</OfflineWithFeedback>
)}

{shouldShowBillable && (
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
<Text color={!transactionBillable ? theme.textSupporting : undefined}>{translate('common.billable')}</Text>
<Switch
accessibilityLabel={translate('common.billable')}
isOn={transactionBillable}
onToggle={(value) => IOU.editMoneyRequest(transaction, report.reportID, {billable: value})}
/>
</View>
<>
<View style={[styles.flexRow, styles.optionRow, styles.justifyContentBetween, styles.alignItemsCenter, styles.ml5, styles.mr8]}>
<Text color={!transactionBillable ? theme.textSupporting : undefined}>{translate('common.billable')}</Text>
<Switch
accessibilityLabel={translate('common.billable')}
isOn={transactionBillable}
onToggle={(value) => IOU.editMoneyRequest(transaction, report.reportID, {billable: value})}
/>
</View>
{hasViolations('billable') && (
<ViolationMessages
violations={getViolationsForField('billable')}
isLast
/>
)}
</>
)}
</View>
<SpacerView
Expand Down Expand Up @@ -349,5 +403,15 @@ export default compose(
return `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`;
},
},
transactionViolation: {
key: ({report}) => {
const parentReportAction = ReportActionsUtils.getParentReportAction(report);
const transactionID = lodashGet(parentReportAction, ['originalMessage', 'IOUTransactionID'], 0);
return `${ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS}${transactionID}`;
},
},
policyTags: {
key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${report.policyID}`,
},
}),
)(MoneyRequestView);
26 changes: 26 additions & 0 deletions src/components/ViolationMessages.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import React, {useMemo} from 'react';
import {View} from 'react-native';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import ViolationsUtils from '@libs/ViolationsUtils';
import type {TransactionViolation} from '@src/types/onyx';
import Text from './Text';

export default function ViolationMessages({violations, isLast}: {violations: TransactionViolation[]; isLast?: boolean}) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const violationMessages = useMemo(() => violations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate)]), [translate, violations]);

return (
<View style={[styles.mtn2, isLast ? styles.mb2 : styles.mb1]}>
{violationMessages.map(([name, message]) => (
<Text
key={`violationMessages.${name}`}
style={[styles.ph5, styles.textLabelError]}
>
{message}
</Text>
))}
</View>
);
}
7 changes: 3 additions & 4 deletions src/hooks/useViolations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ import {useCallback, useMemo} from 'react';
import type {TransactionViolation, ViolationName} from '@src/types/onyx';

/**
* Names of Fields where violations can occur
* Names of Fields where violations can occur.
*/
type ViolationField = 'amount' | 'billable' | 'category' | 'comment' | 'date' | 'merchant' | 'receipt' | 'tag' | 'tax';

/**
* Map from Violation Names to the field where that violation can occur
* Map from Violation Names to the field where that violation can occur.
*/
const violationFields: Record<ViolationName, ViolationField> = {
allTagLevelsRequired: 'tag',
Expand Down Expand Up @@ -60,13 +60,12 @@ function useViolations(violations: TransactionViolation[]) {
return violationGroups ?? new Map();
}, [violations]);

const hasViolations = useCallback((field: ViolationField) => Boolean(violationsByField.get(field)?.length), [violationsByField]);
const getViolationsForField = useCallback((field: ViolationField) => violationsByField.get(field) ?? [], [violationsByField]);

return {
hasViolations,
getViolationsForField,
};
}

export default useViolations;
export type {ViolationField};
Loading

0 comments on commit 8c19e21

Please sign in to comment.