diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 80bf3acf5d242..665a7f8cccee3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -997,6 +997,8 @@ const ONYXKEYS = { EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName', EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft', EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit', + EDIT_TRAVEL_INVOICING_MONTHLY_LIMIT_FORM: 'editTravelInvoicingMonthlyLimit', + EDIT_TRAVEL_INVOICING_MONTHLY_LIMIT_DRAFT_FORM: 'editTravelInvoicingMonthlyLimitDraft', EDIT_EXPENSIFY_CARD_LIMIT_DRAFT_FORM: 'editExpensifyCardLimitDraft', EDIT_EXPENSIFY_CARD_LIMIT_TYPE_FORM: 'editExpensifyCardLimitType', EDIT_EXPENSIFY_CARD_LIMIT_TYPE_DRAFT_FORM: 'editExpensifyCardLimitTypeDraft', @@ -1174,6 +1176,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm; + [ONYXKEYS.FORMS.EDIT_TRAVEL_INVOICING_MONTHLY_LIMIT_FORM]: FormTypes.EditTravelInvoicingMonthlyLimitForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_TYPE_FORM]: FormTypes.EditExpensifyCardLimitTypeForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; [ONYXKEYS.FORMS.NETSUITE_CUSTOM_FIELD_FORM]: FormTypes.NetSuiteCustomFieldForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2da202fba65c7..50bc748b680ba 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2771,6 +2771,10 @@ const ROUTES = { route: 'workspaces/:policyID/travel/settings/frequency', getRoute: (policyID: string) => `workspaces/${policyID}/travel/settings/frequency` as const, }, + WORKSPACE_TRAVEL_SETTINGS_MONTHLY_LIMIT: { + route: 'workspaces/:policyID/travel/settings/monthly-limit', + getRoute: (policyID: string) => `workspaces/${policyID}/travel/settings/monthly-limit` as const, + }, WORKSPACE_TRAVEL_MISSING_PERSONAL_DETAILS: { route: 'workspaces/:policyID/travel/missing-personal-details', getRoute: (policyID: string) => `workspaces/${policyID}/travel/missing-personal-details` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 7e3bf21212745..31b07b0bcde46 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -791,6 +791,7 @@ const SCREENS = { TRAVEL: 'Travel', TRAVEL_SETTINGS_ACCOUNT: 'Workspace_Travel_Settings_Account', TRAVEL_SETTINGS_FREQUENCY: 'Workspace_Travel_Settings_Frequency', + TRAVEL_SETTINGS_MONTHLY_LIMIT: 'Workspace_Travel_Settings_Monthly_Limit', TRAVEL_EXPORT: 'Workspace_Travel_Invoicing_Export', TRAVEL_MISSING_PERSONAL_DETAILS: 'Travel_Missing_Personal_Details', CREATE_DISTANCE_RATE: 'Create_Distance_Rate', diff --git a/src/languages/de.ts b/src/languages/de.ts index 804bd94a6a2a0..46753a7601f26 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -5420,6 +5420,11 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU settlementAccountLabel: 'Verrechnungskonto', settlementFrequencyLabel: 'Auszahlungsfrequenz', settlementFrequencyDescription: 'Wie oft Expensify Ihr Geschäftskonto belastet, um aktuelle Expensify Travel-Transaktionen zu begleichen.', + monthlySpendLimitLabel: 'Monatliches Ausgabelimit pro Mitglied', + monthlySpendLimitDescription: 'Der maximale Betrag, den jedes Mitglied pro Monat für Reisen ausgeben kann.', + reduceLimitTitle: 'Reise-Ausgabelimit reduzieren?', + reduceLimitWarning: + 'Wenn Sie das Limit reduzieren, können Mitglieder, die diesen Betrag bereits überschritten haben, bis zum nächsten Monat keine neuen Reisebuchungen vornehmen.', }, }, disableModal: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 177e54d4603f8..ca0e3805d978e 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -5425,6 +5425,10 @@ const translations = { settlementAccountLabel: 'Settlement account', settlementFrequencyLabel: 'Settlement frequency', settlementFrequencyDescription: 'How often Expensify will pull from your business bank account to settle recent Expensify Travel transactions.', + monthlySpendLimitLabel: 'Monthly spend limit per member', + monthlySpendLimitDescription: 'The maximum amount each member can spend on travel per month.', + reduceLimitTitle: 'Reduce travel spend limit?', + reduceLimitWarning: 'If you reduce the limit, members who have already spent more than this amount will be unable to make new travel bookings until next month.', }, }, disableModal: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 08e608ff3c893..f65a4f65a8b59 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -5310,6 +5310,10 @@ ${amount} para ${merchant} - ${date}`, settlementFrequencyLabel: 'Frecuencia de liquidación', settlementFrequencyDescription: 'Con qué frecuencia Expensify retirará fondos de la cuenta bancaria de tu empresa para liquidar transacciones recientes de Expensify Travel.', + monthlySpendLimitLabel: 'Límite de gasto mensual por miembro', + monthlySpendLimitDescription: 'El monto máximo que cada miembro puede gastar en viajes por mes.', + reduceLimitTitle: '¿Reducir el límite de gasto en viajes?', + reduceLimitWarning: 'Si reduces el límite, los miembros que ya hayan gastado más de este monto no podrán hacer nuevas reservas de viaje hasta el próximo mes.', }, }, disableModal: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 889ba7bd3327b..b71b20c3c1dc3 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -5439,6 +5439,11 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. settlementFrequencyLabel: 'Fréquence de règlement', settlementFrequencyDescription: 'Fréquence à laquelle Expensify prélèvera sur votre compte bancaire professionnel pour régler les transactions récentes d’Expensify Travel.', + monthlySpendLimitLabel: 'Limite de dépenses mensuelle par membre', + monthlySpendLimitDescription: 'Le montant maximum que chaque membre peut dépenser en déplacements par mois.', + reduceLimitTitle: 'Réduire la limite de dépenses de voyage\u00A0?', + reduceLimitWarning: + 'Si vous réduisez la limite, les membres ayant déjà dépensé plus que ce montant ne pourront pas effectuer de nouvelles réservations de voyage avant le mois prochain.', }, }, disableModal: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 1dbbdf39e215a..7acb2d3222864 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -5412,6 +5412,11 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. settlementFrequencyLabel: 'Frequenza di regolamento', settlementFrequencyDescription: 'Con quale frequenza Expensify preleverà dal tuo conto bancario aziendale per saldare le recenti transazioni di Expensify Travel.', + monthlySpendLimitLabel: 'Limite di spesa mensile per membro', + monthlySpendLimitDescription: "L'importo massimo che ciascun membro può spendere in viaggi al mese.", + reduceLimitTitle: 'Ridurre il limite di spesa per i viaggi?', + reduceLimitWarning: + 'Se riduci il limite, i membri che hanno già speso più di questo importo non potranno effettuare nuove prenotazioni di viaggio fino al mese prossimo.', }, }, disableModal: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 15845d5a9c3c2..a77005213a790 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -5351,6 +5351,10 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO settlementAccountLabel: '決済口座', settlementFrequencyLabel: '清算頻度', settlementFrequencyDescription: 'Expensify が直近の Expensify Travel 取引を精算するために、あなたのビジネス銀行口座から資金を引き落とす頻度。', + monthlySpendLimitLabel: 'メンバーごとの月間支出上限', + monthlySpendLimitDescription: '各メンバーが1か月に出張に使える最大金額。', + reduceLimitTitle: '出張支出上限を引き下げますか?', + reduceLimitWarning: 'この上限を引き下げると、すでにこの金額を超えて支出しているメンバーは、翌月まで新しい出張予約ができなくなります。', }, }, disableModal: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index f021695962964..aecb83c112306 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -5399,6 +5399,10 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ settlementAccountLabel: 'Verrekeningsrekening', settlementFrequencyLabel: 'Uitbetalingsfrequentie', settlementFrequencyDescription: 'Hoe vaak Expensify geld van uw zakelijke bankrekening zal incasseren om recente Expensify Travel-transacties te vereffenen.', + monthlySpendLimitLabel: 'Maandelijks bestedingslimiet per lid', + monthlySpendLimitDescription: 'Het maximale bedrag dat elk lid per maand aan reizen kan besteden.', + reduceLimitTitle: 'Reisbestedingslimiet verlagen?', + reduceLimitWarning: 'Als u het limiet verlaagt, kunnen leden die dit bedrag al hebben overschreden geen nieuwe reisboekingen maken tot volgende maand.', }, }, disableModal: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 8b04775a09441..be3b141bffc19 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -5387,6 +5387,11 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy settlementAccountLabel: 'Konto rozliczeniowe', settlementFrequencyLabel: 'Częstotliwość rozliczeń', settlementFrequencyDescription: 'Jak często Expensify będzie pobierać środki z firmowego konta bankowego, aby rozliczyć ostatnie transakcje Expensify Travel.', + monthlySpendLimitLabel: 'Miesięczny limit wydatków na członka', + monthlySpendLimitDescription: 'Maksymalna kwota, jaką każdy członek może wydać na podróże w ciągu miesiąca.', + reduceLimitTitle: 'Zmniejszyć limit wydatków na podróże?', + reduceLimitWarning: + 'Jeśli zmniejszysz limit, członkowie, którzy już wydali więcej niż ta kwota, nie będą mogli dokonywać nowych rezerwacji podróży do następnego miesiąca.', }, }, disableModal: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 9238e47c4625d..f3d202f1f7e54 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -5394,6 +5394,10 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS settlementFrequencyLabel: 'Frequência de liquidação', settlementFrequencyDescription: 'Com que frequência o Expensify vai debitar da sua conta bancária empresarial para liquidar as transações recentes do Expensify Travel.', + monthlySpendLimitLabel: 'Limite de gastos mensal por membro', + monthlySpendLimitDescription: 'O valor máximo que cada membro pode gastar em viagens por mês.', + reduceLimitTitle: 'Reduzir o limite de gastos com viagens?', + reduceLimitWarning: 'Se você reduzir o limite, os membros que já gastaram mais do que esse valor não poderão fazer novas reservas de viagem até o próximo mês.', }, }, disableModal: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 34e4c9d2daf3a..fdfda9b1e693f 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -5256,6 +5256,10 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM settlementAccountLabel: '结算账户', settlementFrequencyLabel: '结算频率', settlementFrequencyDescription: 'Expensify 从您的企业银行账户中扣款以结算最近 Expensify Travel 交易的频率。', + monthlySpendLimitLabel: '每位成员的月度支出限额', + monthlySpendLimitDescription: '每位成员每月可用于出差的最高金额。', + reduceLimitTitle: '降低出差支出限额?', + reduceLimitWarning: '如果您降低限额,已超出该金额的成员将无法进行新的出差预订,直至下个月。', }, }, disableModal: {title: '关闭差旅开票?', body: '即将到来的酒店和汽车租赁预订可能需要使用不同的付款方式重新预订,以避免被取消。', confirm: '关闭'}, diff --git a/src/libs/API/parameters/UpdateTravelInvoicingMonthlyLimitParams.ts b/src/libs/API/parameters/UpdateTravelInvoicingMonthlyLimitParams.ts new file mode 100644 index 0000000000000..266c5e8678400 --- /dev/null +++ b/src/libs/API/parameters/UpdateTravelInvoicingMonthlyLimitParams.ts @@ -0,0 +1,6 @@ +type UpdateTravelInvoicingMonthlyLimitParams = { + domainAccountID: number; + monthlySpendLimitPerUser: number; +}; + +export default UpdateTravelInvoicingMonthlyLimitParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 04208236c0012..d99c4e1711426 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -390,6 +390,7 @@ export type {default as ConfigureTravelInvoicingForPolicyParams} from './Configu export type {default as DeactivateTravelInvoicingParams} from './DeactivateTravelInvoicingParams'; export type {default as SetTravelInvoicingSettlementAccountParams} from './SetTravelInvoicingSettlementAccountParams'; export type {default as PayTravelInvoicingSpendParams} from './PayTravelInvoicingSpendParams'; +export type {default as UpdateTravelInvoicingMonthlyLimitParams} from './UpdateTravelInvoicingMonthlyLimitParams'; export type {default as UpdateTravelInvoicingSettlementFrequencyParams} from './UpdateTravelInvoicingSettlementFrequencyParams'; export type {default as GetTravelInvoiceStatementPDFParams} from './GetTravelInvoiceStatementPDFParams'; export type {default as ExportTravelInvoiceStatementCSVParams} from './ExportTravelInvoiceStatementCSVParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index fafe1c8bf369b..d0cb32247d1ba 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -485,6 +485,7 @@ const WRITE_COMMANDS = { DEACTIVATE_TRAVEL_INVOICING: 'DeactivateTravelInvoicing', SET_TRAVEL_INVOICING_SETTLEMENT_ACCOUNT: 'SetTravelInvoicingSettlementAccount', UPDATE_TRAVEL_INVOICE_SETTLEMENT_FREQUENCY: 'UpdateTravelInvoiceSettlementFrequency', + UPDATE_TRAVEL_INVOICING_MONTHLY_LIMIT: 'UpdateTravelInvoicingMonthlyLimit', PAY_TRAVEL_INVOICING_SPEND: 'PayTravelInvoicingSpend', UPDATE_XERO_IMPORT_TRACKING_CATEGORIES: 'UpdateXeroImportTrackingCategories', UPDATE_XERO_IMPORT_TAX_RATES: 'UpdateXeroImportTaxRates', @@ -1092,6 +1093,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.DEACTIVATE_TRAVEL_INVOICING]: Parameters.DeactivateTravelInvoicingParams; [WRITE_COMMANDS.SET_TRAVEL_INVOICING_SETTLEMENT_ACCOUNT]: Parameters.SetTravelInvoicingSettlementAccountParams; [WRITE_COMMANDS.UPDATE_TRAVEL_INVOICE_SETTLEMENT_FREQUENCY]: Parameters.UpdateTravelInvoicingSettlementFrequencyParams; + [WRITE_COMMANDS.UPDATE_TRAVEL_INVOICING_MONTHLY_LIMIT]: Parameters.UpdateTravelInvoicingMonthlyLimitParams; [WRITE_COMMANDS.PAY_TRAVEL_INVOICING_SPEND]: Parameters.PayTravelInvoicingSpendParams; [WRITE_COMMANDS.SET_PERSONAL_DETAILS_AND_SHIP_EXPENSIFY_CARDS]: Parameters.SetPersonalDetailsAndShipExpensifyCardsParams; [WRITE_COMMANDS.SELF_TOUR_VIEWED]: null; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0fdf0251f7bf8..af880e5a5cea8 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -823,6 +823,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/expensifyCard/WorkspaceSettlementFrequencyPage').default, [SCREENS.WORKSPACE.TRAVEL_SETTINGS_ACCOUNT]: () => require('../../../../pages/workspace/travel/WorkspaceTravelInvoicingSettlementAccountPage').default, [SCREENS.WORKSPACE.TRAVEL_SETTINGS_FREQUENCY]: () => require('../../../../pages/workspace/travel/WorkspaceTravelInvoicingSettlementFrequencyPage').default, + [SCREENS.WORKSPACE.TRAVEL_SETTINGS_MONTHLY_LIMIT]: () => require('../../../../pages/workspace/travel/WorkspaceTravelInvoicingMonthlyLimitPage').default, [SCREENS.WORKSPACE.TRAVEL_EXPORT]: () => require('../../../../pages/workspace/travel/WorkspaceTravelInvoicingExportPage').default, [SCREENS.WORKSPACE.TRAVEL_MISSING_PERSONAL_DETAILS]: () => require('../../../../pages/Travel/TravelLegalNamePage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SELECT_FEED]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardSelectorPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 97d78694d8818..33d1f9c807589 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -231,6 +231,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.TRAVEL_SETTINGS_FREQUENCY]: { path: ROUTES.WORKSPACE_TRAVEL_SETTINGS_FREQUENCY.route, }, + [SCREENS.WORKSPACE.TRAVEL_SETTINGS_MONTHLY_LIMIT]: { + path: ROUTES.WORKSPACE_TRAVEL_SETTINGS_MONTHLY_LIMIT.route, + }, [SCREENS.WORKSPACE.TRAVEL_EXPORT]: { path: ROUTES.WORKSPACE_TRAVEL_EXPORT.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 6053b255c2b57..8a7bb89625f1a 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1298,6 +1298,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.TRAVEL_SETTINGS_FREQUENCY]: { policyID: string; }; + [SCREENS.WORKSPACE.TRAVEL_SETTINGS_MONTHLY_LIMIT]: { + policyID: string; + }; [SCREENS.WORKSPACE.TRAVEL_EXPORT]: { policyID: string; }; diff --git a/src/libs/actions/TravelInvoicing.ts b/src/libs/actions/TravelInvoicing.ts index ac9eac9d992b8..2e452efbf94e2 100644 --- a/src/libs/actions/TravelInvoicing.ts +++ b/src/libs/actions/TravelInvoicing.ts @@ -9,6 +9,7 @@ import type { OpenPolicyTravelPageParams, PayTravelInvoicingSpendParams, SetTravelInvoicingSettlementAccountParams, + UpdateTravelInvoicingMonthlyLimitParams, UpdateTravelInvoicingSettlementFrequencyParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; @@ -488,6 +489,85 @@ function exportTravelInvoiceStatementCSV(policyID: string, startDate: string, en fileDownload(translate, commandURL, filename, '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); } +/** + * Updates the per-user monthly spend limit for Travel Invoicing cards. + */ +function updateTravelInvoicingMonthlyLimit(workspaceAccountID: number, monthlySpendLimitPerUser: number, currentMonthlySpendLimitPerUser?: number) { + const cardSettingsKey = getTravelInvoicingCardSettingsKey(workspaceAccountID); + + const optimisticData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: cardSettingsKey, + value: { + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + monthlySpendLimitPerUser, + }, + pendingFields: { + monthlySpendLimitPerUser: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + monthlySpendLimitPerUser: null, + }, + }, + }, + ]; + + const successData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: cardSettingsKey, + value: { + pendingFields: { + monthlySpendLimitPerUser: null, + }, + errorFields: { + monthlySpendLimitPerUser: null, + }, + }, + }, + ]; + + const failureData: Array> = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: cardSettingsKey, + value: { + [CONST.TRAVEL.PROGRAM_TRAVEL_US]: { + monthlySpendLimitPerUser: currentMonthlySpendLimitPerUser ?? null, + }, + pendingFields: { + monthlySpendLimitPerUser: null, + }, + errorFields: { + monthlySpendLimitPerUser: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + }, + }, + }, + ]; + + const params: UpdateTravelInvoicingMonthlyLimitParams = { + domainAccountID: workspaceAccountID, + monthlySpendLimitPerUser, + }; + + API.write(WRITE_COMMANDS.UPDATE_TRAVEL_INVOICING_MONTHLY_LIMIT, params, {optimisticData, successData, failureData}); +} + +/** + * Clears any errors from the Travel Invoicing monthly limit settings. + */ +function clearTravelInvoicingMonthlyLimitErrors(workspaceAccountID: number) { + Onyx.merge(getTravelInvoicingCardSettingsKey(workspaceAccountID), { + pendingFields: { + monthlySpendLimitPerUser: null, + }, + errorFields: { + monthlySpendLimitPerUser: null, + }, + }); +} + export { openPolicyTravelPage, setTravelInvoicingSettlementAccount, @@ -500,4 +580,6 @@ export { configureTravelInvoicingForPolicy, deactivateTravelInvoicing, clearTravelInvoicingErrors, + updateTravelInvoicingMonthlyLimit, + clearTravelInvoicingMonthlyLimitErrors, }; diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingMonthlyLimitPage.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingMonthlyLimitPage.tsx new file mode 100644 index 0000000000000..7a9026110ea2b --- /dev/null +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingMonthlyLimitPage.tsx @@ -0,0 +1,122 @@ +import React, {useState} from 'react'; +import {View} from 'react-native'; +import AmountForm from '@components/AmountForm'; +import ConfirmModal from '@components/ConfirmModal'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; +import {updateTravelInvoicingMonthlyLimit} from '@libs/actions/TravelInvoicing'; +import {getCardSettings} from '@libs/CardUtils'; +import {convertToBackendAmount, convertToFrontendAmountAsString} from '@libs/CurrencyUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getTravelInvoicingCardSettingsKey} from '@libs/TravelInvoicingUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/EditTravelInvoicingMonthlyLimitForm'; + +type WorkspaceTravelInvoicingMonthlyLimitPageProps = PlatformStackScreenProps; + +function WorkspaceTravelInvoicingMonthlyLimitPage({route}: WorkspaceTravelInvoicingMonthlyLimitPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const policyID = route.params?.policyID; + const workspaceAccountID = useWorkspaceAccountID(policyID); + const [cardSettings] = useOnyx(getTravelInvoicingCardSettingsKey(workspaceAccountID)); + const travelSettings = getCardSettings(cardSettings, CONST.TRAVEL.PROGRAM_TRAVEL_US); + const currentLimit = travelSettings?.monthlySpendLimitPerUser ?? 0; + const defaultValue = convertToFrontendAmountAsString(currentLimit, CONST.DEFAULT_CURRENCY_DECIMALS); + const {inputCallbackRef} = useAutoFocusInput(); + const [isConfirmModalVisible, setIsConfirmModalVisible] = useState(false); + const [pendingLimit, setPendingLimit] = useState(0); + + const submitLimit = (newLimitInCents: number) => { + updateTravelInvoicingMonthlyLimit(workspaceAccountID, newLimitInCents, currentLimit); + Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); + }; + + const validate = ({ + limit, + }: FormOnyxValues): FormInputErrors => { + const errors: FormInputErrors = {}; + const parsed = parseFloat(limit); + if (Number.isNaN(parsed) || parsed < 0) { + errors[INPUT_IDS.LIMIT] = translate('iou.error.invalidAmount'); + } + return errors; + }; + + const handleSubmit = ({limit}: FormOnyxValues) => { + const newLimitInCents = convertToBackendAmount(parseFloat(limit)); + if (newLimitInCents < currentLimit && currentLimit > 0) { + setPendingLimit(newLimitInCents); + setIsConfirmModalVisible(true); + return; + } + submitLimit(newLimitInCents); + }; + + return ( + + Navigation.goBack()} + /> + + + + + {translate('workspace.moreFeatures.travel.travelInvoicing.centralInvoicingSection.subsections.monthlySpendLimitDescription')} + + + + { + setIsConfirmModalVisible(false); + submitLimit(pendingLimit); + }} + onCancel={() => setIsConfirmModalVisible(false)} + danger + /> + + ); +} + +export default WorkspaceTravelInvoicingMonthlyLimitPage; diff --git a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx index b54d7951346dc..5865a3a0bb4a6 100644 --- a/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx +++ b/src/pages/workspace/travel/WorkspaceTravelInvoicingSection.tsx @@ -15,6 +15,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWorkspaceAccountID from '@hooks/useWorkspaceAccountID'; import { clearTravelInvoicingErrors, + clearTravelInvoicingMonthlyLimitErrors, clearTravelInvoicingSettlementAccountErrors, clearTravelInvoicingSettlementFrequencyErrors, configureTravelInvoicingForPolicy, @@ -128,6 +129,9 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec const hasSettlementAccountError = !!settlementAccountErrors; const hasSettlementFrequencyError = !!cardSettings?.errorFields?.[CONST.TRAVEL.MONTHLY_SETTLEMENT_DATE]; const settlementFrequencyErrors = hasSettlementFrequencyError ? cardSettings?.errorFields?.[CONST.TRAVEL.MONTHLY_SETTLEMENT_DATE] : null; + const hasMonthlyLimitError = !!cardSettings?.errorFields?.monthlySpendLimitPerUser; + const monthlyLimitErrors = hasMonthlyLimitError ? cardSettings?.errorFields?.monthlySpendLimitPerUser : null; + const formattedMonthlyLimit = convertToDisplayString(travelSettings?.monthlySpendLimitPerUser ?? 0, CONST.CURRENCY.USD); // Bank account eligibility for toggle handler const isSetupUnfinished = hasInProgressUSDVBBA(reimbursementAccount?.achData); @@ -344,6 +348,24 @@ function WorkspaceTravelInvoicingSection({policyID}: WorkspaceTravelInvoicingSec brickRoadIndicator={hasSettlementFrequencyError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> + clearTravelInvoicingMonthlyLimitErrors(workspaceAccountID)} + errorRowStyles={styles.mh2half} + errorRowTextStyles={styles.mr3} + > + Navigation.navigate(ROUTES.WORKSPACE_TRAVEL_SETTINGS_MONTHLY_LIMIT.getRoute(policyID))} + wrapperStyle={[styles.sectionMenuItemTopDescription]} + titleStyle={styles.textNormalThemeText} + descriptionTextStyle={styles.textLabelSupportingNormal} + shouldShowRightIcon + brickRoadIndicator={hasMonthlyLimitError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + ); diff --git a/src/types/form/EditTravelInvoicingMonthlyLimitForm.ts b/src/types/form/EditTravelInvoicingMonthlyLimitForm.ts new file mode 100644 index 0000000000000..d2d3a27482ff3 --- /dev/null +++ b/src/types/form/EditTravelInvoicingMonthlyLimitForm.ts @@ -0,0 +1,13 @@ +import type {ValueOf} from 'type-fest'; +import type Form from './Form'; + +const INPUT_IDS = { + LIMIT: 'limit', +} as const; + +type InputID = ValueOf; + +type EditTravelInvoicingMonthlyLimitForm = Form; + +export type {EditTravelInvoicingMonthlyLimitForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 72b9146ee875a..d8b039ea8f10a 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -78,6 +78,7 @@ export type {NetSuiteTokenInputForm} from './NetSuiteTokenInputForm'; export type {NetSuiteCustomFormIDForm} from './NetSuiteCustomFormIDForm'; export type {SearchAdvancedFiltersForm} from './SearchAdvancedFiltersForm'; export type {EditExpensifyCardLimitForm} from './EditExpensifyCardLimitForm'; +export type {EditTravelInvoicingMonthlyLimitForm} from './EditTravelInvoicingMonthlyLimitForm'; export type {default as TextPickerModalForm} from './TextPickerModalForm'; export type {default as Form} from './Form'; export type {ReportsDefaultTitleModalForm} from './ReportsDefaultTitleModalForm'; diff --git a/src/types/onyx/ExpensifyCardSettings.ts b/src/types/onyx/ExpensifyCardSettings.ts index f6d36eb6320cf..6a4fe7e81a435 100644 --- a/src/types/onyx/ExpensifyCardSettings.ts +++ b/src/types/onyx/ExpensifyCardSettings.ts @@ -62,6 +62,9 @@ type ExpensifyCardSettingsBase = { /** Credit limit for the card program */ limit?: number; + /** Per-user monthly spend limit for travel invoicing cards (in cents) */ + monthlySpendLimitPerUser?: number; + /** Currency for the card program (e.g. USD, GBP, EUR) */ currency?: string;