diff --git a/assets/images/product-illustrations/illustration_agents-ice-cream.svg b/assets/images/product-illustrations/illustration_agents-ice-cream.svg new file mode 100644 index 000000000000..752665de979c --- /dev/null +++ b/assets/images/product-illustrations/illustration_agents-ice-cream.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST/index.ts b/src/CONST/index.ts index e08d44c8279d..7eac21252ef3 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -5898,6 +5898,7 @@ const CONST = { EXPENSE_DEFAULTS: 'expenseDefaults', REQUIRE_FIELDS: 'requireFields', FLAG_FOR_REVIEW: 'flagForReview', + AGENTS: 'agents', }, SPLIT: { AMOUNT: 'amount', @@ -8485,6 +8486,7 @@ const CONST = { NEW_RULE_MENU_ITEM_FLAG_FOR_REVIEW: 'WorkspaceRules-NewRuleMenuItem-FlagForReview', NEW_RULE_MENU_ITEM_REQUIRE_FIELDS: 'WorkspaceRules-NewRuleMenuItem-RequireFields', NEW_RULE_MENU_ITEM_APPLY_EXPENSE_DEFAULTS: 'WorkspaceRules-NewRuleMenuItem-ApplyExpenseDefaults', + NEW_RULE_MENU_ITEM_CREATE_AGENT_RULE: 'WorkspaceRules-NewRuleMenuItem-CreateAgentRule', REQUIRE_RECEIPTS_SAVE: 'WorkspaceRules-RequireReceiptsSave', REQUIRE_FIELDS_SAVE: 'WorkspaceRules-RequireFieldsSave', FLAG_RECEIPT_LINE_ITEMS_SAVE: 'WorkspaceRules-FlagReceiptLineItemsSave', diff --git a/src/components/Icon/chunks/illustrations.chunk.ts b/src/components/Icon/chunks/illustrations.chunk.ts index e4369b370ac5..4031e5a03cd6 100644 --- a/src/components/Icon/chunks/illustrations.chunk.ts +++ b/src/components/Icon/chunks/illustrations.chunk.ts @@ -72,6 +72,7 @@ import Hands from '@assets/images/product-illustrations/home-illustration-hands. import SortingMachine from '@assets/images/product-illustrations/illustration-sorting-machine.svg'; import CardReplacementSuccess from '@assets/images/product-illustrations/illustration__card-replacement-success.svg'; import Copilots from '@assets/images/product-illustrations/illustration__copilots.svg'; +import AgentsIceCream from '@assets/images/product-illustrations/illustration_agents-ice-cream.svg'; import MagicCode from '@assets/images/product-illustrations/magic-code.svg'; import ModalHoldOrReject from '@assets/images/product-illustrations/modal-hold-or-reject.svg'; import MushroomTopHat from '@assets/images/product-illustrations/mushroom-top-hat.svg'; @@ -299,6 +300,7 @@ const Illustrations = { QuestionMark, SmartScan, SortingMachine, + AgentsIceCream, TeleScope, Telescope: TeleScope, // Alias for consistency ThreeLeggedLaptopWoman, diff --git a/src/languages/de.ts b/src/languages/de.ts index a62f082fe36b..7c7400b661b7 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -7594,6 +7594,11 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc agentCreatedTitle: 'RuleBot wurde zu Ihrem Arbeitsbereich hinzugefügt!', agentCreatedDescription: (agentsRoute: string) => `Um Ihre Agent-Regeln durchzusetzen, haben wir einen Agenten für Sie erstellt und ihn als Administrator zu Ihrem Arbeitsbereich hinzugefügt.

Bearbeiten Sie die Details Ihres Agenten unter Konto > Agenten.
`, + revampSubtitle: 'Beschreiben Sie flexible Regeln, die bei Bedarf ausgeführt werden.', + newRuleTitle: 'Neue Regel', + describeRuleForConcierge: 'Beschreiben Sie Ihre Regel und Concierge erstellt sie', + nextButton: 'Weiter', + gotIt: 'Verstanden', }, tabs: { general: 'Allgemein', @@ -7601,6 +7606,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc expenseDefaults: 'Standardausgaben', requireFields: 'Felder erforderlich', flagForReview: 'Zur Überprüfung markieren', + agents: 'Agents', }, bulkActions: { deleteMultiple: () => ({ @@ -7659,9 +7665,11 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc applyExpenseDefaults: 'Standardspesen übernehmen', applyExpenseDefaultsDescription: 'Felder aktualisieren, ohne dass die einreichende Person etwas tun muss', flagForReview: 'Zur Überprüfung markieren', - flagForReviewDescription: 'Genehmigende benachrichtigen, wenn Ausgaben Kategorienlimits überschreiten', + flagForReviewDescription: 'Benachrichtigen, wenn Ihre Bedingungen erfüllt sind.', requireFields: 'Felder erforderlich', - requireFieldsDescription: 'Stellen Sie sicher, dass die wichtigsten Felder ausgefüllt sind, bevor Ausgaben eingereicht werden', + requireFieldsDescription: 'Belege, Kategorien usw. beim Einreichen.', + createAgentRule: 'Agentenregel', + createAgentRuleDescription: 'Beschreiben Sie flexible Regeln, die bei Bedarf ausgeführt werden.', }, expenseDefaultsTable: { tableColumnType: 'Typ', @@ -7725,6 +7733,11 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc confirmErrorAmount: 'Bitte geben Sie einen Betrag ein.', thenFlagForReview: 'Dann zur Überprüfung kennzeichnen, wenn:', }, + agentRulesEmptyState: { + title: 'Keine Agentenregeln hinzugefügt', + subtitle: 'Erstellen Sie eine Regel, um Ihre Arbeitsbereichsrichtlinien zu automatisieren.', + cta: 'KI-Regel hinzufügen', + }, }, planTypePage: { planTypes: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 50f48f2d444e..0df4c5f4d226 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -7417,6 +7417,7 @@ const translations = { expenseDefaults: 'Expense defaults', requireFields: 'Require fields', flagForReview: 'Flag for review', + agents: 'Agents', }, bulkActions: { deleteMultiple: () => ({ @@ -7593,11 +7594,13 @@ const translations = { restrictCardSpend: 'Restrict card spend', restrictCardSpendDescription: 'Block or limit spend at the point of sale', flagForReview: 'Flag for review', - flagForReviewDescription: 'Notify approvers when expenses exceed category limits', + flagForReviewDescription: 'Notify when your conditions are met.', requireFields: 'Require fields', - requireFieldsDescription: 'Make sure key fields are filled in before expenses are submitted', + requireFieldsDescription: 'Receipts, categories, etc, when submitting.', applyExpenseDefaults: 'Apply expense defaults', applyExpenseDefaultsDescription: 'Update fields without submitter doing anything', + createAgentRule: 'Agent rule', + createAgentRuleDescription: 'Describe flexible rules that run when you need.', }, expenseDefaultsTable: { tableColumnType: 'Type', @@ -7657,6 +7660,11 @@ const translations = { subtitle: 'Alert approvers when specific expenses are worth an extra review.', cta: 'Create flag rule', }, + agentRulesEmptyState: { + title: 'No agent rules added', + subtitle: 'Create a rule to automate your workspace policies.', + cta: 'Add AI rule', + }, flagForReviewRule: { title: 'Flag for review', subtitle: 'Notify approvers when the following conditions are met.', @@ -7845,19 +7853,24 @@ const translations = { agentRules: { title: 'Agent rules', subtitle: 'Set rules for how AI agents handle expenses on this workspace.', + revampSubtitle: 'Describe flexible rules that run when you need.', enforcedBy: 'Agent rules are enforced by', ruleBotName: 'RuleBot', addRule: 'Add agent rule', findRule: 'Find agent rule', addRuleTitle: 'Add rule', + newRuleTitle: 'New rule', editRuleTitle: 'Edit rule', deleteRule: 'Delete rule', deleteRuleConfirmation: 'Are you sure you want to delete this rule?', describeRuleTitle: 'Describe the rule for your AI agent to follow', + describeRuleForConcierge: 'Describe your rule and Concierge will build it', disclaimer: 'AI agents can make mistakes.', + nextButton: 'Next', agentCreatedTitle: 'RuleBot has been added to your workspace!', agentCreatedDescription: (agentsRoute: string) => `To enforce your agent rules, we’ve created an agent for you and added it as an admin to your workspace.

Edit your agent’s details in Account > Agents.
`, + gotIt: 'Got it', }, }, planTypePage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 9b8219bd0b36..8f1e591c2a0c 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -7485,6 +7485,11 @@ ${amount} para ${merchant} - ${date}`, agentCreatedTitle: '¡RuleBot se ha añadido a tu espacio de trabajo!', agentCreatedDescription: (agentsRoute: string) => `Para aplicar tus reglas de agente, hemos creado un agente para ti y lo hemos añadido como administrador de tu espacio de trabajo.

Edita los datos de tu agente en Cuenta > Agentes.
`, + revampSubtitle: 'Describe reglas flexibles que se ejecutan cuando las necesitas.', + newRuleTitle: 'Nueva regla', + describeRuleForConcierge: 'Describe tu regla y Concierge la creará', + nextButton: 'Siguiente', + gotIt: 'Entendido', }, tabs: { general: 'General', @@ -7492,6 +7497,7 @@ ${amount} para ${merchant} - ${date}`, expenseDefaults: 'Valores predeterminados de gastos', requireFields: 'Campos obligatorios', flagForReview: 'Marcar para revisión', + agents: 'Agentes', }, bulkActions: { deleteMultiple: () => ({ @@ -7550,9 +7556,11 @@ ${amount} para ${merchant} - ${date}`, applyExpenseDefaults: 'Aplicar valores predeterminados de gastos', applyExpenseDefaultsDescription: 'Actualizar campos sin que quien los envía haga nada', flagForReview: 'Marcar para revisión', - flagForReviewDescription: 'Notificar a los aprobadores cuando los gastos superen los límites de categoría', + flagForReviewDescription: 'Notificar cuando se cumplan tus condiciones.', requireFields: 'Campos obligatorios', - requireFieldsDescription: 'Asegúrate de que los campos clave estén rellenos antes de enviar los gastos', + requireFieldsDescription: 'Recibos, categorías, etc., al enviar.', + createAgentRule: 'Regla de agente', + createAgentRuleDescription: 'Describe reglas flexibles que se ejecutan cuando las necesitas.', }, expenseDefaultsTable: { tableColumnType: 'Tipo', @@ -7616,6 +7624,11 @@ ${amount} para ${merchant} - ${date}`, confirmErrorAmount: 'Por favor, introduce una cantidad.', thenFlagForReview: 'Luego marcar para revisión cuando:', }, + agentRulesEmptyState: { + title: 'No se han añadido reglas de agente', + subtitle: 'Crea una regla para automatizar las políticas de tu espacio de trabajo.', + cta: 'Añadir regla de IA', + }, }, emptyDomain: { title: 'Mejora tu seguridad con dominios', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 194741bd75ce..b75858128e3d 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -7621,6 +7621,11 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e agentCreatedTitle: 'RuleBot a été ajouté à votre espace de travail !', agentCreatedDescription: (agentsRoute: string) => `Pour appliquer vos règles d’agent, nous avons créé un agent pour vous et l’avons ajouté comme administrateur de votre espace de travail.

Modifiez les détails de votre agent dans Compte > Agents.
`, + revampSubtitle: 'Décrivez des règles flexibles qui s’exécutent quand vous en avez besoin.', + newRuleTitle: 'Nouvelle règle', + describeRuleForConcierge: 'Décrivez votre règle et Concierge la créera', + nextButton: 'Suivant', + gotIt: 'Compris', }, tabs: { general: 'Général', @@ -7628,6 +7633,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e expenseDefaults: 'Paramètres par défaut des dépenses', requireFields: 'Champs obligatoires', flagForReview: 'Marquer pour examen', + agents: 'Agents', }, bulkActions: { deleteMultiple: () => ({ @@ -7686,9 +7692,11 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e applyExpenseDefaults: 'Appliquer les valeurs de dépense par défaut', applyExpenseDefaultsDescription: 'Mettre à jour les champs sans que le déclarant ne fasse quoi que ce soit', flagForReview: 'Marquer pour examen', - flagForReviewDescription: 'Avertir les approbateurs lorsque les dépenses dépassent les limites de catégorie', + flagForReviewDescription: 'Avertir lorsque vos conditions sont remplies.', requireFields: 'Champs obligatoires', - requireFieldsDescription: 'Assurez-vous que les champs clés sont remplis avant que les dépenses soient soumises', + requireFieldsDescription: 'Reçus, catégories, etc., lors de la soumission.', + createAgentRule: 'Règle d’agent', + createAgentRuleDescription: 'Décrivez des règles flexibles qui s’exécutent quand vous en avez besoin.', }, expenseDefaultsTable: { tableColumnType: 'Type', @@ -7752,6 +7760,11 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e confirmErrorAmount: 'Veuillez saisir un montant.', thenFlagForReview: 'Puis signaler pour examen lorsque :', }, + agentRulesEmptyState: { + title: 'Aucune règle d’agent ajoutée', + subtitle: 'Créez une règle pour automatiser les politiques de votre espace de travail.', + cta: 'Ajouter une règle IA', + }, }, planTypePage: { planTypes: { diff --git a/src/languages/it.ts b/src/languages/it.ts index ced18bde4860..dd191668c17d 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -7578,6 +7578,11 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, agentCreatedTitle: 'RuleBot è stato aggiunto al tuo spazio di lavoro!', agentCreatedDescription: (agentsRoute: string) => `Per applicare le tue regole dell’agente, abbiamo creato un agente per te e lo abbiamo aggiunto come amministratore del tuo spazio di lavoro.

Modifica i dettagli del tuo agente in Account > Agenti.
`, + revampSubtitle: 'Descrivi regole flessibili che vengono eseguite quando ne hai bisogno.', + newRuleTitle: 'Nuova regola', + describeRuleForConcierge: 'Descrivi la tua regola e Concierge la creerà', + nextButton: 'Avanti', + gotIt: 'Capito', }, tabs: { general: 'Generale', @@ -7585,6 +7590,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, expenseDefaults: 'Impostazioni predefinite spese', requireFields: 'Rendi obbligatori i campi', flagForReview: 'Contrassegna per revisione', + agents: 'Agenti', }, bulkActions: { deleteMultiple: () => ({ @@ -7643,9 +7649,11 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, applyExpenseDefaults: 'Applica impostazioni predefinite spese', applyExpenseDefaultsDescription: 'Aggiorna i campi senza che chi invia debba fare nulla', flagForReview: 'Contrassegna per revisione', - flagForReviewDescription: 'Avvisa gli approvatori quando le spese superano i limiti di categoria', + flagForReviewDescription: 'Notifica quando le tue condizioni sono soddisfatte.', requireFields: 'Rendi obbligatori i campi', - requireFieldsDescription: 'Assicurati che i campi chiave siano compilati prima di inviare le spese', + requireFieldsDescription: 'Ricevute, categorie, ecc. durante l’invio.', + createAgentRule: 'Regola agente', + createAgentRuleDescription: 'Descrivi regole flessibili che vengono eseguite quando ne hai bisogno.', }, expenseDefaultsTable: { tableColumnType: 'Tipo', @@ -7709,6 +7717,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, confirmErrorAmount: 'Inserisci un importo.', thenFlagForReview: 'Poi segnala per revisione quando:', }, + agentRulesEmptyState: {title: 'Nessuna regola agente aggiunta', subtitle: 'Crea una regola per automatizzare le policy del tuo workspace.', cta: 'Aggiungi regola IA'}, }, planTypePage: { planTypes: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 00ac27dbd9a7..71727532d52f 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -7485,8 +7485,20 @@ ${reportName}`, agentCreatedTitle: 'RuleBot がワークスペースに追加されました!', agentCreatedDescription: (agentsRoute: string) => `エージェント ルールを適用するために、エージェントを作成し、ワークスペースの管理者として追加しました。

エージェントの詳細は 「アカウント」>「エージェント」 で編集できます。
`, + revampSubtitle: '必要なときに実行できる柔軟なルールを設定します。', + newRuleTitle: '新しいルール', + describeRuleForConcierge: 'ルールの内容を入力すると、Concierge が自動作成します', + nextButton: '次へ', + gotIt: '了解しました', + }, + tabs: { + general: '一般', + cardRestrictions: 'カードの制限', + expenseDefaults: '経費のデフォルト設定', + requireFields: '必須項目', + flagForReview: '確認のためにフラグを付ける', + agents: 'エージェント', }, - tabs: {general: '一般', cardRestrictions: 'カードの制限', expenseDefaults: '経費のデフォルト設定', requireFields: '必須項目', flagForReview: '確認のためにフラグを付ける'}, bulkActions: { deleteMultiple: () => ({ one: 'ルールを削除', @@ -7544,9 +7556,11 @@ ${reportName}`, applyExpenseDefaults: '経費のデフォルトを適用', applyExpenseDefaultsDescription: '申請者が何も操作しなくてもフィールドを更新する', flagForReview: '確認のためにフラグを付ける', - flagForReviewDescription: '経費がカテゴリの上限を超えたとき承認者に通知します', + flagForReviewDescription: '条件が満たされたときに通知します。', requireFields: '必須項目', - requireFieldsDescription: '経費を提出する前に、主要な項目が入力されていることを確認してください', + requireFieldsDescription: '提出時の領収書、カテゴリなど', + createAgentRule: 'エージェントルール', + createAgentRuleDescription: '必要なときに実行できる柔軟なルールを設定します。', }, expenseDefaultsTable: { tableColumnType: '種類', @@ -7610,6 +7624,7 @@ ${reportName}`, confirmErrorAmount: '金額を入力してください。', thenFlagForReview: '次の条件で確認フラグを付けます:', }, + agentRulesEmptyState: {title: 'エージェントルールが追加されていません', subtitle: 'ワークスペースのポリシーを自動化するルールを作成します。', cta: 'AIルールを追加'}, }, planTypePage: { planTypes: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index cd605d22e0d2..eb86e0bdcdaf 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -7556,6 +7556,11 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, agentCreatedTitle: 'RuleBot is toegevoegd aan je werkruimte!', agentCreatedDescription: (agentsRoute: string) => `Om je agentregels af te dwingen, hebben we een agent voor je gemaakt en deze als beheerder aan je werkruimte toegevoegd.

Bewerk de gegevens van je agent in Account > Agents.
`, + revampSubtitle: 'Beschrijf flexibele regels die worden uitgevoerd wanneer jij dat nodig hebt.', + newRuleTitle: 'Nieuwe regel', + describeRuleForConcierge: 'Beschrijf je regel en Concierge bouwt hem voor je', + nextButton: 'Volgende', + gotIt: 'Begrepen', }, tabs: { general: 'Algemeen', @@ -7563,6 +7568,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, expenseDefaults: 'Standaardinstellingen voor uitgaven', requireFields: 'Maak velden verplicht', flagForReview: 'Markeren voor controle', + agents: 'Agenten', }, bulkActions: { deleteMultiple: () => ({ @@ -7621,9 +7627,11 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, applyExpenseDefaults: 'Standaardinstellingen voor uitgaven toepassen', applyExpenseDefaultsDescription: 'Velden bijwerken zonder dat de indiener iets hoeft te doen', flagForReview: 'Markeren voor controle', - flagForReviewDescription: 'Goedkeurders informeren wanneer onkosten de categorielimieten overschrijden', + flagForReviewDescription: 'Stel een melding in wanneer aan je voorwaarden is voldaan.', requireFields: 'Maak velden verplicht', - requireFieldsDescription: 'Zorg dat belangrijke velden zijn ingevuld voordat onkosten worden ingediend', + requireFieldsDescription: 'Bonnetjes, categorieën, enz. bij het indienen.', + createAgentRule: 'Agentregel', + createAgentRuleDescription: 'Beschrijf flexibele regels die worden uitgevoerd wanneer jij dat nodig hebt.', }, expenseDefaultsTable: { tableColumnType: 'Type', @@ -7687,6 +7695,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, confirmErrorAmount: 'Voer een bedrag in.', thenFlagForReview: 'Vlag dan voor controle wanneer:', }, + agentRulesEmptyState: {title: 'Geen agentregels toegevoegd', subtitle: 'Maak een regel om je werkruimtebeleid te automatiseren.', cta: 'AI-regel toevoegen'}, }, planTypePage: { planTypes: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 2127c9808542..e72f364cd288 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -7538,6 +7538,11 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, agentCreatedTitle: 'RuleBot został dodany do Twojego obszaru roboczego!', agentCreatedDescription: (agentsRoute: string) => `Aby egzekwować Twoje reguły agenta, utworzyliśmy dla Ciebie agenta i dodaliśmy go jako administratora do Twojego obszaru roboczego.

Edytuj dane swojego agenta w sekcji Konto > Agenci.
`, + revampSubtitle: 'Opisuj elastyczne reguły, które uruchamiają się wtedy, kiedy tego potrzebujesz.', + newRuleTitle: 'Nowa reguła', + describeRuleForConcierge: 'Opisz swoją regułę, a Concierge ją utworzy', + nextButton: 'Dalej', + gotIt: 'Jasne', }, tabs: { general: 'Ogólne', @@ -7545,6 +7550,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, expenseDefaults: 'Domyślne ustawienia wydatków', requireFields: 'Wymagaj pól', flagForReview: 'Oznacz do przejrzenia', + agents: 'Agenci', }, bulkActions: { deleteMultiple: () => ({ @@ -7603,9 +7609,11 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, applyExpenseDefaults: 'Zastosuj domyślne ustawienia wydatków', applyExpenseDefaultsDescription: 'Aktualizuj pola bez wymagania działania od osoby zgłaszającej', flagForReview: 'Oznacz do przejrzenia', - flagForReviewDescription: 'Powiadamiaj zatwierdzających, gdy wydatki przekraczają limity kategorii', + flagForReviewDescription: 'Powiadom, gdy twoje warunki zostaną spełnione.', requireFields: 'Wymagaj pól', - requireFieldsDescription: 'Upewnij się, że kluczowe pola są wypełnione przed wysłaniem wydatków', + requireFieldsDescription: 'Paragony, kategorie itd. przy wysyłaniu.', + createAgentRule: 'Reguła agenta', + createAgentRuleDescription: 'Opisuj elastyczne reguły, które uruchamiają się wtedy, kiedy tego potrzebujesz.', }, expenseDefaultsTable: { tableColumnType: 'Typ', @@ -7669,6 +7677,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, confirmErrorAmount: 'Wpisz kwotę.', thenFlagForReview: 'Następnie oznacz do przejrzenia, gdy:', }, + agentRulesEmptyState: {title: 'Nie dodano reguł agenta', subtitle: 'Utwórz regułę, żeby zautomatyzować zasady swojego workspace’u.', cta: 'Dodaj regułę AI'}, }, planTypePage: { planTypes: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 0b7f5c5bb7c9..13acce6d9447 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -7548,6 +7548,11 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, agentCreatedTitle: 'O RuleBot foi adicionado ao seu espaço de trabalho!', agentCreatedDescription: (agentsRoute: string) => `Para aplicar suas regras de agente, criamos um agente para você e o adicionamos como administrador do seu espaço de trabalho.

Edite os detalhes do seu agente em Conta > Agentes.
`, + revampSubtitle: 'Descreva regras flexíveis que são executadas quando você precisar.', + newRuleTitle: 'Nova regra', + describeRuleForConcierge: 'Descreva sua regra e o Concierge vai criá-la', + nextButton: 'Próximo', + gotIt: 'Entendi', }, tabs: { general: 'Geral', @@ -7555,6 +7560,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, expenseDefaults: 'Padrões de despesa', requireFields: 'Tornar campos obrigatórios', flagForReview: 'Marcar para revisão', + agents: 'Agentes', }, bulkActions: { deleteMultiple: () => ({ @@ -7613,9 +7619,11 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, applyExpenseDefaults: 'Aplicar padrões de despesa', applyExpenseDefaultsDescription: 'Atualizar campos sem o responsável pelo envio fazer nada', flagForReview: 'Marcar para revisão', - flagForReviewDescription: 'Notificar aprovadores quando as despesas excederem os limites da categoria', + flagForReviewDescription: 'Notificar quando suas condições forem atendidas.', requireFields: 'Tornar campos obrigatórios', - requireFieldsDescription: 'Certifique-se de que os campos principais estejam preenchidos antes de enviar as despesas', + requireFieldsDescription: 'Recibos, categorias, etc., ao enviar.', + createAgentRule: 'Regra do agente', + createAgentRuleDescription: 'Descreva regras flexíveis que são executadas quando você precisar.', }, expenseDefaultsTable: { tableColumnType: 'Tipo', @@ -7679,6 +7687,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, confirmErrorAmount: 'Insira um valor.', thenFlagForReview: 'Então sinalizar para revisão quando:', }, + agentRulesEmptyState: {title: 'Nenhuma regra de agente adicionada', subtitle: 'Crie uma regra para automatizar as políticas do seu workspace.', cta: 'Adicionar regra de IA'}, }, planTypePage: { planTypes: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 24cdbbf2cbeb..3a2ef84cfce4 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -7342,8 +7342,13 @@ ${reportName}`, agentCreatedTitle: 'RuleBot 已添加到你的工作区!', agentCreatedDescription: (agentsRoute: string) => `为了执行你的代理规则,我们为你创建了一个代理,并将其添加为你工作区的管理员。

你可以在 “账户”>“代理” 中编辑代理的详细信息。
`, + revampSubtitle: '按需运行的灵活规则描述', + newRuleTitle: '新规则', + describeRuleForConcierge: '描述你的规则,Concierge 会帮你创建', + nextButton: '下一步', + gotIt: '知道了', }, - tabs: {general: '常规', cardRestrictions: '卡片限制', expenseDefaults: '报销默认设置', requireFields: '必填字段', flagForReview: '标记以供审核'}, + tabs: {general: '常规', cardRestrictions: '卡片限制', expenseDefaults: '报销默认设置', requireFields: '必填字段', flagForReview: '标记以供审核', agents: '代理人'}, bulkActions: { deleteMultiple: () => ({ one: '删除规则', @@ -7401,9 +7406,11 @@ ${reportName}`, applyExpenseDefaults: '应用报销默认设置', applyExpenseDefaultsDescription: '在提交人无须执行任何操作的情况下更新字段', flagForReview: '标记以供审核', - flagForReviewDescription: '当费用超出类别限额时通知审批人', + flagForReviewDescription: '在满足您的条件时通知。', requireFields: '必填字段', - requireFieldsDescription: '请确保在提交报销前填写所有关键字段', + requireFieldsDescription: '提交时的收据、类别等。', + createAgentRule: '代理规则', + createAgentRuleDescription: '按需运行的灵活规则描述', }, expenseDefaultsTable: { tableColumnType: '类型', @@ -7459,6 +7466,7 @@ ${reportName}`, confirmErrorAmount: '请输入金额。', thenFlagForReview: '然后在以下情况下标记为待审核:', }, + agentRulesEmptyState: {title: '未添加代理规则', subtitle: '创建规则以自动化您的工作区策略。', cta: '添加 AI 规则'}, }, planTypePage: { planTypes: { diff --git a/src/libs/AgentRulesUtils.ts b/src/libs/AgentRulesUtils.ts new file mode 100644 index 000000000000..ad15b9c64e7b --- /dev/null +++ b/src/libs/AgentRulesUtils.ts @@ -0,0 +1,31 @@ +import CONST from '@src/CONST'; +import type {AgentRule} from '@src/types/onyx/Policy'; + +type AgentRulesCollection = Record | undefined; + +type AgentRuleWithID = AgentRule & { + ruleID: string; +}; + +function getAgentRuleDisplayTitle(rule: AgentRule): string { + return (rule.title ?? rule.prompt).replaceAll(/\s+/g, ' ').trim(); +} + +function getSortedAgentRules(agentRules: AgentRulesCollection): AgentRuleWithID[] { + return Object.entries(agentRules ?? {}) + .filter(([, rule]) => !!rule) + .map(([ruleID, rule]) => ({...rule, ruleID})) + .sort((a, b) => { + if (a.created && b.created) { + return a.created < b.created ? 1 : -1; + } + + return 0; + }); +} + +function getVisibleAgentRules(agentRules: AgentRulesCollection, isOffline: boolean): AgentRuleWithID[] { + return getSortedAgentRules(agentRules).filter((rule) => isOffline || rule.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); +} + +export {getAgentRuleDisplayTitle, getVisibleAgentRules}; diff --git a/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx b/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx index ccb4cb021a53..7eb10b8c2db7 100644 --- a/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx +++ b/src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx @@ -15,6 +15,7 @@ import usePermissions from '@hooks/usePermissions'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import Tab from '@libs/actions/Tab'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; @@ -33,7 +34,7 @@ import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/AddAgentRuleForm'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import type {TextInputKeyPressEvent} from 'react-native'; +import type {StyleProp, TextInputKeyPressEvent, ViewStyle} from 'react-native'; import React, {useRef} from 'react'; import {View} from 'react-native'; @@ -51,6 +52,8 @@ function AddAgentRulePage({ const shouldUseScrollableLayout = useIsInLandscapeMode(); const {isBetaEnabled} = usePermissions(); const isCustomAgentEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); + const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); + const shouldUseExpandedRevampFormLayout = isRulesRevampEnabled && !shouldUseScrollableLayout; const policy = usePolicy(policyID); const formRef = useRef(null); const linkPressedRef = useRef(false); @@ -73,13 +76,23 @@ function AddAgentRulePage({ return errors; }; + const navigateBackToAgentsTab = () => { + if (isRulesRevampEnabled) { + Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS); + Navigation.goBack(ROUTES.WORKSPACE_RULES.getRoute(policyID)); + return; + } + + Navigation.goBack(); + }; + const saveRule = (values: FormOnyxValues): void => { // When the workspace has no agent rules yet, the backend creates the "RuleBot" agent and adds it as // an admin. Surface a one-time modal explaining this side effect before navigating back. const isFirstRule = isEmptyObject(policy?.rules?.agentRules); addPolicyAgentRule(policyID, rand64(), values[INPUT_IDS.PROMPT]); if (!isFirstRule) { - Navigation.goBack(); + navigateBackToAgentsTab(); return; } linkPressedRef.current = false; @@ -100,7 +113,7 @@ function AddAgentRulePage({ /> ), - confirmText: translate('common.buttonConfirm'), + confirmText: isRulesRevampEnabled ? translate('workspace.rules.agentRules.gotIt') : translate('common.buttonConfirm'), shouldShowCancelButton: false, shouldUseSuccessStyleForConfirm: true, iconSource: BotAvatarBlue, @@ -108,17 +121,29 @@ function AddAgentRulePage({ shouldCenterIcon: true, iconWidth: variables.iconSizeUltraLarge, iconHeight: variables.iconSizeUltraLarge, - iconAdditionalStyles: {borderRadius: variables.iconSizeUltraLarge / 2, overflow: 'hidden', marginTop: 12}, + iconAdditionalStyles: { + borderRadius: variables.iconSizeUltraLarge / 2, + overflow: 'hidden', + marginTop: 12, + }, }).then(() => { - if (!linkPressedRef.current) { + if (linkPressedRef.current) { + Navigation.navigate(ROUTES.SETTINGS_AGENTS); return; } - Navigation.navigate(ROUTES.SETTINGS_AGENTS); + + if (isRulesRevampEnabled) { + Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS); + } }); }, }); }; + const inputWrapperStyles: StyleProp = shouldUseExpandedRevampFormLayout + ? [styles.flex1, styles.mnh0, styles.agentRulePromptInput] + : [styles.flex1, shouldUseScrollableLayout && styles.minHeight42]; + return ( - + - + (null); + const describeRuleLabel = isRulesRevampEnabled ? translate('workspace.rules.agentRules.describeRuleForConcierge') : translate('workspace.rules.agentRules.describeRuleTitle'); const handleKeyPress = (e: TextInputKeyPressEvent | KeyboardEvent) => { if (!('key' in e)) { @@ -103,6 +106,10 @@ function EditAgentRulePage({ return ; } + const inputWrapperStyles: StyleProp = shouldUseExpandedRevampFormLayout + ? [styles.flex1, styles.mnh0, styles.agentRulePromptInput] + : [styles.flex1, shouldUseScrollableLayout && styles.minHeight42]; + return ( - + 0; // RuleBot is the agent the backend provisions on the first Agent rule and stores on the policy. const ruleBotAccountID = policy?.ruleBotAccountID; @@ -51,20 +52,6 @@ function AgentRulesSection({policyID, canWriteRules, showReadOnlyModal}: AgentRu // ruleBotAccountID stays set on the policy after RuleBot is removed from the workspace, so also require it to still be an active member before showing the "enforced by" line. const isRuleBotActiveMember = isPolicyMemberWithoutPendingDelete(ruleBot?.login, policy); - const sortedRules = Object.entries(agentRules ?? {}) - .filter(([, rule]) => !!rule) - .map(([ruleID, rule]) => ({...rule, ruleID})) - .sort((a, b) => { - if (a.created && b.created) { - return a.created < b.created ? 1 : -1; - } - return 0; - }); - - // Exclude pending-delete rules when online because OfflineWithFeedback hides them visually. - // When offline, keep them so OfflineWithFeedback can show strikethrough styling. - const visibleRules = sortedRules.filter((rule) => isOffline || rule.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE); - const renderTitle = () => ( {translate('workspace.rules.agentRules.title')} @@ -112,7 +99,7 @@ function AgentRulesSection({policyID, canWriteRules, showReadOnlyModal}: AgentRu onClose={() => clearPolicyAgentRuleErrors(policyID, rule.ruleID, rule)} > ; - const RULES_TAB_VALUES = new Set(Object.values(RULES_TAB)); function isRulesTab(key: string): key is RulesTab { return RULES_TAB_VALUES.has(key); } -function isTableSelectionTab(tab: RulesTab): tab is Exclude { - return tab !== RULES_TAB.GENERAL; -} - function updateSelectionKeysIfChanged(previousKeys: string[], nextKeys: string[]) { if (previousKeys.length === nextKeys.length && previousKeys.every((key, index) => key === nextKeys.at(index))) { return previousKeys; @@ -83,19 +81,22 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { useWorkspaceDocumentTitle(policy?.name, 'workspace.common.rules'); const styles = useThemeStyles(); const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {isOffline} = useNetwork(); const illustrations = useMemoizedLazyIllustrations(['Flash']); - const icons = useMemoizedLazyExpensifyIcons(['Plus', 'Feed', 'CreditCardExclamation', 'DocumentMagicWand', 'Task', 'Flag', 'Trashcan']); + const icons = useMemoizedLazyExpensifyIcons(['Plus', 'Feed', 'CreditCardExclamation', 'DocumentMagicWand', 'Task', 'Flag', 'Bot', 'Trashcan']); const {canWrite: canWriteRules, showReadOnlyModal} = usePolicyFeatureWriteAccess(policy, CONST.POLICY.POLICY_FEATURE.RULES); const {isBetaEnabled} = usePermissions(); const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); + const isCustomAgentBetaEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); const isMobileSelectionModeEnabled = useMobileSelectionMode(); const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); const [isAgentsRulesBannerDismissed = false] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {selector: agentsRulesBannerDismissedSelector}); const [lastSelectedTab] = useOnyx(`${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RULES_TAB_TYPE}`); const lastSelectedTabStr = lastSelectedTab as string | undefined; - const activeTab: RulesTab = lastSelectedTabStr && isRulesTab(lastSelectedTabStr) ? lastSelectedTabStr : RULES_TAB.GENERAL; - const [selectedRuleKeysByTab, setSelectedRuleKeysByTab] = useState, string[]>>>({}); + const resolvedTab: RulesTab = lastSelectedTabStr && isRulesTab(lastSelectedTabStr) ? lastSelectedTabStr : RULES_TAB.GENERAL; + const activeTab: RulesTab = resolvedTab === RULES_TAB.AGENTS && !isCustomAgentBetaEnabled ? RULES_TAB.GENERAL : resolvedTab; + const [selectedRuleKeysByTab, setSelectedRuleKeysByTab] = useState>>({}); const {showConfirmModal} = useConfirmModal(); @@ -126,7 +127,7 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { turnOffMobileSelectionMode(); }, [activeTab]); - const updateTabSelectionKeys = useCallback((tab: Exclude, selectedRowKeys: string[]) => { + const updateTabSelectionKeys = useCallback((tab: TableSelectionTab, selectedRowKeys: string[]) => { setSelectedRuleKeysByTab((prev) => { const nextKeys = updateSelectionKeysIfChanged(prev[tab] ?? [], selectedRowKeys); if (prev[tab] === nextKeys) { @@ -154,6 +155,9 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { const hasSelectedRules = selectedRuleKeys.length > 0; const isTableTab = activeTab === RULES_TAB.CARD_RESTRICTIONS || activeTab === RULES_TAB.EXPENSE_DEFAULTS || activeTab === RULES_TAB.REQUIRE_FIELDS || activeTab === RULES_TAB.FLAG_FOR_REVIEW; + const isAgentsTab = activeTab === RULES_TAB.AGENTS && isCustomAgentBetaEnabled; + const hasAgentRules = getVisibleAgentRules(policy?.rules?.agentRules, isOffline).length > 0; + const shouldUseFullWidthAgentsTabLayout = isAgentsTab && !hasAgentRules; const shouldShowBulkActions = canWriteRules && isTableTab && (shouldUseNarrowLayout ? isMobileSelectionModeEnabled : hasSelectedRules); const shouldShowAddRuleButton = activeTab === RULES_TAB.GENERAL || !shouldShowBulkActions; @@ -170,7 +174,9 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { return [ { icon: icons.Trashcan, - text: translate('workspace.rules.bulkActions.deleteMultiple', {count: selectedRuleKeys.length}), + text: translate('workspace.rules.bulkActions.deleteMultiple', { + count: selectedRuleKeys.length, + }), value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, onSelected: async () => { const {action} = await showConfirmModal({ @@ -217,6 +223,15 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { title: translate('workspace.rules.tabs.flagForReview'), icon: icons.Flag, }, + ...(isCustomAgentBetaEnabled + ? [ + { + key: RULES_TAB.AGENTS, + title: translate('workspace.rules.tabs.agents'), + icon: icons.Bot, + }, + ] + : []), ]; const handleNewRule = () => { @@ -234,7 +249,9 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { onPress={() => null} shouldAlwaysShowDropdownMenu buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} - customText={translate('workspace.common.selected', {count: selectedRuleKeys.length})} + customText={translate('workspace.common.selected', { + count: selectedRuleKeys.length, + })} options={getBulkActionsButtonOptions()} isSplitButton={false} style={[shouldDisplayButtonsInSeparateLine && styles.w100, shouldDisplayButtonsInSeparateLine && styles.mb3]} @@ -308,7 +325,15 @@ function PolicyRulesPageRevamp({route}: PolicyRulesPageRevampProps) { {shouldDisplayButtonsInSeparateLine && !!headerButtons && {headerButtons}} - + {activeTab === RULES_TAB.GENERAL && ( )} + {isAgentsTab && ( + + + + )} diff --git a/src/pages/workspace/rules/RulesNewPage.tsx b/src/pages/workspace/rules/RulesNewPage.tsx index ee2c4f3d607a..3caf7170f4d2 100644 --- a/src/pages/workspace/rules/RulesNewPage.tsx +++ b/src/pages/workspace/rules/RulesNewPage.tsx @@ -32,7 +32,8 @@ function RulesNewPage({route}: RulesNewPageProps) { const styles = useThemeStyles(); const {isBetaEnabled} = usePermissions(); const isRulesRevampEnabled = isBetaEnabled(CONST.BETAS.RULES_REVAMP); - const illustrations = useMemoizedLazyIllustrations(['CardReaderAlt', 'Flag', 'CheckboxText', 'ReportReceipt']); + const isCustomAgentBetaEnabled = isBetaEnabled(CONST.BETAS.CUSTOM_AGENT); + const illustrations = useMemoizedLazyIllustrations(['CardReaderAlt', 'Flag', 'CheckboxText', 'ReportReceipt', 'AiBot']); return ( Navigation.navigate(ROUTES.RULES_MERCHANT_NEW.getRoute(policyID))} + onPress={() => Navigation.navigate(ROUTES.RULES_FLAG_FOR_REVIEW_RULE_NEW.getRoute(policyID))} displayInDefaultIconColor iconWidth={variables.iconSizeExtraLarge} iconHeight={variables.iconSizeExtraLarge} wrapperStyle={styles.rulesNewMenuItem} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.NEW_RULE_MENU_ITEM_APPLY_EXPENSE_DEFAULTS} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.NEW_RULE_MENU_ITEM_FLAG_FOR_REVIEW} /> Navigation.navigate(ROUTES.RULES_FLAG_FOR_REVIEW_RULE_NEW.getRoute(policyID))} + onPress={() => Navigation.navigate(ROUTES.RULES_MERCHANT_NEW.getRoute(policyID))} displayInDefaultIconColor iconWidth={variables.iconSizeExtraLarge} iconHeight={variables.iconSizeExtraLarge} wrapperStyle={styles.rulesNewMenuItem} - sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.NEW_RULE_MENU_ITEM_FLAG_FOR_REVIEW} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.NEW_RULE_MENU_ITEM_APPLY_EXPENSE_DEFAULTS} /> + {isCustomAgentBetaEnabled && ( + Navigation.navigate(ROUTES.RULES_AGENT_NEW.getRoute(policyID))} + displayInDefaultIconColor + iconWidth={variables.iconSizeExtraLarge} + iconHeight={variables.iconSizeExtraLarge} + wrapperStyle={styles.rulesNewMenuItem} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.NEW_RULE_MENU_ITEM_CREATE_AGENT_RULE} + /> + )} diff --git a/src/pages/workspace/rules/tabs/RulesAgentsTab.tsx b/src/pages/workspace/rules/tabs/RulesAgentsTab.tsx new file mode 100644 index 000000000000..7e9981c3808d --- /dev/null +++ b/src/pages/workspace/rules/tabs/RulesAgentsTab.tsx @@ -0,0 +1,159 @@ +import Badge from '@components/Badge'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import ScrollView from '@components/ScrollView'; +import Section from '@components/Section'; +import Text from '@components/Text'; +import UserPill from '@components/UserPill'; + +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import usePolicy from '@hooks/usePolicy'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; + +import {getAgentRuleDisplayTitle, getVisibleAgentRules} from '@libs/AgentRulesUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import {isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; + +import {clearPolicyAgentRuleErrors} from '@userActions/Policy/Rules'; + +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; + +import React from 'react'; +import {View} from 'react-native'; + +import RulesTabEmptyState from './RulesTabEmptyState'; + +type RulesAgentsTabProps = { + policyID: string; + canWriteRules: boolean; + showReadOnlyModal: () => void; +}; + +function RulesAgentsTab({policyID, canWriteRules, showReadOnlyModal}: RulesAgentsTabProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const theme = useTheme(); + const {isOffline} = useNetwork(); + const policy = usePolicy(policyID); + const personalDetailsList = usePersonalDetails(); + const illustrations = useMemoizedLazyIllustrations(['AgentsIceCream']); + const icons = useMemoizedLazyExpensifyIcons(['Plus']); + + const agentRules = policy?.rules?.agentRules; + const visibleRules = getVisibleAgentRules(agentRules, isOffline); + const hasRules = visibleRules.length > 0; + + const ruleBotAccountID = policy?.ruleBotAccountID; + const ruleBot = ruleBotAccountID ? personalDetailsList?.[ruleBotAccountID] : undefined; + const ruleBotDisplayName = ruleBot?.displayName ?? ruleBot?.login ?? translate('workspace.rules.agentRules.ruleBotName'); + const isRuleBotActiveMember = isPolicyMemberWithoutPendingDelete(ruleBot?.login, policy); + + const handleAddAgentRule = () => { + if (!canWriteRules) { + showReadOnlyModal(); + return; + } + + Navigation.navigate(ROUTES.RULES_AGENT_NEW.getRoute(policyID)); + }; + + const handleEditAgentRule = (ruleID: string, pendingAction?: PendingAction) => { + if (!canWriteRules) { + showReadOnlyModal(); + return; + } + + if (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return; + } + + Navigation.navigate(ROUTES.RULES_AGENT_EDIT.getRoute(policyID, ruleID)); + }; + + if (!hasRules) { + return ( + + ); + } + + const renderTitle = () => ( + + {translate('workspace.rules.agentRules.title')} + + + ); + + const renderSubtitle = () => ( + + {translate('workspace.rules.agentRules.revampSubtitle')} + {!!ruleBotAccountID && isRuleBotActiveMember && ( + + {translate('workspace.rules.agentRules.enforcedBy')} + + + )} + + ); + + return ( + +
+ + {visibleRules.map((rule) => ( + clearPolicyAgentRuleErrors(policyID, rule.ruleID, rule)} + > + handleEditAgentRule(rule.ruleID, rule.pendingAction)} + sentryLabel={CONST.SENTRY_LABEL.WORKSPACE.RULES.AGENT_RULE_ITEM} + disabled={rule.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE} + /> + + ))} + +
+
+ ); +} + +export default RulesAgentsTab; diff --git a/src/pages/workspace/rules/tabs/RulesGeneralTab.tsx b/src/pages/workspace/rules/tabs/RulesGeneralTab.tsx index d73a0a3c695c..e41399098abf 100644 --- a/src/pages/workspace/rules/tabs/RulesGeneralTab.tsx +++ b/src/pages/workspace/rules/tabs/RulesGeneralTab.tsx @@ -4,13 +4,12 @@ import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; +import Tab from '@libs/actions/Tab'; import {dismissProductTraining} from '@libs/actions/Welcome'; -import Navigation from '@libs/Navigation/Navigation'; import IndividualExpenseRulesSectionRevamp from '@pages/workspace/rules/IndividualExpenseRulesSectionRevamp'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; import React from 'react'; @@ -37,7 +36,7 @@ function RulesGeneralTab({policyID, canWriteRules, isAgentsRulesBannerDismissed} title={translate('workspace.rules.agentsPromoBanner.title')} subtitle={translate('workspace.rules.agentsPromoBanner.subtitle')} ctaText={translate('workspace.rules.agentsPromoBanner.cta')} - onCtaPress={() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID))} + onCtaPress={() => Tab.setSelectedTab(CONST.TAB.RULES_TAB_TYPE, CONST.TAB.RULES.AGENTS)} ctaSentryLabel={CONST.SENTRY_LABEL.AGENTS_RULES_BANNER.CTA} onDismiss={() => dismissProductTraining(CONST.AGENTS_RULES_BANNER, true)} dismissSentryLabel={CONST.SENTRY_LABEL.AGENTS_RULES_BANNER.DISMISS} diff --git a/src/pages/workspace/rules/tabs/RulesTabEmptyState.tsx b/src/pages/workspace/rules/tabs/RulesTabEmptyState.tsx index e13e11d99a2a..008d8d7754dd 100644 --- a/src/pages/workspace/rules/tabs/RulesTabEmptyState.tsx +++ b/src/pages/workspace/rules/tabs/RulesTabEmptyState.tsx @@ -19,11 +19,12 @@ type RulesTabEmptyStateProps = { title: string; subtitle: string; buttonText: string; + buttonIcon?: IconAsset; onPress: () => void; isDisabled: boolean; }; -function RulesTabEmptyState({illustration, headerContentStyles, title, subtitle, buttonText, onPress, isDisabled}: RulesTabEmptyStateProps) { +function RulesTabEmptyState({illustration, headerContentStyles, title, subtitle, buttonText, buttonIcon, onPress, isDisabled}: RulesTabEmptyStateProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -40,7 +41,6 @@ function RulesTabEmptyState({illustration, headerContentStyles, title, subtitle, title={title} subtitle={subtitle} subtitleStyles={[styles.textLabel, styles.textSupporting]} - minModalHeight={0} cardContentStyles={styles.ph0} containerStyles={[styles.alignItemsCenter, styles.w100, styles.alignSelfCenter, StyleUtils.getMaximumWidth(variables.cardRulesEmptyStateMaxWidth)]} buttons={[ @@ -48,6 +48,7 @@ function RulesTabEmptyState({illustration, headerContentStyles, title, subtitle, buttonText, buttonAction: onPress, success: true, + icon: buttonIcon, isDisabled, }, ]} diff --git a/src/pages/workspace/rules/tabs/useRulesTableBulkActions.ts b/src/pages/workspace/rules/tabs/useRulesTableBulkActions.ts index 5df265201621..e587fd3e78fd 100644 --- a/src/pages/workspace/rules/tabs/useRulesTableBulkActions.ts +++ b/src/pages/workspace/rules/tabs/useRulesTableBulkActions.ts @@ -38,7 +38,7 @@ const DEFAULT_SPEND_RULE_ID = 'default-rule'; const RULES_TAB = CONST.TAB.RULES; type RulesTab = ValueOf; -type TableSelectionTab = Exclude; +type TableSelectionTab = Exclude; type UseRulesTableBulkActionsParams = { policyID: string; @@ -49,7 +49,7 @@ type UseRulesTableBulkActionsParams = { }; function isTableSelectionTab(tab: RulesTab): tab is TableSelectionTab { - return tab !== RULES_TAB.GENERAL; + return tab !== RULES_TAB.GENERAL && tab !== RULES_TAB.AGENTS; } function useRulesTableBulkActions({policyID, activeTab, selectedRuleKeysByTab, canWriteRules, clearTableSelection}: UseRulesTableBulkActionsParams) { @@ -274,4 +274,6 @@ function useRulesTableBulkActions({policyID, activeTab, selectedRuleKeysByTab, c }; } +export {isTableSelectionTab}; +export type {RulesTab, TableSelectionTab}; export default useRulesTableBulkActions; diff --git a/src/styles/index.ts b/src/styles/index.ts index d10ea3dc152d..5c4717cf8061 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -5542,6 +5542,15 @@ const staticStyles = (theme: ThemeColors) => height: variables.sortingMachineRulesEmptyStateIllustrationHeight, }, + agentsRulesEmptyStateIllustration: { + width: variables.agentsRulesEmptyStateIllustrationWidth, + height: variables.agentsRulesEmptyStateIllustrationHeight, + }, + + agentRulePromptInput: { + maxHeight: variables.agentRulePromptInputHeight, + }, + emptyStateSamlIllustration: { width: 183, height: 160, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index 66784ffd5a17..771c13fdc1a3 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -317,10 +317,13 @@ export default { cardRulesEmptyStateIllustrationHeight: 196, sortingMachineRulesEmptyStateIllustrationWidth: 229, sortingMachineRulesEmptyStateIllustrationHeight: 240, + agentsRulesEmptyStateIllustrationWidth: 268, + agentsRulesEmptyStateIllustrationHeight: 194, cardRulesEmptyStateMaxWidth: 496, expensifyCardEmptyIllustrationWidth: 280, expensifyCardEmptyIllustrationHeight: 172, rulesNewMenuItemMinHeight: 84, + agentRulePromptInputHeight: 340, cardPreviewWidth: 235, cardDetailsActionButtonMinWidth: 140, cardScarfOverlayWidth: 264,