diff --git a/src/components/MoneyRequestHeader.tsx b/src/components/MoneyRequestHeader.tsx index 5db2839654a0..6f5c54325d88 100644 --- a/src/components/MoneyRequestHeader.tsx +++ b/src/components/MoneyRequestHeader.tsx @@ -78,7 +78,7 @@ function MoneyRequestHeader({ // Only the requestor can take delete the expense, admins can only edit it. 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) && (session?.accountID ?? null) === moneyRequestReport?.managerID; + const isApprover = ReportUtils.isMoneyRequestReport(moneyRequestReport) && moneyRequestReport?.managerID !== null && session?.accountID === moneyRequestReport?.managerID; const deleteTransaction = useCallback(() => { if (parentReportAction) { diff --git a/src/components/ReceiptAudit.tsx b/src/components/ReceiptAudit.tsx new file mode 100644 index 000000000000..ac1b36c6bf32 --- /dev/null +++ b/src/components/ReceiptAudit.tsx @@ -0,0 +1,42 @@ +import React from 'react'; +import {View} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import Text from './Text'; + +function ReceiptAuditHeader({notes, shouldShowAuditMessage}: {notes: string[]; shouldShowAuditMessage: boolean}) { + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + + const auditText = notes.length > 0 ? translate('iou.receiptIssuesFound', notes.length) : translate('common.verified'); + return ( + + + {translate('common.receipt')} + {shouldShowAuditMessage && ( + <> + {` • ${auditText}`} + + + )} + + + ); +} + +function ReceiptAuditMessages({notes = []}: {notes?: string[]}) { + const styles = useThemeStyles(); + return {notes.length > 0 && notes.map((message) => {message})}; +} + +export {ReceiptAuditHeader, ReceiptAuditMessages}; diff --git a/src/components/ReceiptEmptyState.tsx b/src/components/ReceiptEmptyState.tsx index 9884e97a3fa0..71d64c7483f1 100644 --- a/src/components/ReceiptEmptyState.tsx +++ b/src/components/ReceiptEmptyState.tsx @@ -12,10 +12,12 @@ type ReceiptEmptyStateProps = { /** Callback to be called on onPress */ onPress?: () => void; + + disabled?: boolean; }; // Returns an SVG icon indicating that the user should attach a receipt -function ReceiptEmptyState({hasError = false, onPress = () => {}}: ReceiptEmptyStateProps) { +function ReceiptEmptyState({hasError = false, onPress = () => {}, disabled = false}: ReceiptEmptyStateProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -24,6 +26,8 @@ function ReceiptEmptyState({hasError = false, onPress = () => {}}: ReceiptEmptyS accessibilityRole="imagebutton" accessibilityLabel={translate('receipt.upload')} onPress={onPress} + disabled={disabled} + disabledStyle={styles.cursorDefault} style={[styles.alignItemsCenter, styles.justifyContentCenter, styles.moneyRequestViewImage, styles.moneyRequestAttachReceipt, hasError && styles.borderColorDanger]} > ); + const shouldShowMapOrReceipt = showMapAsImage || hasReceipt; + const shouldShowReceiptEmptyState = !hasReceipt && (canEditReceipt || isAdmin || isApprover); + const noticeTypeViolations = transactionViolations?.filter((violation) => violation.type === 'notice').map((v) => ViolationsUtils.getViolationTranslation(v, translate)) ?? []; + const shouldShowNotesViolations = !isReceiptBeingScanned && canUseViolations && ReportUtils.isPaidGroupPolicy(report); + return ( {shouldShowAnimatedBackground && } - {/* eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing */} - {(showMapAsImage || hasReceipt) && ( + + {shouldShowMapOrReceipt && ( )} - {!hasReceipt && canEditReceipt && ( + {shouldShowReceiptEmptyState && ( Navigation.navigate( ROUTES.MONEY_REQUEST_STEP_SCAN.getRoute( @@ -375,6 +391,8 @@ function MoneyRequestView({ } /> )} + {!shouldShowReceiptEmptyState && !shouldShowMapOrReceipt && } + {shouldShowNotesViolations && } {canUseViolations && } violations.map((violation) => [violation.name, ViolationsUtils.getViolationTranslation(violation, translate)]), [translate, violations]); return ( - + {violationMessages.map(([name, message]) => ( `${count === 1 ? 'Issue' : 'Issues'} found`, fieldPending: 'Pending...', defaultRate: 'Default rate', receiptScanning: 'Scan in progress…', diff --git a/src/languages/es.ts b/src/languages/es.ts index c7a1592c6d06..f132fe15027a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -287,6 +287,7 @@ export default { nonBillable: 'No facturable', tag: 'Etiqueta', receipt: 'Recibo', + verified: 'Verificado', replace: 'Sustituir', distance: 'Distancia', mile: 'milla', @@ -628,6 +629,7 @@ export default { canceled: 'Canceló', posted: 'Contabilizado', deleteReceipt: 'Eliminar recibo', + receiptIssuesFound: (count: number) => `${count === 1 ? 'Problema encontrado' : 'Problemas encontrados'}`, fieldPending: 'Pendiente...', defaultRate: 'Tasa predeterminada', receiptScanning: 'Escaneo en curso…', diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index 73efe4083623..74bc15f32487 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -491,6 +491,10 @@ function isReceiptBeingScanned(transaction: OnyxEntry): boolean { return [CONST.IOU.RECEIPT_STATE.SCANREADY, CONST.IOU.RECEIPT_STATE.SCANNING].some((value) => value === transaction?.receipt?.state); } +function didRceiptScanSucceed(transaction: OnyxEntry): boolean { + return [CONST.IOU.RECEIPT_STATE.SCANCOMPLETE].some((value) => value === transaction?.receipt?.state); +} + /** * Check if the transaction has a non-smartscanning receipt and is missing required fields */ @@ -606,6 +610,13 @@ function hasViolation(transactionID: string, transactionViolations: OnyxCollecti ); } +/** + * Checks if any violations for the provided transaction are of type 'notice' + */ +function hasNoticeTypeViolation(transactionID: string, transactionViolations: OnyxCollection): boolean { + return Boolean(transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID]?.some((violation: TransactionViolation) => violation.type === 'notice')); +} + function getTransactionViolations(transactionID: string, transactionViolations: OnyxCollection): TransactionViolation[] | null { return transactionViolations?.[ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS + transactionID] ?? null; } @@ -691,6 +702,7 @@ export { hasEReceipt, hasRoute, isReceiptBeingScanned, + didRceiptScanSucceed, getValidWaypoints, isDistanceRequest, isFetchingWaypointsFromServer, @@ -711,6 +723,7 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, + hasNoticeTypeViolation, isCustomUnitRateIDForP2P, getRateID, };