Skip to content
Open
4 changes: 2 additions & 2 deletions src/components/Search/SearchBulkActionsButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,8 @@ function SearchBulkActionsButton({queryJSON}: SearchBulkActionsButtonProps) {

return Object.keys(transactionsToCount).reduce((count, key) => {
if (key.startsWith(CONST.SEARCH.GROUP_PREFIX)) {
const group = searchData?.[key as keyof typeof searchData] as {count?: number} | undefined;
return count + (group?.count ?? 0);
const group = searchData?.[key as keyof typeof searchData] as {count?: number; isCashBack?: boolean} | undefined;
return count + (group?.isCashBack ? 1 : (group?.count ?? 0));
}
return count + 1;
}, 0);
Expand Down
64 changes: 38 additions & 26 deletions src/components/Search/SearchList/ListItem/GroupHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ import useThemeStyles from '@hooks/useThemeStyles';
import type {TransactionPreviewData} from '@libs/actions/Search';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab';
import {getColumnsToShow} from '@libs/SearchUIUtils';
import {getColumnsToShow, isCashBackWithdrawalGroup} from '@libs/SearchUIUtils';
import {isDeletedTransaction} from '@libs/TransactionUtils';

import variables from '@styles/variables';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {ReportAction, ReportActions, Transaction} from '@src/types/onyx';
Expand Down Expand Up @@ -176,6 +178,7 @@ function GroupHeader({
const isEmpty = groupItem.transactions.length === 0 && !hasSnapshotTransactions && !groupItem.transactionsQueryJSON;
const isDisabled = item.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
const isDisabledOrEmpty = isEmpty || isDisabled;
const isCashBackWithdrawal = isCashBackWithdrawalGroup(groupItem);

const effectiveTransactions = useMemo((): TransactionListItemType[] => {
if (isExpenseReportType || groupItem.transactions.length > 0) {
Expand Down Expand Up @@ -301,6 +304,7 @@ function GroupHeader({
<WithdrawalIDListItemHeader
withdrawalID={groupItem}
{...commonProps}
onDownArrowClick={isCashBackWithdrawal ? undefined : commonProps.onDownArrowClick}
/>
);
case CONST.SEARCH.GROUP_BY.CATEGORY:
Expand Down Expand Up @@ -378,7 +382,7 @@ function GroupHeader({
if (isExpenseReportType) {
onSelectRow(withOriginalKey(item), transactionPreviewData, event);
}
if (!isExpenseReportType) {
if (!isExpenseReportType && !isCashBackWithdrawal) {
onToggle();
}
};
Expand All @@ -398,7 +402,9 @@ function GroupHeader({
accessibilityLabel={item.text ?? ''}
role={getButtonRole(true)}
isNested
hoverStyle={[!isExpanded && !item.isDisabled && styles.hoveredComponentBG, isItemSelected && styles.activeComponentBG]}
interactive={!isCashBackWithdrawal}
pressDimmingValue={isCashBackWithdrawal ? 1 : undefined}
hoverStyle={[!isExpanded && !item.isDisabled && !isCashBackWithdrawal && styles.hoveredComponentBG, isItemSelected && styles.activeComponentBG]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true, [CONST.INNER_BOX_SHADOW_ELEMENT]: false}}
onMouseDown={(e) => e.preventDefault()}
id={item.keyForList ?? ''}
Expand All @@ -424,29 +430,35 @@ function GroupHeader({
<View style={styles.flex1}>
<View style={[styles.flexRow, styles.alignItemsCenter, isLargeScreenWidth && styles.tableRowHeight]}>
<View style={styles.flex1}>{renderHeader(hovered)}</View>
{isLargeScreenWidth && (
<PressableWithFeedback
onPress={() => {
if (isEmpty && !shouldDisplayEmptyView) {
handlePress();
return;
}
onToggle();
}}
style={[styles.p3Half, styles.justifyContentCenter, styles.alignItemsCenter, styles.pv2]}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={isExpanded ? CONST.ACCESSIBILITY_LABELS.COLLAPSE : CONST.ACCESSIBILITY_LABELS.EXPAND}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.GROUP_EXPAND_TOGGLE}
>
{({hovered: arrowHovered}) => (
<Icon
src={isExpanded ? expensifyIcons.UpArrow : expensifyIcons.DownArrow}
fill={theme.icon}
additionalStyles={!arrowHovered && styles.opacitySemiTransparent}
/>
)}
</PressableWithFeedback>
)}
{isLargeScreenWidth &&
(isCashBackWithdrawal ? (
// Reserves the toggle's footprint so the Total column stays aligned with the settlement rows.
<View style={[styles.p3Half, styles.justifyContentCenter, styles.alignItemsCenter, styles.pv2]}>
<View style={StyleUtils.getWidthAndHeightStyle(variables.iconSizeNormal)} />
</View>
) : (
<PressableWithFeedback
onPress={() => {
if (isEmpty && !shouldDisplayEmptyView) {
handlePress();
return;
}
onToggle();
}}
style={[styles.p3Half, styles.justifyContentCenter, styles.alignItemsCenter, styles.pv2]}
accessibilityRole={CONST.ROLE.BUTTON}
accessibilityLabel={isExpanded ? CONST.ACCESSIBILITY_LABELS.COLLAPSE : CONST.ACCESSIBILITY_LABELS.EXPAND}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.GROUP_EXPAND_TOGGLE}
>
{({hovered: arrowHovered}) => (
<Icon
src={isExpanded ? expensifyIcons.UpArrow : expensifyIcons.DownArrow}
fill={theme.icon}
additionalStyles={!arrowHovered && styles.opacitySemiTransparent}
/>
)}
</PressableWithFeedback>
))}
</View>
{isLargeScreenWidth && subHeaderColumns.length > 0 && (
<Animated.View style={subHeaderAnimatedStyle}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type {TransactionPreviewData} from '@libs/actions/Search';
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import type {ModifiedMouseEvent} from '@libs/Navigation/helpers/openInternalRouteInNewTab';
import {getLoginByAccountID} from '@libs/PersonalDetailsUtils';
import {getSections} from '@libs/SearchUIUtils';
import {getSections, isCashBackWithdrawalGroup} from '@libs/SearchUIUtils';
import {getVisibleTransactionViolations} from '@libs/TransactionUtils';

import variables from '@styles/variables';
Expand Down Expand Up @@ -311,7 +311,13 @@ function TransactionGroupListItemImpl({
});
};

const isCashBackWithdrawal = isCashBackWithdrawalGroup(groupItem);

const onPress = (event?: ModifiedMouseEvent) => {
// A cash back row has no children to drill into.
if (isCashBackWithdrawal) {
return;
}
const isEmptyGroupWithoutTransactionsQuery = transactions.length === 0 && !groupItem.transactionsQueryJSON;
if (isExpenseReportType || isEmptyGroupWithoutTransactionsQuery) {
onSelectRow(item, transactionPreviewData, event);
Expand All @@ -334,6 +340,9 @@ function TransactionGroupListItemImpl({
};

const onExpandIconPress = () => {
if (isCashBackWithdrawal) {
return;
}
if (isEmpty && !shouldDisplayEmptyView) {
onPress();
// onPress handles handleToggle() for us, so we return early to avoid calling it twice
Expand Down Expand Up @@ -381,7 +390,7 @@ function TransactionGroupListItemImpl({
canSelectMultiple={canSelectMultiple}
isSelectAllChecked={isSelectAllChecked}
isIndeterminate={isIndeterminate}
onDownArrowClick={onExpandIconPress}
onDownArrowClick={isCashBackWithdrawal ? undefined : onExpandIconPress}
isExpanded={isExpanded}
/>
),
Expand Down Expand Up @@ -570,7 +579,9 @@ function TransactionGroupListItemImpl({
accessibilityLabel={item.text ?? ''}
role={getButtonRole(true)}
isNested
hoverStyle={[!isExpanded && !item.isDisabled && styles.hoveredComponentBG, isItemSelected && styles.activeComponentBG]}
interactive={!isCashBackWithdrawal}
pressDimmingValue={isCashBackWithdrawal ? 1 : undefined}
hoverStyle={[!isExpanded && !item.isDisabled && !isCashBackWithdrawal && styles.hoveredComponentBG, isItemSelected && styles.activeComponentBG]}
dataSet={{[CONST.SELECTION_SCRAPER_HIDDEN_ELEMENT]: true, [CONST.INNER_BOX_SHADOW_ELEMENT]: false}}
onMouseDown={(e) => e.preventDefault()}
id={item.keyForList ?? ''}
Expand Down Expand Up @@ -599,7 +610,7 @@ function TransactionGroupListItemImpl({
header={getHeader(hovered)}
onPress={onExpandIconPress}
expandButtonStyle={isLargeScreenWidth ? styles.pv2 : styles.pv4Half}
shouldShowToggleButton={isLargeScreenWidth}
shouldShowToggleButton={isLargeScreenWidth && !isCashBackWithdrawal}
borderBottomStyle={isLargeScreenWidth ? styles.borderNone : isItemSelected && {borderColor: theme.buttonHoveredBG}}
sentryLabel={CONST.SENTRY_LABEL.SEARCH.GROUP_EXPAND_TOGGLE}
>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,9 @@ function WithdrawalIDListItemHeaderImpl({
);
const {debitedAmount, debitedCurrency, creditedAmount, creditedCurrency} = withdrawalIDItem;

const badgeProps = getSettlementStatusBadgeProps(withdrawalIDItem.state, translate, theme);
const settlementStatus = getSettlementStatus(withdrawalIDItem.state);
const isCashBack = !!withdrawalIDItem.isCashBack;
const badgeProps = getSettlementStatusBadgeProps(withdrawalIDItem.state, translate, theme, isCashBack);
const settlementStatus = isCashBack ? undefined : getSettlementStatus(withdrawalIDItem.state);
const statusBadge = !!badgeProps && (
<StatusBadge
text={badgeProps.text}
Expand Down Expand Up @@ -177,7 +178,7 @@ function WithdrawalIDListItemHeaderImpl({
key={CONST.SEARCH.TABLE_COLUMNS.EXPENSES}
style={StyleUtils.getReportTableColumnStyles(CONST.SEARCH.TABLE_COLUMNS.EXPENSES)}
>
<TextCell text={String(withdrawalIDItem.count)} />
<TextCell text={isCashBack ? '' : String(withdrawalIDItem.count)} />
</View>
),
// A settlement that did not convert currencies reports neither amount, and an amount says nothing without the currency it moved in.
Expand Down Expand Up @@ -271,6 +272,13 @@ function WithdrawalIDListItemHeaderImpl({
onPress={onDownArrowClick}
/>
)}
{!onDownArrowClick &&
isCashBack && (
// Reserves the arrow's footprint so the total stays aligned with the settlement rows.
<View style={[styles.pl3, styles.justifyContentCenter, styles.alignItemsEnd]}>
<View style={StyleUtils.getWidthAndHeightStyle(variables.iconSizeNormal)} />
</View>
)}
</View>
)}
</View>
Expand Down
3 changes: 3 additions & 0 deletions src/components/Search/SearchList/ListItem/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ type TransactionWithdrawalIDGroupListItemType = TransactionGroupListItemType & {
/** Final and formatted "withdrawalID" value used for displaying and sorting */
formattedWithdrawalID?: string;

/** Value used for sorting the "withdrawal status" column */
settlementStatusRank?: number;

/** Whether any withdrawn date in the current results belongs to a past year */
shouldShowYearWithdrawn?: boolean;
};
Expand Down
6 changes: 4 additions & 2 deletions src/components/Search/SearchSelectionFooter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,8 @@ function SearchSelectionFooter({searchResults}: SearchSelectionFooterProps) {
// Source figures for every loaded group, not just the selected ones. The grouped response caches every group's
// converted value, so stamping them all lets a later selection of another group reuse the cache instead of
// re-running the grouped query. Uses the same expense-signed figure as getEntrySource so a stamp always matches
// the live source the freshness checks compare against.
// the live source the freshness checks compare against, which is why cash back is signed the other way here:
// selectionBuilders gives a selected cash back row a positive groupAmount, and a mismatch would never go fresh.
const loadedGroupSourceByKey = useMemo(() => {
const data = currentSearchResults?.data;
if (!isGroupedSearch || !data) {
Expand All @@ -215,7 +216,8 @@ function SearchSelectionFooter({searchResults}: SearchSelectionFooterProps) {
}
const group: unknown = data[key];
if (group && typeof group === 'object' && 'total' in group && typeof group.total === 'number') {
sources[key] = -Math.abs(group.total);
const isCashBack = 'isCashBack' in group && group.isCashBack === true;
sources[key] = isCashBack ? Math.abs(group.total) : -Math.abs(group.total);
}
}
return sources;
Expand Down
5 changes: 4 additions & 1 deletion src/components/Search/selectionBuilders.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {isSplitAction} from '@libs/ReportSecondaryActionUtils';
import {canEditFieldOfMoneyRequest, canHoldUnholdReportAction, canRejectReportAction, getReimbursableTotal, isMoneyRequestReport, isOneTransactionReport} from '@libs/ReportUtils';
import {isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
import {isCashBackWithdrawalGroup, isTransactionListItemType, isTransactionReportGroupListItemType} from '@libs/SearchUIUtils';
import {getOriginalTransactionWithSplitInfo, hasValidModifiedAmount, isExpenseUnreported, isOnHold} from '@libs/TransactionUtils';

import CONST from '@src/CONST';
Expand Down Expand Up @@ -133,6 +133,7 @@ function mapEmptyReportToSelectedEntry(item: TransactionReportGroupListItemType
}

const currency = item.currency ?? '';
const isCashBack = isCashBackWithdrawalGroup(item);

return [
item.keyForList ?? '',
Expand All @@ -151,6 +152,8 @@ function mapEmptyReportToSelectedEntry(item: TransactionReportGroupListItemType
policyID: item.policyID ?? CONST.POLICY.ID_FAKE,
amount: item.total ?? 0,
currency,
// Without groupAmount the footer falls back to -Math.abs(amount), flipping the credit's sign.
...(isCashBack ? {groupAmount: Math.abs(item.total ?? 0)} : {}),
...(currency ? {groupCurrency: currency} : {}),
},
];
Expand Down
7 changes: 1 addition & 6 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9218,12 +9218,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc
},
},
settlement: {
status: {
pending: 'Ausstehend',
cleared: 'Ausgeglichen',
failed: 'Fehlgeschlagen',
never: 'Nie',
},
status: {pending: 'Ausstehend', cleared: 'Ausgeglichen', failed: 'Fehlgeschlagen', never: 'Nie', cashBack: 'Cashback'},
failedError: ({link}: {link: string}) => `Wir versuchen diese Abrechnung erneut, sobald du <a href="${link}">dein Konto entsperrst</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • Auszahlungs-ID: ${withdrawalID}`,
},
Expand Down
7 changes: 1 addition & 6 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9437,12 +9437,7 @@ ${reportName}`,
},
},
settlement: {
status: {
pending: 'Σε εκκρεμότητα',
cleared: 'Εκκαθαρισμένο',
failed: 'Απέτυχε',
never: 'Ποτέ',
},
status: {pending: 'Σε εκκρεμότητα', cleared: 'Εκκαθαρισμένο', failed: 'Απέτυχε', never: 'Ποτέ', cashBack: 'Επιστροφή μετρητών'},
failedError: ({link}: {link: string}) => `Θα προσπαθήσουμε ξανά για αυτόν τον διακανονισμό όταν <a href="${link}">ξεκλειδώσετε τον λογαριασμό σας</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • Αναγνωριστικό ανάληψης: ${withdrawalID}`,
},
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9349,6 +9349,7 @@ const translations = {
cleared: 'Cleared',
failed: 'Failed',
never: 'Never',
cashBack: 'Cash back',
},
failedError: ({link}: {link: string}) => `We'll retry this settlement when you <a href="${link}">unlock your account</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • Withdrawal ID: ${withdrawalID}`,
Expand Down
7 changes: 1 addition & 6 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9045,12 +9045,7 @@ El plan Controlar empieza en 9 $ por miembro activo al mes.`,
},
},
settlement: {
status: {
pending: 'Pendiente',
cleared: 'Liquidado',
failed: 'Fallido',
never: 'Nunca',
},
status: {pending: 'Pendiente', cleared: 'Liquidado', failed: 'Fallido', never: 'Nunca', cashBack: 'Devolución de dinero'},
failedError: ({link}: {link: string}) => `Reintentaremos esta liquidación cuando <a href="${link}">desbloquees tu cuenta</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • ID de retiro: ${withdrawalID}`,
},
Expand Down
7 changes: 1 addition & 6 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9253,12 +9253,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e
},
},
settlement: {
status: {
pending: 'En attente',
cleared: 'Compensé',
failed: 'Échec',
never: 'Jamais',
},
status: {pending: 'En attente', cleared: 'Compensé', failed: 'Échec', never: 'Jamais', cashBack: 'Remboursement en espèces'},
failedError: ({link}: {link: string}) => `Nous réessaierons ce règlement lorsque vous <a href="${link}">déverrouillerez votre compte</a>.`,
withdrawalInfo: ({date, withdrawalID}: {date: string; withdrawalID: number}) => `${date} • ID de retrait : ${withdrawalID}`,
},
Expand Down
Loading
Loading