diff --git a/assets/images/document-check.svg b/assets/images/document-check.svg new file mode 100644 index 000000000000..1364765e7e70 --- /dev/null +++ b/assets/images/document-check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/ButtonWithDropdownMenu/index.tsx b/src/components/ButtonWithDropdownMenu/index.tsx index 56e8a81e0033..b05e44daaf39 100644 --- a/src/components/ButtonWithDropdownMenu/index.tsx +++ b/src/components/ButtonWithDropdownMenu/index.tsx @@ -59,8 +59,10 @@ function ButtonWithDropdownMenu({ref, ...props}: ButtonWithDropdownM shouldUseModalPaddingStyle = true, shouldUseShortForm = false, shouldUseOptionIcon = false, + headerTextStyles, shouldStayNormalOnDisable = false, sentryLabel, + shouldPutHeaderTextAfterBackButton = false, } = props; const icons = useMemoizedLazyExpensifyIcons(['DownArrow']); @@ -273,6 +275,7 @@ function ButtonWithDropdownMenu({ref, ...props}: ButtonWithDropdownM setIsMenuVisible(false); } }} + headerStyles={headerTextStyles} anchorPosition={popoverAnchorPosition} shouldShowSelectedItemCheck={shouldShowSelectedItemCheck} anchorRef={nullCheckRef(dropdownAnchor)} @@ -302,6 +305,7 @@ function ButtonWithDropdownMenu({ref, ...props}: ButtonWithDropdownM shouldCallAfterModalHide: true, subMenuItems: item.subMenuItems?.map((subItem) => ({...subItem, shouldCallAfterModalHide: true})), }))} + shouldPutHeaderTextAfterBackButton={shouldPutHeaderTextAfterBackButton} /> )} diff --git a/src/components/ButtonWithDropdownMenu/types.ts b/src/components/ButtonWithDropdownMenu/types.ts index 9120a183d6c7..216b6a5896f2 100644 --- a/src/components/ButtonWithDropdownMenu/types.ts +++ b/src/components/ButtonWithDropdownMenu/types.ts @@ -158,8 +158,14 @@ type ButtonWithDropdownMenuProps = WithSentryLabel & { /** Whether to display the option icon when only one option is available */ shouldUseOptionIcon?: boolean; + /** Used to apply styles specifically to the header text */ + headerTextStyles?: StyleProp; + /** Reference to the outer element */ ref?: React.Ref; + + /** Whether to put the header text after the back button */ + shouldPutHeaderTextAfterBackButton?: boolean; }; type ButtonWithDropdownMenuRef = { diff --git a/src/components/ExpenseHeaderApprovalButton.tsx b/src/components/ExpenseHeaderApprovalButton.tsx new file mode 100644 index 000000000000..8a7536b8e6e0 --- /dev/null +++ b/src/components/ExpenseHeaderApprovalButton.tsx @@ -0,0 +1,151 @@ +import React from 'react'; +import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; +import Button from './Button'; +import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; +import type {LocaleContextProps} from './LocaleContextProvider'; + +type ExpenseHeaderApprovalButtonProps = { + /** Whether any transaction is on hold */ + isAnyTransactionOnHold: boolean; + + /** Whether delegate access is restricted */ + isDelegateAccessRestricted: boolean; + + /** Whether the report has only held expenses */ + hasOnlyHeldExpenses: boolean; + + /** Whether there is a valid non-held amount */ + hasValidNonHeldAmount: boolean; + + /** The non-held amount string */ + nonHeldAmount: string | undefined; + + /** The full amount string */ + fullAmount: string; + + /** Callback when approval is confirmed */ + onApprove: (isFullApproval: boolean) => void; + + /** Whether the button is disabled */ + isDisabled?: boolean; +}; + +type ApprovalOption = { + value: string; + text: string; + icon: IconAsset; + onSelected: () => void; + keyForList: string; +}; + +type ApprovalDropdownOptionProps = { + nonHeldAmount: string | undefined; + fullAmount: string; + hasValidNonHeldAmount: boolean; + hasOnlyHeldExpenses: boolean; + onPartialApprove: () => void; + onFullApprove: () => void; + translate: LocaleContextProps['translate']; + illustrations: Record<'ThumbsUp' | 'DocumentCheck', IconAsset>; +}; + +/** + * Generates dropdown options for approve button when there are held expenses + */ +function getApprovalDropdownOptions({ + nonHeldAmount, + fullAmount, + hasValidNonHeldAmount, + hasOnlyHeldExpenses, + onPartialApprove, + onFullApprove, + translate, + illustrations, +}: ApprovalDropdownOptionProps): ApprovalOption[] { + const APPROVE_PARTIAL = 'approve_partial'; + const APPROVE_FULL = 'approve_full'; + const options: ApprovalOption[] = []; + + if (nonHeldAmount && hasValidNonHeldAmount && !hasOnlyHeldExpenses) { + options.push({ + value: APPROVE_PARTIAL, + text: `${translate('iou.approveOnly')} ${nonHeldAmount}`, + icon: illustrations.ThumbsUp, + onSelected: onPartialApprove, + keyForList: APPROVE_PARTIAL, + }); + } + + options.push({ + value: APPROVE_FULL, + text: `${translate('iou.approve')} ${fullAmount}`, + icon: illustrations.DocumentCheck, + onSelected: onFullApprove, + keyForList: APPROVE_FULL, + }); + + return options; +} + +function ExpenseHeaderApprovalButton({ + isAnyTransactionOnHold, + isDelegateAccessRestricted, + hasOnlyHeldExpenses, + hasValidNonHeldAmount, + nonHeldAmount, + fullAmount, + onApprove, + isDisabled = false, +}: ExpenseHeaderApprovalButtonProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const illustrations = useMemoizedLazyExpensifyIcons(['ThumbsUp', 'DocumentCheck']); + + const shouldShowDropdown = isAnyTransactionOnHold && !isDelegateAccessRestricted; + + if (shouldShowDropdown) { + const approvalOptions = getApprovalDropdownOptions({ + nonHeldAmount: !hasOnlyHeldExpenses && hasValidNonHeldAmount ? nonHeldAmount : undefined, + fullAmount, + hasValidNonHeldAmount, + hasOnlyHeldExpenses, + onPartialApprove: () => onApprove(false), + onFullApprove: () => onApprove(true), + translate, + illustrations, + }); + + if (approvalOptions.length > 1) { + return ( + {}} + customText={translate('iou.approve')} + headerTextStyles={styles.lineHeightNormal} + shouldAlwaysShowDropdownMenu + isSplitButton={false} + isDisabled={isDisabled} + /> + ); + } + } + + return ( +