Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2496,6 +2496,8 @@ ${amount} für ${merchant} – ${date}`,
frozenByAdminNeedsUnfreezePrefix: 'Diese Karte wurde von ',
frozenByAdminNeedsUnfreezeSuffix: ' gesperrt. Bitte kontaktiere einen Admin, um sie zu entsperren.',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Diese Karte wurde von ${person} gesperrt. Bitte kontaktiere einen Admin, um sie zu entsperren.`,
spendRules: 'Ausgaberegeln',
editSpendRules: 'Ausgaberegeln bearbeiten',
},
workflowsPage: {
workflowTitle: 'Ausgaben',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2492,6 +2492,8 @@ const translations = {
getPhysicalCard: 'Get physical card',
reportFraud: 'Report virtual card fraud',
reportTravelFraud: 'Report travel card fraud',
spendRules: 'Spend rules',
editSpendRules: 'Edit spend rules',
reviewTransaction: 'Review transaction',
suspiciousBannerTitle: 'Suspicious transaction',
suspiciousBannerDescription: 'We noticed suspicious transactions on your card. Tap below to review.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2307,6 +2307,8 @@ const translations: TranslationDeepObject<typeof en> = {
getPhysicalCard: 'Obtener tarjeta física',
reportFraud: 'Reportar fraude con la tarjeta virtual',
reportTravelFraud: 'Reportar fraude con la tarjeta de viaje',
spendRules: 'Reglas de gasto',
editSpendRules: 'Editar reglas de gasto',
reviewTransaction: 'Revisar transacción',
suspiciousBannerTitle: 'Transacción sospechosa',
suspiciousBannerDescription: 'Hemos detectado una transacción sospechosa en la tarjeta. Haz click abajo para revisarla.',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2503,6 +2503,8 @@ ${amount} pour ${merchant} - ${date}`,
frozenByAdminNeedsUnfreezePrefix: 'Cette carte a été gelée par ',
frozenByAdminNeedsUnfreezeSuffix: '. Veuillez contacter un administrateur pour la dégeler.',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Cette carte a été gelée par ${person}. Veuillez contacter un administrateur pour la dégeler.`,
spendRules: 'Règles de dépense',
editSpendRules: 'Modifier les règles de dépense',
},
workflowsPage: {
workflowTitle: 'Dépense',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2492,6 +2492,8 @@ ${amount} per ${merchant} - ${date}`,
frozenByAdminNeedsUnfreezePrefix: 'Questa carta è stata bloccata da ',
frozenByAdminNeedsUnfreezeSuffix: '. Contatta un amministratore per sbloccarla.',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Questa carta è stata bloccata da ${person}. Contatta un amministratore per sbloccarla.`,
spendRules: 'Regole di spesa',
editSpendRules: 'Modifica le regole di spesa',
},
workflowsPage: {
workflowTitle: 'Spesa',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2467,6 +2467,8 @@ ${date} の ${merchant} への ${amount}`,
frozenByAdminNeedsUnfreezePrefix: 'このカードは',
frozenByAdminNeedsUnfreezeSuffix: 'によって一時停止されました。解除するには管理者に連絡してください。',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `このカードは${person}によって一時停止されました。解除するには管理者に連絡してください。`,
spendRules: '支出ルール',
editSpendRules: '支出ルールを編集',
},
workflowsPage: {
workflowTitle: '支出',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2490,6 +2490,8 @@ ${amount} voor ${merchant} - ${date}`,
frozenByAdminNeedsUnfreezePrefix: 'Deze kaart is bevroren door ',
frozenByAdminNeedsUnfreezeSuffix: '. Neem contact op met een beheerder om deze te deblokkeren.',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Deze kaart is bevroren door ${person}. Neem contact op met een beheerder om deze te deblokkeren.`,
spendRules: 'Uitgavenregels',
editSpendRules: 'Uitgavenregels bewerken',
},
workflowsPage: {
workflowTitle: 'Uitgaven',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2485,6 +2485,8 @@ ${amount} dla ${merchant} - ${date}`,
frozenByAdminNeedsUnfreezePrefix: 'Ta karta została zamrożona przez ',
frozenByAdminNeedsUnfreezeSuffix: '. Skontaktuj się z administratorem, aby ją odmrozić.',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Ta karta została zamrożona przez ${person}. Skontaktuj się z administratorem, aby ją odmrozić.`,
spendRules: 'Zasady wydatków',
editSpendRules: 'Edytuj reguły wydatków',
},
workflowsPage: {
workflowTitle: 'Wydatki',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2483,6 +2483,8 @@ ${amount} para ${merchant} - ${date}`,
frozenByAdminNeedsUnfreezePrefix: 'Este cartão foi bloqueado por ',
frozenByAdminNeedsUnfreezeSuffix: '. Entre em contato com um administrador para desbloqueá-lo.',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `Este cartão foi bloqueado por ${person}. Entre em contato com um administrador para desbloqueá-lo.`,
spendRules: 'Regras de gasto',
editSpendRules: 'Editar regras de gasto',
},
workflowsPage: {
workflowTitle: 'Gastos',
Expand Down
2 changes: 2 additions & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2414,6 +2414,8 @@ ${amount},商户:${merchant} - 日期:${date}`,
frozenByAdminNeedsUnfreezePrefix: '此卡已被',
frozenByAdminNeedsUnfreezeSuffix: '冻结。请联系管理员解冻。',
frozenByAdminNeedsUnfreeze: ({person}: {person: string}) => `此卡已被${person}冻结。请联系管理员解冻。`,
spendRules: '支出规则',
editSpendRules: '编辑支出规则',
},
workflowsPage: {
workflowTitle: '支出',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useStyleUtils from '@hooks/useStyleUtils';
import useThemeStyles from '@hooks/useThemeStyles';
import {getSpendRuleFormValuesFromCardRule} from '@libs/actions/Card';
import {openPolicyExpensifyCardsPage} from '@libs/actions/Policy/Policy';
import {getAllCardsForWorkspace, getCardHintText, getTranslationKeyForLimitType, isCardFrozen, maskCard} from '@libs/CardUtils';
import {convertToDisplayString} from '@libs/CurrencyUtils';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
Expand All @@ -36,6 +38,7 @@
import Navigation from '@navigation/Navigation';
import NotFoundPage from '@pages/ErrorPage/NotFoundPage';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import {getSpendRuleSummaryParts} from '@pages/workspace/rules/SpendRules/SpendRulesUtils';
import variables from '@styles/variables';
import {deactivateCard as deactivateCardAction, freezeCard as freezeCardAction, openCardDetailsPage, unfreezeCard as unfreezeCardAction} from '@userActions/Card';
import CONST from '@src/CONST';
Expand Down Expand Up @@ -87,17 +90,23 @@
const displayName = getDisplayNameOrDefault(cardholder);
const translationForLimitType = getTranslationKeyForLimitType(card?.nameValuePairs?.limitType);
const isAdmin = isPolicyAdmin(policy, session?.email);

const shouldGoBack = useRef(false);

const fetchCardDetails = useCallback(() => {
openCardDetailsPage(Number(cardID));
}, [cardID]);

const {isOffline} = useNetwork({onReconnect: fetchCardDetails});

useEffect(() => fetchCardDetails(), [fetchCardDetails]);

useEffect(() => {
if (!isAdmin || expensifyCardSettings?.isLoading || expensifyCardSettings?.hasOnceLoaded) {
return;
}

openPolicyExpensifyCardsPage(policyID, defaultFundID);
}, [defaultFundID, expensifyCardSettings.isLoading, expensifyCardSettings.hasOnceLoaded, isAdmin, policyID]);

Check failure on line 108 in src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

'expensifyCardSettings' is possibly 'undefined'.

Check failure on line 108 in src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

'expensifyCardSettings' is possibly 'undefined'.

const deactivateCard = () => {
setIsDeactivateModalVisible(false);
shouldGoBack.current = true;
Expand Down Expand Up @@ -132,6 +141,41 @@
};

const canManageCardFreeze = isBetaEnabled(CONST.BETAS.FREEZE_CARD) && isAdmin && !!card;
const matchingSpendRules = useMemo(
() =>
Object.entries(expensifyCardSettings?.cardRules ?? {}).flatMap(([ruleID, rule]) => {
const formValues = getSpendRuleFormValuesFromCardRule(rule);

Check failure on line 147 in src/pages/workspace/expensifyCard/WorkspaceExpensifyCardDetailsPage.tsx

View workflow job for this annotation

GitHub Actions / typecheck

Argument of type 'string | number | boolean | Date | Errors | ErrorFields | ExpensifyCardSettingsBase | Record<string, ExpensifyCardRule> | Partial<...> | null' is not assignable to parameter of type 'ExpensifyCardRule'.
if (!formValues?.cardIDs.includes(cardID)) {
return [];
}

return [{ruleID, formValues}];
}),
[cardID, expensifyCardSettings?.cardRules],
);
const spendRulesSummary = useMemo(
() =>
matchingSpendRules
.flatMap(({formValues}) => {
const actionLabel =
formValues.restrictionAction === CONST.SPEND_RULES.ACTION.BLOCK ? translate('workspace.rules.spendRules.block') : translate('workspace.rules.spendRules.allow');

return getSpendRuleSummaryParts(formValues, currency, actionLabel, translate).map((part) => `${part.badgeLabel} ${part.text}`);
})
.join('\n'),
[currency, matchingSpendRules, translate],
);
const spendRulesRoute = useMemo(() => {
if (matchingSpendRules.length === 0) {
return ROUTES.RULES_SPEND_NEW.getRoute(policyID);
}

if (matchingSpendRules.length === 1) {
return ROUTES.RULES_SPEND_EDIT.getRoute(policyID, matchingSpendRules.at(0)?.ruleID ?? ROUTES.NEW);
}

return ROUTES.WORKSPACE_RULES.getRoute(policyID);
}, [matchingSpendRules, policyID]);
const scarfOverlayStyle = useMemo(
() => ({
top: 0,
Expand Down Expand Up @@ -283,6 +327,16 @@
}
/>
</OfflineWithFeedback>
{isAdmin && (
<MenuItemWithTopDescription
description={translate('cardPage.spendRules')}
title={spendRulesSummary || translate('cardPage.editSpendRules')}
shouldShowRightIcon
numberOfLinesTitle={0}
titleStyle={styles.flex1}
onPress={() => Navigation.navigate(spendRulesRoute)}
/>
)}
<MenuItem
icon={expensifyIcons.MoneySearch}
title={translate('workspace.common.viewTransactions')}
Expand All @@ -307,6 +361,12 @@
onPress={handleFreezePress}
/>
)}
{isWorkspaceCardRhp && isAdmin && (
<MenuItem
title={translate('cardPage.editSpendRules')}
onPress={() => Navigation.navigate(spendRulesRoute)}
/>
)}
<MenuItem
icon={expensifyIcons.Trashcan}
title={translate('workspace.expensifyCard.deactivate')}
Expand Down
43 changes: 1 addition & 42 deletions src/pages/workspace/rules/SpendRules/SpendRulesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,59 +20,18 @@ import useThemeStyles from '@hooks/useThemeStyles';
import {getSpendRuleFormValuesFromCardRule} from '@libs/actions/Card';
import {openPolicyExpensifyCardsPage} from '@libs/actions/Policy/Policy';
import {filterInactiveCards, getCardDescriptionForSearchTable, getSelectedCardsSharedCurrency, isCard} from '@libs/CardUtils';
import {convertToBackendAmount, convertToDisplayString} from '@libs/CurrencyUtils';
import Navigation from '@libs/Navigation/Navigation';
import {getDisplayNameOrDefault} from '@libs/PersonalDetailsUtils';
import variables from '@styles/variables';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import ROUTES from '@src/ROUTES';
import type {SpendRuleForm} from '@src/types/form';
import {getTruncatedSpendRuleSummary} from './SpendRulesUtils';
import {getSpendRuleSummaryParts, getTruncatedSpendRuleSummary} from './SpendRulesUtils';

type SpendRulesSectionProps = {
policyID: string;
};

type SpendRuleSummaryPart = {
badgeLabel: string;
text: string;
isNeutral?: boolean;
};

function getSpendRuleSummaryParts(
formValues: SpendRuleForm,
selectedCurrency: string | undefined,
actionLabel: string,
translate: ReturnType<typeof useLocalize>['translate'],
): SpendRuleSummaryPart[] {
const summaryParts: SpendRuleSummaryPart[] = [];
const merchantNames = getTruncatedSpendRuleSummary(formValues.merchantNames, (summary, count) => translate('workspace.rules.spendRules.summaryMoreCount', {summary, count}));
const categories = getTruncatedSpendRuleSummary(
formValues.categories.map((category) => translate(`workspace.rules.spendRules.categoryOptions.${category}`)),
(summary, count) => translate('workspace.rules.spendRules.summaryMoreCount', {summary, count}),
);
const maxAmount = formValues.maxAmount.trim();

if (merchantNames) {
summaryParts.push({badgeLabel: actionLabel, text: `${translate('workspace.rules.spendRules.merchants')}: ${merchantNames}`});
}

if (categories) {
summaryParts.push({badgeLabel: actionLabel, text: `${translate('workspace.rules.spendRules.categories')}: ${categories}`});
}

if (maxAmount) {
summaryParts.push({
badgeLabel: translate('workspace.rules.spendRules.max'),
text: `${translate('iou.amount')}: ${convertToDisplayString(convertToBackendAmount(Number.parseFloat(maxAmount)), selectedCurrency ?? CONST.CURRENCY.USD)}`,
isNeutral: true,
});
}

return summaryParts;
}

function SpendRulesSection({policyID}: SpendRulesSectionProps) {
const {translate, localeCompare} = useLocalize();
const styles = useThemeStyles();
Expand Down
43 changes: 40 additions & 3 deletions src/pages/workspace/rules/SpendRules/SpendRulesUtils.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,17 @@
import type {LocalizedTranslate} from '@components/LocaleContextProvider';
import {convertToBackendAmount, convertToDisplayString} from '@libs/CurrencyUtils';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';
import type {Route} from '@src/ROUTES';
import type {SpendRuleForm} from '@src/types/form';

const MAX_SUMMARY_CHARS = 74;

type MoreCountFormatter = (summary: string, count: number) => string;
type SpendRuleSummaryPart = {
badgeLabel: string;
text: string;
isNeutral?: boolean;
};

function getTruncatedSpendRuleSummary(values: string[] | undefined, formatMoreCount: MoreCountFormatter): string {
const normalizedValues = (values ?? []).map((value) => value.trim()).filter((value) => value !== '');
Expand All @@ -28,8 +36,37 @@ function getTruncatedSpendRuleSummary(values: string[] | undefined, formatMoreCo
return text && hiddenCount > 0 ? formatMoreCount(text, hiddenCount) : text;
}

function getParentRoute(policyID: string, ruleID: string): Route {
function getParentRoute(policyID: string, ruleID: string) {
return ruleID === ROUTES.NEW ? ROUTES.RULES_SPEND_NEW.getRoute(policyID) : ROUTES.RULES_SPEND_EDIT.getRoute(policyID, ruleID);
}

export {getTruncatedSpendRuleSummary, getParentRoute};
function getSpendRuleSummaryParts(formValues: SpendRuleForm, selectedCurrency: string | undefined, actionLabel: string, translate: LocalizedTranslate): SpendRuleSummaryPart[] {
const summaryParts: SpendRuleSummaryPart[] = [];
const merchantNames = getTruncatedSpendRuleSummary(formValues.merchantNames, (summary, count) => translate('workspace.rules.spendRules.summaryMoreCount', {summary, count}));
const categories = getTruncatedSpendRuleSummary(
formValues.categories.map((category) => translate(`workspace.rules.spendRules.categoryOptions.${category}`)),
(summary, count) => translate('workspace.rules.spendRules.summaryMoreCount', {summary, count}),
);
const maxAmount = formValues.maxAmount.trim();

if (merchantNames) {
summaryParts.push({badgeLabel: actionLabel, text: `${translate('workspace.rules.spendRules.merchants')}: ${merchantNames}`});
}

if (categories) {
summaryParts.push({badgeLabel: actionLabel, text: `${translate('workspace.rules.spendRules.categories')}: ${categories}`});
}

if (maxAmount) {
summaryParts.push({
badgeLabel: translate('workspace.rules.spendRules.max'),
text: `${translate('iou.amount')}: ${convertToDisplayString(convertToBackendAmount(Number.parseFloat(maxAmount)), selectedCurrency ?? CONST.CURRENCY.USD)}`,
isNeutral: true,
});
}

return summaryParts;
}

export {getParentRoute, getSpendRuleSummaryParts, getTruncatedSpendRuleSummary};
export type {SpendRuleSummaryPart};
Loading