diff --git a/src/CONST/index.ts b/src/CONST/index.ts index a295f031849a..435fb10c93c6 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -631,6 +631,7 @@ const CONST = { FLAG_FOR_REVIEW: 'flagForReview', RESTRICT_CARD_SPEND: 'restrictCardSpend', EXPENSE_DEFAULTS: 'expenseDefaults', + CATEGORY_TAX_RATE: 'categoryTaxRate', }, }, diff --git a/src/languages/de.ts b/src/languages/de.ts index 95710fccbaf2..65e68352a6b1 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -8843,6 +8843,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc unsupported: 'Diese Regel wird noch nicht unterstützt – wir arbeiten daran. Bitte versuche vorerst eine andere Regel.', unintelligible: 'Formuliere deine Regel etwa so: Wenn Ausgaben über 100 $ liegen, ist ein Beleg erforderlich.', multipleRules: 'Bitte eine Regel nach der anderen – beschreibe eine einzelne Regel, z. B.: Einzelhandelskäufe auf allen Karten blockieren.', + categoryHasRule: 'Für diese Kategorie gibt es bereits eine Regel. Bearbeite die bestehende Regel oder beschreibe eine Regel für eine andere Kategorie.', }, }, expenseDefaultsTable: { diff --git a/src/languages/el.ts b/src/languages/el.ts index 322bae595055..9cab815c9c25 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -8937,6 +8937,7 @@ ${reportName}`, unsupported: 'Αυτός ο κανόνας δεν υποστηρίζεται ακόμη — το δουλεύουμε. Δοκιμάστε προς το παρόν έναν διαφορετικό κανόνα.', unintelligible: 'Δοκιμάστε να διατυπώσετε τον κανόνα σας έτσι: αν οι δαπάνες ξεπερνούν τα 100 $, να απαιτείται απόδειξη.', multipleRules: 'Έναν κανόνα τη φορά — δοκιμάστε να περιγράψετε έναν μόνο κανόνα, π.χ.: αποκλεισμός αγορών λιανικής σε όλες τις κάρτες.', + categoryHasRule: 'Αυτή η κατηγορία έχει ήδη κανόνα. Επεξεργαστείτε τον υπάρχοντα κανόνα ή περιγράψτε έναν κανόνα για άλλη κατηγορία.', }, }, expenseDefaultsTable: { diff --git a/src/languages/en.ts b/src/languages/en.ts index e11f453b5db7..3040782a0900 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -9171,6 +9171,7 @@ const translations = { unsupported: "That rule isn't supported yet — we're working on it. Please try a different rule for now.", unintelligible: 'Try phrasing your rule like: if expenses have a value over $100, then require a receipt.', multipleRules: 'One rule at a time, please — try describing a single rule, like: block retail purchases on all cards.', + categoryHasRule: 'That category already has a rule. Edit the existing rule or describe a rule for another category.', }, }, expenseDefaultsTable: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 954331664658..85d11a24129b 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8848,6 +8848,7 @@ ${reportName}`, unsupported: 'Esa regla aún no es compatible: estamos trabajando en ello. Prueba con otra regla por ahora.', unintelligible: 'Prueba a formular tu regla así: si los gastos superan los $100, exige un recibo.', multipleRules: 'Una regla a la vez, por favor. Prueba a describir una sola regla, como: bloquear compras en comercios minoristas en todas las tarjetas.', + categoryHasRule: 'Esa categoría ya tiene una regla. Edita la regla existente o describe una regla para otra categoría.', }, }, expenseDefaultsTable: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index a35a8506e5f9..e9e7e14cffb3 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -8870,6 +8870,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e unsupported: 'Cette règle n’est pas encore prise en charge — nous y travaillons. Essayez une autre règle pour le moment.', unintelligible: 'Essayez de formuler votre règle ainsi : si les dépenses dépassent 100 $, exigez un reçu.', multipleRules: 'Une règle à la fois, s’il vous plaît — essayez de décrire une seule règle, comme : bloquer les achats en magasin sur toutes les cartes.', + categoryHasRule: 'Cette catégorie a déjà une règle. Modifiez la règle existante ou décrivez une règle pour une autre catégorie.', }, }, expenseDefaultsTable: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 88ce439f43b8..3b65fb865cf2 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -8794,6 +8794,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, unsupported: 'Questa regola non è ancora supportata: ci stiamo lavorando. Per ora prova una regola diversa.', unintelligible: 'Prova a formulare la regola così: se le spese superano i 100 $, richiedi una ricevuta.', multipleRules: 'Una regola alla volta, per favore: prova a descrivere una singola regola, ad esempio bloccare gli acquisti al dettaglio su tutte le carte.', + categoryHasRule: "Questa categoria ha già una regola. Modifica la regola esistente o descrivi una regola per un'altra categoria.", }, }, expenseDefaultsTable: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index afe40800db2b..df312586f606 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -8683,6 +8683,7 @@ ${reportName}`, unsupported: 'そのルールはまだ対応していません。現在対応を進めています。ほかのルールをお試しください。', unintelligible: '「経費が100ドルを超える場合は領収書を必須にする」のように書いてみてください。', multipleRules: '一度に1つのルールのみです。「すべてのカードで小売店での購入をブロックする」のように、1つのルールを説明してください。', + categoryHasRule: 'このカテゴリにはすでにルールがあります。既存のルールを編集するか、別のカテゴリのルールを説明してください。', }, }, expenseDefaultsTable: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 5cfa1a7d0489..458451e9d5f8 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -8768,6 +8768,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, unsupported: 'Die regel wordt nog niet ondersteund — we werken eraan. Probeer voorlopig een andere regel.', unintelligible: 'Formuleer je regel bijvoorbeeld zo: als uitgaven hoger zijn dan $100, vraag dan een bon.', multipleRules: 'Eén regel per keer graag — beschrijf één enkele regel, bijvoorbeeld: retailaankopen op alle kaarten blokkeren.', + categoryHasRule: 'Deze categorie heeft al een regel. Bewerk de bestaande regel of beschrijf een regel voor een andere categorie.', }, }, expenseDefaultsTable: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 791df2822ef4..206f065a73ee 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -8793,6 +8793,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, unsupported: 'Ta reguła nie jest jeszcze obsługiwana — pracujemy nad tym. Na razie spróbuj innej reguły.', unintelligible: 'Spróbuj sformułować regułę tak: jeśli wydatek przekracza 100 $, wymagaj paragonu.', multipleRules: 'Jedna reguła naraz — opisz pojedynczą regułę, na przykład: zablokuj zakupy detaliczne na wszystkich kartach.', + categoryHasRule: 'Ta kategoria ma już regułę. Edytuj istniejącą regułę lub opisz regułę dla innej kategorii.', }, }, expenseDefaultsTable: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 14b8e80954f6..07ab575b9755 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -8775,6 +8775,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, unsupported: 'Essa regra ainda não tem suporte — estamos trabalhando nisso. Tente uma regra diferente por enquanto.', unintelligible: 'Tente formular sua regra assim: se a despesa for maior que $100, exija um recibo.', multipleRules: 'Uma regra por vez, por favor — tente descrever uma única regra, como: bloquear compras no varejo em todos os cartões.', + categoryHasRule: 'Essa categoria já tem uma regra. Edite a regra existente ou descreva uma regra para outra categoria.', }, }, expenseDefaultsTable: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 989b0710d02e..a4b1c59810b8 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -8465,6 +8465,7 @@ ${reportName}`, unsupported: '该规则暂不支持,我们正在开发中。请先尝试其他规则。', unintelligible: '可以这样描述规则:如果支出超过 100 美元,则需要收据。', multipleRules: '请一次描述一条规则,例如:在所有卡上阻止零售消费。', + categoryHasRule: '该类别已有规则。请编辑现有规则,或为其他类别描述规则。', }, }, expenseDefaultsTable: { diff --git a/src/libs/SpendRulesUtils.ts b/src/libs/SpendRulesUtils.ts index 994062fe36da..37bc6d62faab 100644 --- a/src/libs/SpendRulesUtils.ts +++ b/src/libs/SpendRulesUtils.ts @@ -4,10 +4,10 @@ import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import type {SpendRuleForm} from '@src/types/form'; import {isSpendRuleCategory} from '@src/types/form/SpendRuleForm'; -import type {ExpensifyCardSettings} from '@src/types/onyx'; +import type {Card, ExpensifyCardSettings, WorkspaceCardsList} from '@src/types/onyx'; import type {ExpensifyCardRule, ExpensifyCardRuleFilter} from '@src/types/onyx/ExpensifyCardSettings'; -import type {OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import {convertToBackendAmount} from './CurrencyUtils'; @@ -194,6 +194,35 @@ type SpendRuleByCardID = { formValues: SpendRuleForm; }; +function getCardIDsWithSpendRules(cardRules: Record | undefined, currentRuleID?: string): Set { + const cardIDs = new Set(); + if (!cardRules) { + return cardIDs; + } + + for (const [ruleID, rule] of Object.entries(cardRules)) { + if (ruleID === currentRuleID) { + continue; + } + const formValues = getSpendRuleFormValuesFromCardRule(rule); + for (const cardID of formValues?.cardIDs ?? []) { + const numericCardID = Number(cardID); + if (Number.isFinite(numericCardID)) { + cardIDs.add(numericCardID); + } + } + } + + return cardIDs; +} + +/** Returns the cards a spend rule can cover, which are the cards no other spend rule covers */ +function getEligibleSpendRuleCards(cardsList: OnyxEntry, expensifyCardSettings: OnyxEntry, currentRuleID?: string): Card[] { + const {cardList, ...cards} = cardsList ?? {}; + const cardIDsWithSpendRules = getCardIDsWithSpendRules(expensifyCardSettings?.cardRules, currentRuleID); + return Object.values(cards).filter((card: Card) => !cardIDsWithSpendRules.has(card.cardID)); +} + /** At most one custom spend rule should reference a card; returns rule id + form values, or undefined. */ function getSpendRuleByCardID(expensifyCardSettingsCollection: OnyxCollection | undefined, cardID: string): SpendRuleByCardID | undefined { for (const settings of Object.values(expensifyCardSettingsCollection ?? {})) { @@ -356,4 +385,12 @@ function getSpendRuleSummaryText( } export type {SpendRuleSummaryPart}; -export {buildSpendRuleAST, getSpendRuleByCardID, getSpendRuleFormValuesFromCardRule, getSpendRuleSummaryParts, getSpendRuleSummaryText, getTruncatedSpendRuleSummary}; +export { + buildSpendRuleAST, + getEligibleSpendRuleCards, + getSpendRuleByCardID, + getSpendRuleFormValuesFromCardRule, + getSpendRuleSummaryParts, + getSpendRuleSummaryText, + getTruncatedSpendRuleSummary, +}; diff --git a/src/libs/actions/Policy/Rules.ts b/src/libs/actions/Policy/Rules.ts index f8d94fc18ae5..97037adde083 100644 --- a/src/libs/actions/Policy/Rules.ts +++ b/src/libs/actions/Policy/Rules.ts @@ -447,9 +447,14 @@ function generateRule(policyID: string, ruleType: GeneratedRuleType, prompt: str /** Shows on the prompt form why a description did not become a rule */ function setNewRulePromptError(state: GeneratedRuleState) { + setNewRulePromptErrorMessage(PROMPT_ERROR_BY_STATE[state] ?? 'common.genericErrorMessage'); +} + +/** Shows an error on the prompt form */ +function setNewRulePromptErrorMessage(translationKey: TranslationPaths) { Onyx.merge(ONYXKEYS.FORMS.NEW_RULE_PROMPT_FORM, { isLoading: false, - errors: getMicroSecondOnyxErrorWithTranslationKey(PROMPT_ERROR_BY_STATE[state] ?? 'common.genericErrorMessage'), + errors: getMicroSecondOnyxErrorWithTranslationKey(translationKey), }); } @@ -728,6 +733,7 @@ export { addPolicyAgentRule, generateRule, setNewRulePromptError, + setNewRulePromptErrorMessage, clearGeneratedRule, clearNewRulePrompt, updatePolicyAgentRule, diff --git a/src/pages/workspace/rules/DescribeRuleButton.tsx b/src/pages/workspace/rules/DescribeRuleButton.tsx index 3cc111931343..4aee47c5d369 100644 --- a/src/pages/workspace/rules/DescribeRuleButton.tsx +++ b/src/pages/workspace/rules/DescribeRuleButton.tsx @@ -7,7 +7,10 @@ import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; +import {clearNewRulePrompt} from '@userActions/Policy/Rules'; + import CONST from '@src/CONST'; +import type {PolicyCategory} from '@src/types/onyx'; import type {GeneratedRuleType, GeneratedRuleValues} from '@src/types/onyx/GeneratedRule'; import React, {useState} from 'react'; @@ -27,6 +30,9 @@ type DescribeRuleButtonProps = { /** Called with the generated form values once Concierge has built the rule */ onRuleGenerated: (values: GeneratedRuleValues) => void; + /** Whether a new rule can use the category, so a generated category that already has this kind of rule shows an error */ + isCategoryEligible?: (category: PolicyCategory) => boolean; + onPress?: never; } | { @@ -46,12 +52,21 @@ function DescribeRuleButton(props: DescribeRuleButtonProps) { return null; } + const openModal = () => { + clearNewRulePrompt(); + if (props.onPress) { + props.onPress(); + return; + } + setIsModalVisible(true); + }; + return ( <>