From 904ca23652fa51ef329deb02c2785f91001b6d85 Mon Sep 17 00:00:00 2001 From: Shubham Agrawal Date: Tue, 22 Oct 2024 18:44:41 +0900 Subject: [PATCH 01/22] Enable/disable per diem rates --- .../simple-illustration__perdiem.svg | 82 ++++ src/CONST.ts | 2 + src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + .../FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts | 1 + src/components/Icon/Illustrations.ts | 2 + src/languages/en.ts | 24 + src/languages/es.ts | 24 + .../parameters/EnablePolicyPerDiemParams.ts | 6 + .../OpenPolicyPerDiemRatesPageParams.ts | 5 + src/libs/API/parameters/index.ts | 2 + src/libs/API/types.ts | 4 + .../Navigators/FullScreenNavigator.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/libs/PolicyUtils.ts | 8 + src/libs/actions/Policy/PerDiem.ts | 104 +++++ src/pages/workspace/WorkspaceInitialPage.tsx | 13 +- .../workspace/WorkspaceMoreFeaturesPage.tsx | 14 + .../perDiem/WorkspacePerDiemPage.tsx | 441 ++++++++++++++++++ src/types/onyx/Policy.ts | 18 + 21 files changed, 761 insertions(+), 1 deletion(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__perdiem.svg create mode 100644 src/libs/API/parameters/EnablePolicyPerDiemParams.ts create mode 100644 src/libs/API/parameters/OpenPolicyPerDiemRatesPageParams.ts create mode 100644 src/libs/actions/Policy/PerDiem.ts create mode 100644 src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx diff --git a/assets/images/simple-illustrations/simple-illustration__perdiem.svg b/assets/images/simple-illustrations/simple-illustration__perdiem.svg new file mode 100644 index 000000000000..ea5a865a2694 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__perdiem.svg @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index 440f942e1244..9b82554cfc5b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2321,6 +2321,7 @@ const CONST = { ARE_INVOICES_ENABLED: 'areInvoicesEnabled', ARE_TAXES_ENABLED: 'tax', ARE_RULES_ENABLED: 'areRulesEnabled', + ARE_PER_DIEM_ENABLED: 'arePerDiemEnabled', }, DEFAULT_CATEGORIES: [ 'Advertising', @@ -2487,6 +2488,7 @@ const CONST = { CUSTOM_UNITS: { NAME_DISTANCE: 'Distance', + NAME_PER_DIEM_INTERNATIONAL: 'Per Diem International', DISTANCE_UNIT_MILES: 'mi', DISTANCE_UNIT_KILOMETERS: 'km', MILEAGE_IRS_RATE: 0.67, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cf15013fed9b..3de1c21c2ddc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1198,6 +1198,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/distance-rates/:rateID/tax-rate/edit', getRoute: (policyID: string, rateID: string) => `settings/workspaces/${policyID}/distance-rates/${rateID}/tax-rate/edit` as const, }, + WORKSPACE_PER_DIEM: { + route: 'settings/workspaces/:policyID/per-diem', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem` as const, + }, RULES_CUSTOM_NAME: { route: 'settings/workspaces/:policyID/rules/name', getRoute: (policyID: string) => `settings/workspaces/${policyID}/rules/name` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ff428edcd7eb..404634e8dfac 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -519,6 +519,7 @@ const SCREENS = { RULES_MAX_EXPENSE_AMOUNT: 'Rules_Max_Expense_Amount', RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age', RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default', + PER_DIEM: 'Per_Diem', }, EDIT_REQUEST: { diff --git a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts index 4673b4f269ec..32e063f03109 100644 --- a/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts +++ b/src/components/FocusTrap/WIDE_LAYOUT_INACTIVE_SCREENS.ts @@ -35,6 +35,7 @@ const WIDE_LAYOUT_INACTIVE_SCREENS: string[] = [ SCREENS.SETTINGS.TROUBLESHOOT, SCREENS.SETTINGS.SAVE_THE_WORLD, SCREENS.WORKSPACE.RULES, + SCREENS.WORKSPACE.PER_DIEM, ]; export default WIDE_LAYOUT_INACTIVE_SCREENS; diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 18ae1792686f..1469c6fc222c 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -129,6 +129,7 @@ import Workflows from '@assets/images/simple-illustrations/simple-illustration__ import ExpensifyApprovedLogoLight from '@assets/images/subscription-details__approvedlogo--light.svg'; import ExpensifyApprovedLogo from '@assets/images/subscription-details__approvedlogo.svg'; import TurtleInShell from '@assets/images/turtle-in-shell.svg'; +import PerDiem from '@assets/simple-illustrations/simple-illustration__per-diem.svg'; export { Abracadabra, @@ -262,4 +263,5 @@ export { OtherCompanyCardDetail, StripeCompanyCardDetail, WellsFargoCompanyCardDetail, + PerDiem, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 8b9569dc1267..d916d42c255d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2325,6 +2325,7 @@ const translations = { displayedAs: 'Displayed as', plan: 'Plan', profile: 'Profile', + perDiem: 'Per Diem', bankAccount: 'Bank account', connectBankAccount: 'Connect bank account', testTransactions: 'Test transactions', @@ -2389,6 +2390,25 @@ const translations = { } }, }, + perDiem: { + subtitle: 'Set per diem rates to control daily employee spend.', + destination: 'Destination', + subrate: 'Subrate', + amount: 'Amount', + deleteRates: () => ({ + one: 'Delete rate', + other: 'Delete rates', + }), + deletePerDiemRate: 'Delete per diem rate', + areYouSureDelete: () => ({ + one: 'Are you sure you want to delete this rate?', + other: 'Are you sure you want to delete these rates?', + }), + emptyList: { + title: 'Per diem', + subtitle: 'Set per diem rates to control daily employee spend. Import rates from a spreadsheet to get started.', + }, + }, qbd: { exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Desktop.', exportOutOfPocketExpensesCheckToogle: 'Mark checks as “print later”', @@ -3264,6 +3284,10 @@ const translations = { title: 'Distance rates', subtitle: 'Add, update, and enforce rates.', }, + perDiem: { + title: 'Per Diem', + subtitle: 'Set Per diem rates to control daily employee spend.', + }, expensifyCard: { title: 'Expensify Card', subtitle: 'Gain insights and control over spend.', diff --git a/src/languages/es.ts b/src/languages/es.ts index b7f66ef2bec0..275090c4c4d4 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2346,6 +2346,7 @@ const translations = { rules: 'Reglas', plan: 'Plan', profile: 'Perfil', + perDiem: 'Per Diem', bankAccount: 'Cuenta bancaria', displayedAs: 'Mostrado como', connectBankAccount: 'Conectar cuenta bancaria', @@ -2412,6 +2413,25 @@ const translations = { } }, }, + perDiem: { + subtitle: 'Set per diem rates to control daily employee spend.', + destination: 'Destination', + subrate: 'Subrate', + amount: 'Amount', + deleteRates: () => ({ + one: 'Delete rate', + other: 'Delete rates', + }), + deletePerDiemRate: 'Delete per diem rate', + areYouSureDelete: () => ({ + one: 'Are you sure you want to delete this rate?', + other: 'Are you sure you want to delete these rates?', + }), + emptyList: { + title: 'Per diem', + subtitle: 'Set per diem rates to control daily employee spend. Import rates from a spreadsheet to get started.', + }, + }, qbd: { exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Desktop.', exportOutOfPocketExpensesCheckToogle: 'Marcar los cheques como “imprimir más tarde”', @@ -3306,6 +3326,10 @@ const translations = { title: 'Tasas de distancia', subtitle: 'Añade, actualiza y haz cumplir las tasas.', }, + perDiem: { + title: 'Per Diem', + subtitle: 'Set Per diem rates to control daily employee spend.', + }, expensifyCard: { title: 'Tarjeta Expensify', subtitle: 'Obtén información y control sobre tus gastos.', diff --git a/src/libs/API/parameters/EnablePolicyPerDiemParams.ts b/src/libs/API/parameters/EnablePolicyPerDiemParams.ts new file mode 100644 index 000000000000..6c7d88ac231a --- /dev/null +++ b/src/libs/API/parameters/EnablePolicyPerDiemParams.ts @@ -0,0 +1,6 @@ +type EnablePolicyPerDiemParams = { + policyID: string; + enabled: boolean; +}; + +export default EnablePolicyPerDiemParams; diff --git a/src/libs/API/parameters/OpenPolicyPerDiemRatesPageParams.ts b/src/libs/API/parameters/OpenPolicyPerDiemRatesPageParams.ts new file mode 100644 index 000000000000..de2fa3467027 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyPerDiemRatesPageParams.ts @@ -0,0 +1,5 @@ +type OpenPolicyPerDiemRatesPageParams = { + policyID: string; +}; + +export default OpenPolicyPerDiemRatesPageParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 32a1e01ff3da..86d87e1a47f4 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -341,3 +341,5 @@ export type {default as SetInvoicingTransferBankAccountParams} from './SetInvoic export type {default as ConnectPolicyToQuickBooksDesktopParams} from './ConnectPolicyToQuickBooksDesktopParams'; export type {default as UpdateQuickbooksDesktopExpensesExportDestinationTypeParams} from './UpdateQuickbooksDesktopExpensesExportDestinationTypeParams'; export type {default as UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams} from './UpdateQuickbooksDesktopCompanyCardExpenseAccountTypeParams'; +export type {default as EnablePolicyPerDiemParams} from './EnablePolicyPerDiemParams'; +export type {default as OpenPolicyPerDiemRatesPageParams} from './OpenPolicyPerDiemRatesPageParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 929e709559b7..3127ad72f54d 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -218,6 +218,7 @@ const WRITE_COMMANDS = { ENABLE_POLICY_WORKFLOWS: 'EnablePolicyWorkflows', ENABLE_POLICY_REPORT_FIELDS: 'EnablePolicyReportFields', ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards', + ENABLE_POLICY_PER_DIEM: 'EnablePolicyPerDiem', ENABLE_POLICY_COMPANY_CARDS: 'EnablePolicyCompanyCards', ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing', SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled', @@ -650,6 +651,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS]: Parameters.EnablePolicyWorkflowsParams; [WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS]: Parameters.EnablePolicyReportFieldsParams; [WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams; + [WRITE_COMMANDS.ENABLE_POLICY_PER_DIEM]: Parameters.EnablePolicyPerDiemParams; [WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS]: Parameters.EnablePolicyCompanyCardsParams; [WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams; [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams; @@ -918,6 +920,7 @@ const READ_COMMANDS = { OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage', OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', + OPEN_POLICY_PER_DIEM_RATES_PAGE: 'OpenPolicyPerDiemRatesPage', OPEN_POLICY_MORE_FEATURES_PAGE: 'OpenPolicyMoreFeaturesPage', OPEN_POLICY_ACCOUNTING_PAGE: 'OpenPolicyAccountingPage', OPEN_POLICY_PROFILE_PAGE: 'OpenPolicyProfilePage', @@ -974,6 +977,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; [READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams; [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; + [READ_COMMANDS.OPEN_POLICY_PER_DIEM_RATES_PAGE]: Parameters.OpenPolicyPerDiemRatesPageParams; [READ_COMMANDS.OPEN_POLICY_MORE_FEATURES_PAGE]: Parameters.OpenPolicyMoreFeaturesPageParams; [READ_COMMANDS.OPEN_POLICY_ACCOUNTING_PAGE]: Parameters.OpenPolicyAccountingPageParams; [READ_COMMANDS.OPEN_POLICY_EXPENSIFY_CARDS_PAGE]: Parameters.OpenPolicyExpensifyCardsPageParams; diff --git a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx index 2f513fe804bb..4aac30587725 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/FullScreenNavigator.tsx @@ -29,6 +29,7 @@ const CENTRAL_PANE_WORKSPACE_SCREENS = { [SCREENS.WORKSPACE.REPORT_FIELDS]: () => require('../../../../pages/workspace/reportFields/WorkspaceReportFieldsPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceExpensifyCardPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardsPage').default, + [SCREENS.WORKSPACE.PER_DIEM]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemPage').default, [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default, [SCREENS.WORKSPACE.RULES]: () => require('../../../../pages/workspace/rules/PolicyRulesPage').default, } satisfies Screens; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 72e5f398c1d8..d3205e07c652 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -1327,6 +1327,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.COMPANY_CARDS]: { path: ROUTES.WORKSPACE_COMPANY_CARDS.route, }, + [SCREENS.WORKSPACE.PER_DIEM]: { + path: ROUTES.WORKSPACE_PER_DIEM.route, + }, [SCREENS.WORKSPACE.WORKFLOWS]: { path: ROUTES.WORKSPACE_WORKFLOWS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 0aa6e7474329..32052907e397 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1368,6 +1368,9 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS_ADD_NEW]: { policyID: string; }; + [SCREENS.WORKSPACE.PER_DIEM]: { + policyID: string; + }; [SCREENS.WORKSPACE.WORKFLOWS]: { policyID: string; }; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 36e5ccef3308..5f7bc79d07cc 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -136,6 +136,13 @@ function getDistanceRateCustomUnit(policy: OnyxEntry): CustomUnit | unde return Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); } +/** + * Retrieves the per diem custom unit object for the given policy + */ +function getPerDiemCustomUnit(policy: OnyxEntry): CustomUnit | undefined { + return Object.values(policy?.customUnits ?? {}).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); +} + /** * Retrieves custom unit rate object from the given customUnitRateID */ @@ -1149,6 +1156,7 @@ export { getSageIntacctCreditCards, getSageIntacctBankAccounts, getDistanceRateCustomUnit, + getPerDiemCustomUnit, getDistanceRateCustomUnitRate, sortWorkspacesBySelected, removePendingFieldsFromCustomUnit, diff --git a/src/libs/actions/Policy/PerDiem.ts b/src/libs/actions/Policy/PerDiem.ts new file mode 100644 index 000000000000..557fdb29078e --- /dev/null +++ b/src/libs/actions/Policy/PerDiem.ts @@ -0,0 +1,104 @@ +import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; +import * as API from '@libs/API'; +import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, Report} from '@src/types/onyx'; +import type {OnyxData} from '@src/types/onyx/Request'; + +const allPolicies: OnyxCollection = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + callback: (val, key) => { + if (!key) { + return; + } + if (val === null || val === undefined) { + // If we are deleting a policy, we have to check every report linked to that policy + // and unset the draft indicator (pencil icon) alongside removing any draft comments. Clearing these values will keep the newly archived chats from being displayed in the LHN. + // More info: https://github.com/Expensify/App/issues/14260 + const policyID = key.replace(ONYXKEYS.COLLECTION.POLICY, ''); + const policyReports = ReportUtils.getAllPolicyReports(policyID); + const cleanUpMergeQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT}${string}`, NullishDeep> = {}; + const cleanUpSetQueries: Record<`${typeof ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${string}` | `${typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${string}`, null> = {}; + policyReports.forEach((policyReport) => { + if (!policyReport) { + return; + } + const {reportID} = policyReport; + cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS_DRAFTS}${reportID}`] = null; + }); + Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); + Onyx.multiSet(cleanUpSetQueries); + delete allPolicies[key]; + return; + } + + allPolicies[key] = val; + }, +}); + +function enablePerDiem(policyID: string, enabled: boolean) { + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + arePerDiemEnabled: enabled, + pendingFields: { + arePerDiemEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: { + arePerDiemEnabled: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + arePerDiemEnabled: !enabled, + pendingFields: { + arePerDiemEnabled: null, + }, + }, + }, + ], + }; + + const parameters = {policyID, enabled}; + + API.write(WRITE_COMMANDS.ENABLE_POLICY_PER_DIEM, parameters, onyxData); + + if (enabled && getIsNarrowLayout()) { + navigateWhenEnableFeature(policyID); + } +} + +function openPolicyPerDiemPage(policyID?: string) { + if (!policyID) { + return; + } + + const params = {policyID}; + + API.read(READ_COMMANDS.OPEN_POLICY_PER_DIEM_RATES_PAGE, params); +} + +export {enablePerDiem, openPolicyPerDiemPage}; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 6cfc66466da4..0946cc67126a 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -62,7 +62,8 @@ type WorkspaceMenuItem = { | typeof SCREENS.WORKSPACE.EXPENSIFY_CARD | typeof SCREENS.WORKSPACE.COMPANY_CARDS | typeof SCREENS.WORKSPACE.REPORT_FIELDS - | typeof SCREENS.WORKSPACE.RULES; + | typeof SCREENS.WORKSPACE.RULES + | typeof SCREENS.WORKSPACE.PER_DIEM; badgeText?: string; }; @@ -111,6 +112,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac [CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED]: policy?.areReportFieldsEnabled, [CONST.POLICY.MORE_FEATURES.ARE_RULES_ENABLED]: policy?.areRulesEnabled, [CONST.POLICY.MORE_FEATURES.ARE_INVOICES_ENABLED]: policy?.areInvoicesEnabled, + [CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_ENABLED]: policy?.arePerDiemEnabled, }), [policy], ) as PolicyFeatureStates; @@ -221,6 +223,15 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, route}: Workspac }); } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_PER_DIEM_ENABLED]) { + protectedCollectPolicyMenuItems.push({ + translationKey: 'workspace.common.perDiem', + icon: Expensicons.CalendarSolid, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.PER_DIEM, + }); + } + if (featureStates?.[CONST.POLICY.MORE_FEATURES.ARE_WORKFLOWS_ENABLED]) { protectedCollectPolicyMenuItems.push({ translationKey: 'workspace.common.workflows', diff --git a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx index 0bc44a1d2298..c350a6e9ceb5 100644 --- a/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx +++ b/src/pages/workspace/WorkspaceMoreFeaturesPage.tsx @@ -21,6 +21,7 @@ import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import {isControlPolicy} from '@libs/PolicyUtils'; import * as Category from '@userActions/Policy/Category'; import * as DistanceRate from '@userActions/Policy/DistanceRate'; +import * as PerDiem from '@userActions/Policy/PerDiem'; import * as Policy from '@userActions/Policy/Policy'; import * as Tag from '@userActions/Policy/Tag'; import * as Report from '@userActions/Report'; @@ -117,6 +118,19 @@ function WorkspaceMoreFeaturesPage({policy, route}: WorkspaceMoreFeaturesPagePro setIsDisableExpensifyCardWarningModalOpen(true); }, }, + { + icon: Illustrations.PerDiem, + titleTranslationKey: 'workspace.moreFeatures.perDiem.title', + subtitleTranslationKey: 'workspace.moreFeatures.perDiem.subtitle', + isActive: policy?.arePerDiemEnabled ?? false, + pendingAction: policy?.pendingFields?.arePerDiemEnabled, + action: (isEnabled: boolean) => { + if (!policyID) { + return; + } + PerDiem.enablePerDiem(policyID, isEnabled); + }, + }, ]; if (canUseCompanyCardFeeds) { diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx new file mode 100644 index 000000000000..59f431142e25 --- /dev/null +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -0,0 +1,441 @@ +import {useFocusEffect, useIsFocused} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; +import lodashSortBy from 'lodash/sortBy'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {ActivityIndicator, View} from 'react-native'; +import Button from '@components/Button'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import ConfirmModal from '@components/ConfirmModal'; +import EmptyStateComponent from '@components/EmptyStateComponent'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import LottieAnimations from '@components/LottieAnimations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; +import TableListItem from '@components/SelectionList/TableListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionListWithModal from '@components/SelectionListWithModal'; +import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import useNetwork from '@hooks/useNetwork'; +import usePolicy from '@hooks/usePolicy'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import localeCompare from '@libs/LocaleCompare'; +import Navigation from '@libs/Navigation/Navigation'; +import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; +import {getPerDiemCustomUnit} from '@libs/PolicyUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Modal from '@userActions/Modal'; +// import {deleteWorkspaceCategories, setWorkspaceCategoryEnabled} from '@userActions/Policy/Category'; +import * as Category from '@userActions/Policy/Category'; +import * as PerDiem from '@userActions/Policy/PerDiem'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {Rate} from '@src/types/onyx/Policy'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; + +type PolicyOption = ListItem & { + /** subRateID is used as a key for identification of the entry */ + subRateID: string; + + /** rateID is used as a key for identification of the entry */ + rateID: string; +}; + +type SubRateData = { + pendingAction?: PendingAction; + destination: string; + subRateName: string; + rate: number; + currency: string; + rateID: string; + subRateID: string; +}; + +function getSubRatesData(customUnitRates: Rate[]) { + const subRatesData: SubRateData[] = []; + for (const rate of customUnitRates) { + const subRates = rate.subRates; + if (subRates) { + for (const subRate of subRates) { + subRatesData.push({ + pendingAction: rate.pendingAction, + destination: rate.name ?? '', + subRateName: subRate.name, + rate: subRate.rate, + currency: rate.currency ?? CONST.CURRENCY.USD, + rateID: rate.customUnitRateID ?? '', + subRateID: subRate.id, + }); + } + } + } + return subRatesData; +} + +function generateSingleSubRateData(customUnitRates: Rate[], rateID: string, subRateID: string) { + const selectedRate = customUnitRates.find((rate) => rate.customUnitRateID === rateID); + if (!selectedRate) { + return null; + } + const selectedSubRate = selectedRate.subRates?.find((subRate) => subRate.id === subRateID); + if (!selectedSubRate) { + return null; + } + return { + pendingAction: selectedRate.pendingAction, + destination: selectedRate.name ?? '', + subRateName: selectedSubRate.name, + rate: selectedSubRate.rate, + currency: selectedRate.currency ?? CONST.CURRENCY.USD, + rateID: selectedRate.customUnitRateID ?? '', + subRateID: selectedSubRate.id, + }; +} + +type WorkspacePerDiemPageProps = StackScreenProps; + +function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const {windowWidth} = useWindowDimensions(); + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); + const [selectedPerDiem, setSelectedPerDiem] = useState([]); + const [deletePerDiemConfirmModalVisible, setDeletePerDiemConfirmModalVisible] = useState(false); + const isFocused = useIsFocused(); + const policyID = route.params.policyID ?? '-1'; + const backTo = route.params?.backTo; + const policy = usePolicy(policyID); + const {selectionMode} = useMobileSelectionMode(); + + const customUnit = getPerDiemCustomUnit(policy); + const customUnitRates: Record = useMemo(() => customUnit?.rates ?? {}, [customUnit]); + + const allRatesArray = Object.values(customUnitRates); + + const allSubRates = getSubRatesData(allRatesArray); + + // Filter out rates that will be deleted + const allSelectableSubRates = useMemo(() => allSubRates.filter((subRate) => subRate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE), [allSubRates]); + + const canSelectMultiple = shouldUseNarrowLayout ? selectionMode?.isEnabled : true; + + const fetchPerDiem = useCallback(() => { + PerDiem.openPolicyPerDiemPage(policyID); + }, [policyID]); + + const {isOffline} = useNetwork({onReconnect: fetchPerDiem}); + + useFocusEffect( + useCallback(() => { + fetchPerDiem(); + }, [fetchPerDiem]), + ); + + useEffect(() => { + if (isFocused) { + return; + } + setSelectedPerDiem([]); + }, [isFocused]); + + const subRatesList = useMemo( + () => + (lodashSortBy(allSubRates, 'destination', localeCompare) as SubRateData[]).map((value) => { + const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + return { + text: value.destination, + subRateID: value.subRateID, + rateID: value.rateID, + keyForList: value.subRateID, + isSelected: selectedPerDiem.find((rate) => rate.subRateID === value.subRateID) !== undefined && canSelectMultiple, + isDisabled, + pendingAction: value.pendingAction, + // errors: value.errors ?? undefined, + rightElement: ( + <> + + {value.subRateName} + + + + {CurrencyUtils.convertAmountToDisplayString(value.rate, value.currency)} + + + + + ), + }; + }), + [allSubRates, selectedPerDiem, canSelectMultiple, styles.flex1, styles.alignItemsStart, styles.textSupporting, styles.label, styles.alignSelfEnd, styles.pl2], + ); + + const toggleSubRate = (subRate: PolicyOption) => { + if (selectedPerDiem.find((selectedSubRate) => selectedSubRate.subRateID === subRate.subRateID) !== undefined) { + setSelectedPerDiem((prev) => prev.filter((selectedSubRate) => selectedSubRate.subRateID !== subRate.subRateID)); + } else { + const subRateData = generateSingleSubRateData(allRatesArray, subRate.rateID, subRate.subRateID); + if (!subRateData) { + return; + } + setSelectedPerDiem((prev) => [...prev, subRateData]); + } + }; + + const toggleAllSubRates = () => { + if (selectedPerDiem.length === allSelectableSubRates.length) { + setSelectedPerDiem([]); + } else { + setSelectedPerDiem([...allSelectableSubRates]); + } + }; + + const getCustomListHeader = () => ( + + + {translate('workspace.perDiem.destination')} + + + {translate('workspace.perDiem.subrate')} + + + {translate('workspace.perDiem.amount')} + + + ); + + const openSettings = () => { + // Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_RATES_SETTINGS.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(policyID)); + }; + + const openSubRateDetails = (rate: PolicyOption) => { + // Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_RATE_DETAILS.getRoute(policyID, rate.rateID, rate.subRateID)); + Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATE_DETAILS.getRoute(policyID, rate.subRateID)); + }; + + const dismissError = (item: PolicyOption) => { + Category.clearCategoryErrors(policyID, item.subRateID); + }; + + const handleDeleteCategories = () => { + setSelectedPerDiem([]); + // deleteWorkspaceCategories(policyID, selectedPerDiem); + setDeletePerDiemConfirmModalVisible(false); + }; + + const getHeaderButtons = () => { + const options: Array>> = []; + + if (shouldUseNarrowLayout ? canSelectMultiple : selectedPerDiem.length > 0) { + options.push({ + icon: Expensicons.Trashcan, + text: translate('workspace.perDiem.deleteRates', {count: selectedPerDiem.length}), + value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, + onSelected: () => setDeletePerDiemConfirmModalVisible(true), + }); + + return ( + null} + shouldAlwaysShowDropdownMenu + pressOnEnter + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} + customText={translate('workspace.common.selected', {count: selectedPerDiem.length})} + options={options} + isSplitButton={false} + style={[shouldUseNarrowLayout && styles.flexGrow1, shouldUseNarrowLayout && styles.mb3]} + isDisabled={!selectedPerDiem.length} + /> + ); + } + + return ( + +