From d11310f05d2eda2cb3ed2530039e6bba1107a4ab Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 21 Aug 2024 16:35:14 +0200 Subject: [PATCH 01/31] Handle toggling require description in Category settings --- src/languages/en.ts | 5 ++ ...PolicyCategoryDescriptionRequiredParams.ts | 7 ++ src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + src/libs/actions/Policy/Category.ts | 72 ++++++++++++++++++- .../categories/CategorySettingsPage.tsx | 61 ++++++++++------ 6 files changed, 125 insertions(+), 23 deletions(-) create mode 100644 src/libs/API/parameters/SetPolicyCategoryDescriptionRequiredParams.ts diff --git a/src/languages/en.ts b/src/languages/en.ts index 0c1e6773ee14..11e090d42d93 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3591,6 +3591,11 @@ export default { title: 'Expense reports', subtitle: 'Automate expense report compliance, approvals, and payment.', }, + categoryRules: { + title: 'Category rules', + requireDescription: 'Require description', + descriptionHint: 'Description hint', + }, }, }, getAssistancePage: { diff --git a/src/libs/API/parameters/SetPolicyCategoryDescriptionRequiredParams.ts b/src/libs/API/parameters/SetPolicyCategoryDescriptionRequiredParams.ts new file mode 100644 index 000000000000..6a1748ff9ad1 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyCategoryDescriptionRequiredParams.ts @@ -0,0 +1,7 @@ +type SetPolicyCategoryDescriptionRequiredParams = { + policyID: string; + categoryName: string; + areCommentsRequired: boolean; +}; + +export default SetPolicyCategoryDescriptionRequiredParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 3ad422db5997..4ceb9cc1d845 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -277,3 +277,4 @@ export type {default as ConfigureExpensifyCardsForPolicyParams} from './Configur export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardParams'; export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams'; export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams'; +export type {default as SetPolicyCategoryDescriptionRequiredParams} from './SetPolicyCategoryDescriptionRequiredParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 9fdc79d7750e..d1e4ed289cea 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -200,6 +200,7 @@ const WRITE_COMMANDS = { ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards', ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing', SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled', + SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED: 'SetPolicyCategoryDescriptionRequired', SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax', SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax', SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName', @@ -529,6 +530,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams; [WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams; [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams; + [WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED]: Parameters.SetPolicyCategoryDescriptionRequiredParams; [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index a1828c92216d..0c6f09c43548 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -2,7 +2,13 @@ import lodashUnion from 'lodash/union'; import type {NullishDeep, OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; -import type {EnablePolicyCategoriesParams, OpenPolicyCategoriesPageParams, SetPolicyDistanceRatesDefaultCategoryParams, UpdatePolicyCategoryGLCodeParams} from '@libs/API/parameters'; +import type { + EnablePolicyCategoriesParams, + OpenPolicyCategoriesPageParams, + SetPolicyCategoryDescriptionRequiredParams, + SetPolicyDistanceRatesDefaultCategoryParams, + UpdatePolicyCategoryGLCodeParams, +} from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; @@ -255,6 +261,69 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor API.write(WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED, parameters, onyxData); } +function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: string, areCommentsRequired: boolean) { + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + areCommentsRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + areCommentsRequired, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: null, + pendingFields: { + areCommentsRequired: null, + }, + areCommentsRequired, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + pendingAction: null, + pendingFields: { + areCommentsRequired: null, + }, + }, + }, + }, + ], + }; + + const parameters: SetPolicyCategoryDescriptionRequiredParams = { + policyID, + categoryName, + areCommentsRequired, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED, parameters, onyxData); +} + function createPolicyCategory(policyID: string, categoryName: string) { const onyxData = buildOptimisticPolicyCategories(policyID, [categoryName]); @@ -753,6 +822,7 @@ export { openPolicyCategoriesPage, buildOptimisticPolicyRecentlyUsedCategories, setWorkspaceCategoryEnabled, + setPolicyCategoryDescriptionRequired, setWorkspaceRequiresCategory, setPolicyCategoryPayrollCode, createPolicyCategory, diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index af8b62a5a061..f138f4ff9c21 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -21,7 +21,6 @@ import {isControlPolicy} from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; -import {setWorkspaceCategoryEnabled} from '@userActions/Policy/Category'; import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -36,49 +35,55 @@ type CategorySettingsPageOnyxProps = { type CategorySettingsPageProps = CategorySettingsPageOnyxProps & StackScreenProps; -function CategorySettingsPage({route, policyCategories, navigation}: CategorySettingsPageProps) { +function CategorySettingsPage({ + route: { + params: {backTo, policyID, categoryName}, + }, + policyCategories, + navigation, +}: CategorySettingsPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [deleteCategoryConfirmModalVisible, setDeleteCategoryConfirmModalVisible] = useState(false); - const backTo = route.params?.backTo; - const policy = usePolicy(route.params.policyID); + const policy = usePolicy(policyID); + + const policyCategory = policyCategories?.[categoryName] ?? Object.values(policyCategories ?? {}).find((category) => category.previousCategoryName === categoryName); - const policyCategory = - policyCategories?.[route.params.categoryName] ?? Object.values(policyCategories ?? {}).find((category) => category.previousCategoryName === route.params.categoryName); + const areCommentsRequired = policyCategory?.areCommentsRequired ?? false; const navigateBack = () => { if (backTo) { - Navigation.goBack(ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(route.params.policyID, backTo)); + Navigation.goBack(ROUTES.SETTINGS_CATEGORIES_ROOT.getRoute(policyID, backTo)); return; } Navigation.goBack(); }; useEffect(() => { - if (policyCategory?.name === route.params.categoryName || !policyCategory) { + if (policyCategory?.name === categoryName || !policyCategory) { return; } navigation.setParams({categoryName: policyCategory?.name}); - }, [route.params.categoryName, navigation, policyCategory]); + }, [categoryName, navigation, policyCategory]); if (!policyCategory) { return ; } const updateWorkspaceRequiresCategory = (value: boolean) => { - setWorkspaceCategoryEnabled(route.params.policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}); + Category.setWorkspaceCategoryEnabled(policyID, {[policyCategory.name]: {name: policyCategory.name, enabled: value}}); }; const navigateToEditCategory = () => { if (backTo) { - Navigation.navigate(ROUTES.SETTINGS_CATEGORY_EDIT.getRoute(route.params.policyID, policyCategory.name, backTo)); + Navigation.navigate(ROUTES.SETTINGS_CATEGORY_EDIT.getRoute(policyID, policyCategory.name, backTo)); return; } - Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_EDIT.getRoute(route.params.policyID, policyCategory.name)); + Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_EDIT.getRoute(policyID, policyCategory.name)); }; const deleteCategory = () => { - Category.deleteWorkspaceCategories(route.params.policyID, [route.params.categoryName]); + Category.deleteWorkspaceCategories(policyID, [categoryName]); setDeleteCategoryConfirmModalVisible(false); navigateBack(); }; @@ -88,7 +93,7 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet return ( Category.clearCategoryErrors(route.params.policyID, route.params.categoryName)} + onClose={() => Category.clearCategoryErrors(policyID, categoryName)} > @@ -144,14 +149,14 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet if (!isControlPolicy(policy)) { Navigation.navigate( ROUTES.WORKSPACE_UPGRADE.getRoute( - route.params.policyID, + policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias, - ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(route.params.policyID, policyCategory.name), + ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(policyID, policyCategory.name), ), ); return; } - Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(route.params.policyID, policyCategory.name)); + Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(policyID, policyCategory.name)); }} shouldShowRightIcon /> @@ -164,18 +169,30 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet if (!isControlPolicy(policy)) { Navigation.navigate( ROUTES.WORKSPACE_UPGRADE.getRoute( - route.params.policyID, + policyID, CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias, - ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(route.params.policyID, policyCategory.name), + ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(policyID, policyCategory.name), ), ); return; } - Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(route.params.policyID, policyCategory.name)); + Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(policyID, policyCategory.name)); }} shouldShowRightIcon /> + + + {translate('workspace.rules.categoryRules.title')} + + + {translate('workspace.rules.categoryRules.requireDescription')} + Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired)} + /> + {!isThereAnyAccountingConnection && ( Date: Thu, 22 Aug 2024 12:09:12 +0200 Subject: [PATCH 02/31] Add templates of Category Rules pages --- src/ROUTES.ts | 16 ++++++ src/SCREENS.ts | 4 ++ src/languages/en.ts | 5 ++ .../ModalStackNavigators/index.tsx | 4 ++ .../FULL_SCREEN_TO_RHP_MAPPING.ts | 4 ++ src/libs/Navigation/linkingConfig/config.ts | 24 +++++++++ src/libs/Navigation/types.ts | 16 ++++++ .../categories/CategoryApproverPage.tsx | 53 +++++++++++++++++++ .../categories/CategoryDefaultTaxRatePage.tsx | 53 +++++++++++++++++++ .../CategoryDescriptionHintPage.tsx | 53 +++++++++++++++++++ .../CategoryFlagAmountsOverPage.tsx | 53 +++++++++++++++++++ .../categories/CategorySettingsPage.tsx | 46 +++++++++++++--- src/types/onyx/PolicyCategory.ts | 11 ++++ 13 files changed, 334 insertions(+), 8 deletions(-) create mode 100644 src/pages/workspace/categories/CategoryApproverPage.tsx create mode 100644 src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx create mode 100644 src/pages/workspace/categories/CategoryDescriptionHintPage.tsx create mode 100644 src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 282ac6f9f215..b05e74dd5cd5 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -756,6 +756,22 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/:categoryName/gl-code', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/gl-code` as const, }, + WORSKPACE_CATEGORY_DEFAULT_TAX_RATE: { + route: 'settings/workspaces/:policyID/categories/:categoryName/tax-rate', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/tax-rate` as const, + }, + WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER: { + route: 'settings/workspaces/:policyID/categories/:categoryName/flag-amounts', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/flag-amount` as const, + }, + WORSKPACE_CATEGORY_DESCRIPTION_HINT: { + route: 'settings/workspaces/:policyID/categories/:categoryName/description-hint', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/description-hint` as const, + }, + WORSKPACE_CATEGORY_APPROVER: { + route: 'settings/workspaces/:policyID/categories/:categoryName/approver', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/approver` as const, + }, WORKSPACE_MORE_FEATURES: { route: 'settings/workspaces/:policyID/more-features', getRoute: (policyID: string) => `settings/workspaces/${policyID}/more-features` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index dce7648ec671..28c012ffc136 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -426,6 +426,10 @@ const SCREENS = { CATEGORY_PAYROLL_CODE: 'Category_Payroll_Code', CATEGORY_GL_CODE: 'Category_GL_Code', CATEGORY_SETTINGS: 'Category_Settings', + CATEGORY_DEFAULT_TAX_RATE: 'Category_Default_Tax_Rate', + CATEGORY_FLAG_AMOUNTS_OVER: 'Category_Flag_Amounts_Over', + CATEGORY_DESCRIPTION_HINT: 'Category_Description_Hint', + CATEGORY_APPROVER: 'Category_Approver', CATEGORIES_SETTINGS: 'Categories_Settings', MORE_FEATURES: 'Workspace_More_Features', MEMBER_DETAILS: 'Workspace_Member_Details', diff --git a/src/languages/en.ts b/src/languages/en.ts index 11e090d42d93..aaab3aa20c28 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2826,6 +2826,11 @@ export default { updatePayrollCodeFailureMessage: 'An error occurred while updating the payroll code, please try again.', glCode: 'GL code', updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', + requireDescription: 'Require description', + defaultTaxRate: 'Default tax rate', + flagAmountsOver: 'Flag amounts over', + descriptionHint: 'Description hint', + approver: 'Approver', }, moreFeatures: { spendSection: { diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index f105de52a5ac..442f6ed0258f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -247,6 +247,10 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/categories/EditCategoryPage').default, [SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE]: () => require('../../../../pages/workspace/categories/CategoryPayrollCodePage').default, [SCREENS.WORKSPACE.CATEGORY_GL_CODE]: () => require('../../../../pages/workspace/categories/CategoryGLCodePage').default, + [SCREENS.WORKSPACE.CATEGORY_DEFAULT_TAX_RATE]: () => require('../../../../pages/workspace/categories/CategoryDefaultTaxRatePage').default, + [SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER]: () => require('../../../../pages/workspace/categories/CategoryFlagAmountsOverPage').default, + [SCREENS.WORKSPACE.CATEGORY_DESCRIPTION_HINT]: () => require('../../../../pages/workspace/categories/CategoryDescriptionHintPage').default, + [SCREENS.WORKSPACE.CATEGORY_APPROVER]: () => require('../../../../pages/workspace/categories/CategoryApproverPage').default, [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../../pages/workspace/distanceRates/CreateDistanceRatePage').default, [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default, [SCREENS.WORKSPACE.DISTANCE_RATE_DETAILS]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRateDetailsPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index 942a23068979..e90ba71f61eb 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -141,6 +141,10 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.CATEGORY_EDIT, SCREENS.WORKSPACE.CATEGORY_GL_CODE, SCREENS.WORKSPACE.CATEGORY_PAYROLL_CODE, + SCREENS.WORKSPACE.CATEGORY_DEFAULT_TAX_RATE, + SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER, + SCREENS.WORKSPACE.CATEGORY_DESCRIPTION_HINT, + SCREENS.WORKSPACE.CATEGORY_APPROVER, ], [SCREENS.WORKSPACE.DISTANCE_RATES]: [ SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 731e0d1462ff..addbdcec1240 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -589,6 +589,30 @@ const config: LinkingOptions['config'] = { categoryName: (categoryName: string) => decodeURIComponent(categoryName), }, }, + [SCREENS.WORKSPACE.CATEGORY_DEFAULT_TAX_RATE]: { + path: ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.route, + parse: { + categoryName: (categoryName: string) => decodeURIComponent(categoryName), + }, + }, + [SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER]: { + path: ROUTES.WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER.route, + parse: { + categoryName: (categoryName: string) => decodeURIComponent(categoryName), + }, + }, + [SCREENS.WORKSPACE.CATEGORY_DESCRIPTION_HINT]: { + path: ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.route, + parse: { + categoryName: (categoryName: string) => decodeURIComponent(categoryName), + }, + }, + [SCREENS.WORKSPACE.CATEGORY_APPROVER]: { + path: ROUTES.WORSKPACE_CATEGORY_APPROVER.route, + parse: { + categoryName: (categoryName: string) => decodeURIComponent(categoryName), + }, + }, [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { path: ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 91e6d5f631ca..b24109df34b5 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -220,6 +220,22 @@ type SettingsNavigatorParamList = { policyID: string; categoryName: string; }; + [SCREENS.WORKSPACE.CATEGORY_DEFAULT_TAX_RATE]: { + policyID: string; + categoryName: string; + }; + [SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER]: { + policyID: string; + categoryName: string; + }; + [SCREENS.WORKSPACE.CATEGORY_DESCRIPTION_HINT]: { + policyID: string; + categoryName: string; + }; + [SCREENS.WORKSPACE.CATEGORY_APPROVER]: { + policyID: string; + categoryName: string; + }; [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: { policyID: string; categoryName: string; diff --git a/src/pages/workspace/categories/CategoryApproverPage.tsx b/src/pages/workspace/categories/CategoryApproverPage.tsx new file mode 100644 index 000000000000..909015307b63 --- /dev/null +++ b/src/pages/workspace/categories/CategoryApproverPage.tsx @@ -0,0 +1,53 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type EditCategoryPageProps = StackScreenProps; + +function CategoryApproverPage({ + route: { + params: {policyID, categoryName}, + }, +}: EditCategoryPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + + const {inputCallbackRef} = useAutoFocusInput(); + + return ( + + + Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} + /> + + + ); +} + +CategoryApproverPage.displayName = 'CategoryApproverPage'; + +export default CategoryApproverPage; diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx new file mode 100644 index 000000000000..9e253b7d7748 --- /dev/null +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -0,0 +1,53 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type EditCategoryPageProps = StackScreenProps; + +function CategoryDefaultTaxRatePage({ + route: { + params: {policyID, categoryName}, + }, +}: EditCategoryPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + + const {inputCallbackRef} = useAutoFocusInput(); + + return ( + + + Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} + /> + + + ); +} + +CategoryDefaultTaxRatePage.displayName = 'CategoryDefaultTaxRatePage'; + +export default CategoryDefaultTaxRatePage; diff --git a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx new file mode 100644 index 000000000000..9a337e52559c --- /dev/null +++ b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx @@ -0,0 +1,53 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type EditCategoryPageProps = StackScreenProps; + +function CategoryDescriptionHintPage({ + route: { + params: {policyID, categoryName}, + }, +}: EditCategoryPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + + const {inputCallbackRef} = useAutoFocusInput(); + + return ( + + + Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} + /> + + + ); +} + +CategoryDescriptionHintPage.displayName = 'CategoryDescriptionHintPage'; + +export default CategoryDescriptionHintPage; diff --git a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx new file mode 100644 index 000000000000..240b997f39aa --- /dev/null +++ b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx @@ -0,0 +1,53 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type EditCategoryPageProps = StackScreenProps; + +function CategoryFlagAmountsOverPage({ + route: { + params: {policyID, categoryName}, + }, +}: EditCategoryPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + + const {inputCallbackRef} = useAutoFocusInput(); + + return ( + + + Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} + /> + + + ); +} + +CategoryFlagAmountsOverPage.displayName = 'CategoryFlagAmountsOverPage'; + +export default CategoryFlagAmountsOverPage; diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index f138f4ff9c21..73fc94ec0373 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -182,17 +182,47 @@ function CategorySettingsPage({ /> - + {translate('workspace.rules.categoryRules.title')} - - {translate('workspace.rules.categoryRules.requireDescription')} - Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired)} + + Category.clearCategoryErrors(policyID, categoryName)} + > + + + {translate('workspace.rules.categoryRules.requireDescription')} + Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired)} + /> + + + + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon /> - + + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + {!isThereAnyAccountingConnection && ( ; /** Record of policy categories, indexed by their name */ From 02d692478b2f8a8a441c5664589b0677ced38f7e Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 22 Aug 2024 16:10:36 +0200 Subject: [PATCH 03/31] Handle setting category description hint --- src/ONYXKEYS.ts | 2 + src/languages/en.ts | 3 + .../SetPolicyCategoryApproverParams.ts | 7 ++ .../SetPolicyCategoryMaxAmountParams.ts | 10 +++ .../parameters/SetPolicyCategoryTaxParams.ts | 7 ++ ...tWorkspaceCategoryDescriptionHintParams.ts | 7 ++ src/libs/API/parameters/index.ts | 4 ++ src/libs/API/types.ts | 2 + src/libs/actions/Policy/Category.ts | 66 +++++++++++++++++++ .../CategoryDescriptionHintPage.tsx | 32 +++++++++ .../categories/CategorySettingsPage.tsx | 22 ++++--- .../WorkspaceCategoryDescriptionHintForm.ts | 18 +++++ src/types/form/index.ts | 1 + src/types/onyx/PolicyCategory.ts | 6 +- 14 files changed, 175 insertions(+), 12 deletions(-) create mode 100644 src/libs/API/parameters/SetPolicyCategoryApproverParams.ts create mode 100644 src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts create mode 100644 src/libs/API/parameters/SetPolicyCategoryTaxParams.ts create mode 100644 src/libs/API/parameters/SetWorkspaceCategoryDescriptionHintParams.ts create mode 100644 src/types/form/WorkspaceCategoryDescriptionHintForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b7b6cf53a176..fa6a5c4c19c4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -482,6 +482,7 @@ const ONYXKEYS = { WORKSPACE_SETTINGS_FORM: 'workspaceSettingsForm', WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', + WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', WORKSPACE_TAG_FORM: 'workspaceTagForm', WORKSPACE_TAG_FORM_DRAFT: 'workspaceTagFormDraft', WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', @@ -636,6 +637,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_RATE_AND_UNIT_FORM]: FormTypes.WorkspaceRateAndUnitForm; [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldForm; + [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM]: FormTypes.WorkspaceCategoryDescriptionHintForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; diff --git a/src/languages/en.ts b/src/languages/en.ts index aaab3aa20c28..6218bdaded76 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3600,6 +3600,9 @@ export default { title: 'Category rules', requireDescription: 'Require description', descriptionHint: 'Description hint', + descriptionHintDescription: 'Remind employees to provide additional information for “Home Office” spend. This hint appears in the description field on expenses.', + descriptionHintLabel: 'Hint', + descriptionHintSubtitle: 'Pro-tip: The shorter the better!', }, }, }, diff --git a/src/libs/API/parameters/SetPolicyCategoryApproverParams.ts b/src/libs/API/parameters/SetPolicyCategoryApproverParams.ts new file mode 100644 index 000000000000..197fdaf59df6 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyCategoryApproverParams.ts @@ -0,0 +1,7 @@ +type SetPolicyCategoryApproverParams = { + policyID: string; + categoryName: string; + approver: string; +}; + +export default SetPolicyCategoryApproverParams; diff --git a/src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts b/src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts new file mode 100644 index 000000000000..c887b8b831e0 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts @@ -0,0 +1,10 @@ +import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; + +type SetPolicyCategoryMaxAmountParams = { + policyID: string; + categoryName: string; + maxExpenseAmount: number; + expenseLimitType: PolicyCategoryExpenseLimitType; +}; + +export default SetPolicyCategoryMaxAmountParams; diff --git a/src/libs/API/parameters/SetPolicyCategoryTaxParams.ts b/src/libs/API/parameters/SetPolicyCategoryTaxParams.ts new file mode 100644 index 000000000000..94a0a6025916 --- /dev/null +++ b/src/libs/API/parameters/SetPolicyCategoryTaxParams.ts @@ -0,0 +1,7 @@ +type SetPolicyCategoryTaxParams = { + policyID: string; + categoryName: string; + taxID: string; +}; + +export default SetPolicyCategoryTaxParams; diff --git a/src/libs/API/parameters/SetWorkspaceCategoryDescriptionHintParams.ts b/src/libs/API/parameters/SetWorkspaceCategoryDescriptionHintParams.ts new file mode 100644 index 000000000000..d1c3b36975cb --- /dev/null +++ b/src/libs/API/parameters/SetWorkspaceCategoryDescriptionHintParams.ts @@ -0,0 +1,7 @@ +type SetWorkspaceCategoryDescriptionHintParams = { + policyID: string; + categoryName: string; + commentHint: string; +}; + +export default SetWorkspaceCategoryDescriptionHintParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 4ceb9cc1d845..31b84e871768 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -278,3 +278,7 @@ export type {default as CreateExpensifyCardParams} from './CreateExpensifyCardPa export type {default as UpdateExpensifyCardTitleParams} from './UpdateExpensifyCardTitleParams'; export type {default as OpenCardDetailsPageParams} from './OpenCardDetailsPageParams'; export type {default as SetPolicyCategoryDescriptionRequiredParams} from './SetPolicyCategoryDescriptionRequiredParams'; +export type {default as SetPolicyCategoryApproverParams} from './SetPolicyCategoryApproverParams'; +export type {default as SetWorkspaceCategoryDescriptionHintParams} from './SetWorkspaceCategoryDescriptionHintParams'; +export type {default as SetPolicyCategoryTaxParams} from './SetPolicyCategoryTaxParams'; +export type {default as SetPolicyCategoryMaxAmountParams} from './SetPolicyCategoryMaxAmountParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index d1e4ed289cea..b5fc4f21ca83 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -201,6 +201,7 @@ const WRITE_COMMANDS = { ENABLE_POLICY_INVOICING: 'EnablePolicyInvoicing', SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled', SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED: 'SetPolicyCategoryDescriptionRequired', + SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT: 'SetWorkspaceCategoryDescriptionHint', SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax', SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax', SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName', @@ -531,6 +532,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ENABLE_POLICY_INVOICING]: Parameters.EnablePolicyInvoicingParams; [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams; [WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED]: Parameters.SetPolicyCategoryDescriptionRequiredParams; + [WRITE_COMMANDS.SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT]: Parameters.SetWorkspaceCategoryDescriptionHintParams; [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 0c6f09c43548..9e1dfa63fd00 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -7,6 +7,7 @@ import type { OpenPolicyCategoriesPageParams, SetPolicyCategoryDescriptionRequiredParams, SetPolicyDistanceRatesDefaultCategoryParams, + SetWorkspaceCategoryDescriptionHintParams, UpdatePolicyCategoryGLCodeParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; @@ -818,11 +819,76 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); } +function SetWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) { + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + commentHint: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + commentHint, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: null, + pendingFields: { + commentHint: null, + }, + + commentHint, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + pendingAction: null, + pendingFields: { + commentHint: null, + }, + }, + }, + }, + ], + }; + + const parameters: SetWorkspaceCategoryDescriptionHintParams = { + policyID, + categoryName, + commentHint, + }; + + API.write(WRITE_COMMANDS.SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT, parameters, onyxData); +} + export { openPolicyCategoriesPage, buildOptimisticPolicyRecentlyUsedCategories, setWorkspaceCategoryEnabled, setPolicyCategoryDescriptionRequired, + SetWorkspaceCategoryDescriptionHint, setWorkspaceRequiresCategory, setPolicyCategoryPayrollCode, createPolicyCategory, diff --git a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx index 9a337e52559c..676a017d2c20 100644 --- a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx +++ b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx @@ -1,18 +1,25 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; +import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceCategoryDescriptionHintForm'; type EditCategoryPageProps = StackScreenProps; @@ -27,6 +34,8 @@ function CategoryDescriptionHintPage({ const {inputCallbackRef} = useAutoFocusInput(); + const commentHintDefaultValue = policyCategories?.[categoryName]?.commentHint; + return ( Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> + { + Category.SetWorkspaceCategoryDescriptionHint(policyID, categoryName, commentHint); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); + }} + submitButtonText={translate('common.save')} + enabledWhenOffline + > + + {translate('workspace.rules.categoryRules.descriptionHintDescription')} + + {translate('workspace.rules.categoryRules.descriptionHintSubtitle')} + + ); diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 73fc94ec0373..d03eddf4ef39 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -203,6 +203,18 @@ function CategorySettingsPage({ + {policyCategory?.areCommentsRequired && ( + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + )} - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - {!isThereAnyAccountingConnection && ( ; + +type WorkspaceCategoryDescriptionHintForm = Form< + InputID, + { + [INPUT_IDS.COMMENT_HINT]: string; + } +>; + +export type {WorkspaceCategoryDescriptionHintForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index 61d3e9918164..fd2523b7588a 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -68,5 +68,6 @@ export type {NetSuiteTokenInputForm} from './NetSuiteTokenInputForm'; export type {NetSuiteCustomFormIDForm} from './NetSuiteCustomFormIDForm'; export type {SearchAdvancedFiltersForm} from './SearchAdvancedFiltersForm'; export type {EditExpensifyCardLimitForm} from './EditExpensifyCardLimitForm'; +export type {WorkspaceCategoryDescriptionHintForm} from './WorkspaceCategoryDescriptionHintForm'; export type {default as TextPickerModalForm} from './TextPickerModalForm'; export type {default as Form} from './Form'; diff --git a/src/types/onyx/PolicyCategory.ts b/src/types/onyx/PolicyCategory.ts index fc22d395e37f..5dfcc5ca345a 100644 --- a/src/types/onyx/PolicyCategory.ts +++ b/src/types/onyx/PolicyCategory.ts @@ -1,5 +1,7 @@ import type * as OnyxCommon from './OnyxCommon'; +type PolicyCategoryExpenseLimitType = 'expense' | 'daily'; + /** Model of policy category */ type PolicyCategory = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Name of a category */ @@ -41,7 +43,7 @@ type PolicyCategory = OnyxCommon.OnyxValueWithOfflineFeedback<{ maxExpenseAmount?: number; - expenseLimitType?: 'expense' | 'daily'; + expenseLimitType?: PolicyCategoryExpenseLimitType; maxExpenseAmountNoReceipt?: number | null; }>; @@ -49,4 +51,4 @@ type PolicyCategory = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** Record of policy categories, indexed by their name */ type PolicyCategories = Record; -export type {PolicyCategory, PolicyCategories}; +export type {PolicyCategory, PolicyCategories, PolicyCategoryExpenseLimitType}; From 547ecdda76b19ec7ea4bc9663e485f6c393aef11 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 22 Aug 2024 17:01:42 +0200 Subject: [PATCH 04/31] Add template of CategoryRequireReceiptsOver page --- src/ONYXKEYS.ts | 2 + src/ROUTES.ts | 6 ++- src/SCREENS.ts | 1 + src/languages/en.ts | 4 ++ .../ModalStackNavigators/index.tsx | 1 + .../FULL_SCREEN_TO_RHP_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 6 +++ src/libs/Navigation/types.ts | 4 ++ .../categories/CategoryApproverPage.tsx | 2 +- .../CategoryDescriptionHintPage.tsx | 2 +- .../CategoryFlagAmountsOverPage.tsx | 2 +- .../CategoryRequireReceiptsOverPage.tsx | 53 +++++++++++++++++++ .../categories/CategorySettingsPage.tsx | 20 +++++++ .../WorkspaceCategoryFlagAmountsOverForm.ts | 20 +++++++ src/types/form/index.ts | 1 + 15 files changed, 121 insertions(+), 4 deletions(-) create mode 100644 src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx create mode 100644 src/types/form/WorkspaceCategoryFlagAmountsOverForm.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index fa6a5c4c19c4..b223441d0e96 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -483,6 +483,7 @@ const ONYXKEYS = { WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', + WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM: 'workspaceCategoryFlagAmountsOverForm', WORKSPACE_TAG_FORM: 'workspaceTagForm', WORKSPACE_TAG_FORM_DRAFT: 'workspaceTagFormDraft', WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', @@ -638,6 +639,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.WORKSPACE_TAX_CUSTOM_NAME]: FormTypes.WorkspaceTaxCustomName; [ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM]: FormTypes.WorkspaceReportFieldForm; [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM]: FormTypes.WorkspaceCategoryDescriptionHintForm; + [ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM]: FormTypes.WorkspaceCategoryFlagAmountsOverForm; [ONYXKEYS.FORMS.CLOSE_ACCOUNT_FORM]: FormTypes.CloseAccountForm; [ONYXKEYS.FORMS.PROFILE_SETTINGS_FORM]: FormTypes.ProfileSettingsForm; [ONYXKEYS.FORMS.DISPLAY_NAME_FORM]: FormTypes.DisplayNameForm; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b05e74dd5cd5..8a3c2a88572a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -762,12 +762,16 @@ const ROUTES = { }, WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER: { route: 'settings/workspaces/:policyID/categories/:categoryName/flag-amounts', - getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/flag-amount` as const, + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/flag-amounts` as const, }, WORSKPACE_CATEGORY_DESCRIPTION_HINT: { route: 'settings/workspaces/:policyID/categories/:categoryName/description-hint', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/description-hint` as const, }, + WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER: { + route: 'settings/workspaces/:policyID/categories/:categoryName/require-receipts-over', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/require-receipts-over` as const, + }, WORSKPACE_CATEGORY_APPROVER: { route: 'settings/workspaces/:policyID/categories/:categoryName/approver', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/approver` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 28c012ffc136..f466a1c37bd5 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -430,6 +430,7 @@ const SCREENS = { CATEGORY_FLAG_AMOUNTS_OVER: 'Category_Flag_Amounts_Over', CATEGORY_DESCRIPTION_HINT: 'Category_Description_Hint', CATEGORY_APPROVER: 'Category_Approver', + CATEGORY_REQUIRE_RECEIPTS_OVER: 'Category_Require_Receipts_Over', CATEGORIES_SETTINGS: 'Categories_Settings', MORE_FEATURES: 'Workspace_More_Features', MEMBER_DETAILS: 'Workspace_Member_Details', diff --git a/src/languages/en.ts b/src/languages/en.ts index 6218bdaded76..a5c49b8e5e3a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3603,6 +3603,10 @@ export default { descriptionHintDescription: 'Remind employees to provide additional information for “Home Office” spend. This hint appears in the description field on expenses.', descriptionHintLabel: 'Hint', descriptionHintSubtitle: 'Pro-tip: The shorter the better!', + maxAmount: 'Max amount', + flagAmountsOver: 'Flag amounts over', + flagAmountsOverDescription: (categoryName) => `Applies to the category “${categoryName}”.`, + requireReceiptsOver: 'Require receipts over', }, }, }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 442f6ed0258f..fcbf4410c7d8 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -250,6 +250,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/categories/CategoryDefaultTaxRatePage').default, [SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER]: () => require('../../../../pages/workspace/categories/CategoryFlagAmountsOverPage').default, [SCREENS.WORKSPACE.CATEGORY_DESCRIPTION_HINT]: () => require('../../../../pages/workspace/categories/CategoryDescriptionHintPage').default, + [SCREENS.WORKSPACE.CATEGORY_REQUIRE_RECEIPTS_OVER]: () => require('../../../../pages/workspace/categories/CategoryRequireReceiptsOverPage').default, [SCREENS.WORKSPACE.CATEGORY_APPROVER]: () => require('../../../../pages/workspace/categories/CategoryApproverPage').default, [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: () => require('../../../../pages/workspace/distanceRates/CreateDistanceRatePage').default, [SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRatesSettingsPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index e90ba71f61eb..ff00754c0e41 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -145,6 +145,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER, SCREENS.WORKSPACE.CATEGORY_DESCRIPTION_HINT, SCREENS.WORKSPACE.CATEGORY_APPROVER, + SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER, ], [SCREENS.WORKSPACE.DISTANCE_RATES]: [ SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index addbdcec1240..bdac69e74ddd 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -613,6 +613,12 @@ const config: LinkingOptions['config'] = { categoryName: (categoryName: string) => decodeURIComponent(categoryName), }, }, + [SCREENS.WORKSPACE.CATEGORY_REQUIRE_RECEIPTS_OVER]: { + path: ROUTES.WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.route, + parse: { + categoryName: (categoryName: string) => decodeURIComponent(categoryName), + }, + }, [SCREENS.WORKSPACE.CREATE_DISTANCE_RATE]: { path: ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index b24109df34b5..4c3a6d3426fc 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -236,6 +236,10 @@ type SettingsNavigatorParamList = { policyID: string; categoryName: string; }; + [SCREENS.WORKSPACE.CATEGORY_REQUIRE_RECEIPTS_OVER]: { + policyID: string; + categoryName: string; + }; [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: { policyID: string; categoryName: string; diff --git a/src/pages/workspace/categories/CategoryApproverPage.tsx b/src/pages/workspace/categories/CategoryApproverPage.tsx index 909015307b63..8415742ca8c2 100644 --- a/src/pages/workspace/categories/CategoryApproverPage.tsx +++ b/src/pages/workspace/categories/CategoryApproverPage.tsx @@ -14,7 +14,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type EditCategoryPageProps = StackScreenProps; +type EditCategoryPageProps = StackScreenProps; function CategoryApproverPage({ route: { diff --git a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx index 676a017d2c20..884b0c4ecd7c 100644 --- a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx +++ b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx @@ -21,7 +21,7 @@ import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceCategoryDescriptionHintForm'; -type EditCategoryPageProps = StackScreenProps; +type EditCategoryPageProps = StackScreenProps; function CategoryDescriptionHintPage({ route: { diff --git a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx index 240b997f39aa..e90d2d1ba195 100644 --- a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx @@ -14,7 +14,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -type EditCategoryPageProps = StackScreenProps; +type EditCategoryPageProps = StackScreenProps; function CategoryFlagAmountsOverPage({ route: { diff --git a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx new file mode 100644 index 000000000000..c59c85d05280 --- /dev/null +++ b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx @@ -0,0 +1,53 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import {useOnyx} from 'react-native-onyx'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type EditCategoryPageProps = StackScreenProps; + +function CategoryRequireReceiptsOverPage({ + route: { + params: {policyID, categoryName}, + }, +}: EditCategoryPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + + const {inputCallbackRef} = useAutoFocusInput(); + + return ( + + + Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} + /> + + + ); +} + +CategoryRequireReceiptsOverPage.displayName = 'CategoryRequireReceiptsOverPage'; + +export default CategoryRequireReceiptsOverPage; diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index d03eddf4ef39..21da1ab31152 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -225,6 +225,26 @@ function CategorySettingsPage({ shouldShowRightIcon /> + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + {!isThereAnyAccountingConnection && ( ; + +type WorkspaceCategoryFlagAmountsOverForm = Form< + InputID, + { + [INPUT_IDS.MAX_EXPENSE_AMOUNT]: string; + [INPUT_IDS.EXPENSE_LIMIT_TYPE]: string; + } +>; + +export type {WorkspaceCategoryFlagAmountsOverForm}; +export default INPUT_IDS; diff --git a/src/types/form/index.ts b/src/types/form/index.ts index fd2523b7588a..85be35eecfc6 100644 --- a/src/types/form/index.ts +++ b/src/types/form/index.ts @@ -69,5 +69,6 @@ export type {NetSuiteCustomFormIDForm} from './NetSuiteCustomFormIDForm'; export type {SearchAdvancedFiltersForm} from './SearchAdvancedFiltersForm'; export type {EditExpensifyCardLimitForm} from './EditExpensifyCardLimitForm'; export type {WorkspaceCategoryDescriptionHintForm} from './WorkspaceCategoryDescriptionHintForm'; +export type {WorkspaceCategoryFlagAmountsOverForm} from './WorkspaceCategoryFlagAmountsOverForm'; export type {default as TextPickerModalForm} from './TextPickerModalForm'; export type {default as Form} from './Form'; From d74d1ed1be86753d398f75fd01f413b0c998310e Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 26 Aug 2024 10:06:19 +0200 Subject: [PATCH 05/31] Handle setting require receipts over category rule --- src/CONST.ts | 5 + src/languages/en.ts | 8 +- ...ovePolicyCategoryReceiptsRequiredParams.ts | 6 + ...SetPolicyCategoryReceiptsRequiredParams.ts | 7 + src/libs/API/parameters/index.ts | 2 + src/libs/API/types.ts | 4 + src/libs/actions/Policy/Category.ts | 129 ++++++++++++++++++ .../CategoryDescriptionHintPage.tsx | 2 +- .../CategoryRequireReceiptsOverPage.tsx | 63 ++++++++- 9 files changed, 222 insertions(+), 4 deletions(-) create mode 100644 src/libs/API/parameters/RemovePolicyCategoryReceiptsRequiredParams.ts create mode 100644 src/libs/API/parameters/SetPolicyCategoryReceiptsRequiredParams.ts diff --git a/src/CONST.ts b/src/CONST.ts index 971fefc2d1b7..8e0d8cf407c6 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2222,6 +2222,11 @@ const CONST = { DEFAULT_MAX_EXPENSE_AGE: 90, DEFAULT_MAX_EXPENSE_AMOUNT: 200000, DEFAULT_MAX_AMOUNT_NO_RECEIPT: 2500, + REQUIRE_RECEIPTS_OVER_OPTIONS: { + DEFAULT: 'default', + NEVER: 'never', + ALWAYS: 'always', + }, }, CUSTOM_UNITS: { diff --git a/src/languages/en.ts b/src/languages/en.ts index eaadf4ea142d..3cd43f784cf9 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3627,13 +3627,19 @@ export default { title: 'Category rules', requireDescription: 'Require description', descriptionHint: 'Description hint', - descriptionHintDescription: 'Remind employees to provide additional information for “Home Office” spend. This hint appears in the description field on expenses.', + descriptionHintDescription: (categoryName: string) => + `Remind employees to provide additional information for “${categoryName}” spend. This hint appears in the description field on expenses.`, descriptionHintLabel: 'Hint', descriptionHintSubtitle: 'Pro-tip: The shorter the better!', maxAmount: 'Max amount', flagAmountsOver: 'Flag amounts over', flagAmountsOverDescription: (categoryName) => `Applies to the category “${categoryName}”.`, requireReceiptsOver: 'Require receipts over', + requireReceiptsOverList: { + default: '$75 Default', + never: 'Never require receipts', + always: 'Always require receipts', + }, }, }, }, diff --git a/src/libs/API/parameters/RemovePolicyCategoryReceiptsRequiredParams.ts b/src/libs/API/parameters/RemovePolicyCategoryReceiptsRequiredParams.ts new file mode 100644 index 000000000000..83e62db59811 --- /dev/null +++ b/src/libs/API/parameters/RemovePolicyCategoryReceiptsRequiredParams.ts @@ -0,0 +1,6 @@ +type RemovePolicyCategoryReceiptsRequiredParams = { + policyID: string; + categoryName: string; +}; + +export default RemovePolicyCategoryReceiptsRequiredParams; diff --git a/src/libs/API/parameters/SetPolicyCategoryReceiptsRequiredParams.ts b/src/libs/API/parameters/SetPolicyCategoryReceiptsRequiredParams.ts new file mode 100644 index 000000000000..fe7c15bd8eff --- /dev/null +++ b/src/libs/API/parameters/SetPolicyCategoryReceiptsRequiredParams.ts @@ -0,0 +1,7 @@ +type SetPolicyCategoryReceiptsRequiredParams = { + policyID: string; + categoryName: string; + maxExpenseAmountNoReceipt: number; +}; + +export default SetPolicyCategoryReceiptsRequiredParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 9f8e4388dc68..62fa3d8e7bf3 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -285,3 +285,5 @@ export type {default as SetPolicyCategoryMaxAmountParams} from './SetPolicyCateg export type {default as EnablePolicyCompanyCardsParams} from './EnablePolicyCompanyCardsParams'; export type {default as ToggleCardContinuousReconciliationParams} from './ToggleCardContinuousReconciliationParams'; export type {default as UpdateExpensifyCardLimitTypeParams} from './UpdateExpensifyCardLimitTypeParams'; +export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPolicyCategoryReceiptsRequiredParams'; +export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8b8052572b26..4ae2e5fc0c7b 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -204,6 +204,8 @@ const WRITE_COMMANDS = { SET_POLICY_RULES_ENABLED: 'SetPolicyRulesEnabled', SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED: 'SetPolicyCategoryDescriptionRequired', SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT: 'SetWorkspaceCategoryDescriptionHint', + SET_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'SetPolicyCategoryReceiptsRequired', + REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'RemoveWorkspaceCategoryReceiptsRequired', SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax', SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax', SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName', @@ -538,6 +540,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_RULES_ENABLED]: Parameters.SetPolicyRulesEnabledParams; [WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED]: Parameters.SetPolicyCategoryDescriptionRequiredParams; [WRITE_COMMANDS.SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT]: Parameters.SetWorkspaceCategoryDescriptionHintParams; + [WRITE_COMMANDS.SET_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.SetPolicyCategoryReceiptsRequiredParams; + [WRITE_COMMANDS.REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.RemovePolicyCategoryReceiptsRequiredParams; [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 9e1dfa63fd00..5f18b37ffabf 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -5,7 +5,9 @@ import * as API from '@libs/API'; import type { EnablePolicyCategoriesParams, OpenPolicyCategoriesPageParams, + RemovePolicyCategoryReceiptsRequiredParams, SetPolicyCategoryDescriptionRequiredParams, + SetPolicyCategoryReceiptsRequiredParams, SetPolicyDistanceRatesDefaultCategoryParams, SetWorkspaceCategoryDescriptionHintParams, UpdatePolicyCategoryGLCodeParams, @@ -325,6 +327,131 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_DESCRIPTION_REQUIRED, parameters, onyxData); } +function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: string, maxExpenseAmountNoReceipt: number) { + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + maxExpenseAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + maxExpenseAmountNoReceipt, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: null, + pendingFields: { + maxExpenseAmountNoReceipt: null, + }, + maxExpenseAmountNoReceipt, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + pendingAction: null, + pendingFields: { + maxExpenseAmountNoReceipt: null, + }, + }, + }, + }, + ], + }; + + const parameters: SetPolicyCategoryReceiptsRequiredParams = { + policyID, + categoryName, + maxExpenseAmountNoReceipt, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_RECEIPTS_REQUIRED, parameters, onyxData); +} + +function removePolicyCategoryReceiptsRequired(policyID: string, categoryName: string) { + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + maxExpenseAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + maxExpenseAmountNoReceipt: null, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: null, + pendingFields: { + maxExpenseAmountNoReceipt: null, + }, + maxExpenseAmountNoReceipt: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + pendingAction: null, + pendingFields: { + maxExpenseAmountNoReceipt: null, + }, + }, + }, + }, + ], + }; + + const parameters: RemovePolicyCategoryReceiptsRequiredParams = { + policyID, + categoryName, + }; + + API.write(WRITE_COMMANDS.REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED, parameters, onyxData); +} + function createPolicyCategory(policyID: string, categoryName: string) { const onyxData = buildOptimisticPolicyCategories(policyID, [categoryName]); @@ -899,4 +1026,6 @@ export { setPolicyDistanceRatesDefaultCategory, deleteWorkspaceCategories, buildOptimisticPolicyCategories, + setPolicyCategoryReceiptsRequired, + removePolicyCategoryReceiptsRequired, }; diff --git a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx index 884b0c4ecd7c..034e3def4f47 100644 --- a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx +++ b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx @@ -63,7 +63,7 @@ function CategoryDescriptionHintPage({ enabledWhenOffline > - {translate('workspace.rules.categoryRules.descriptionHintDescription')} + {translate('workspace.rules.categoryRules.descriptionHintDescription', categoryName)} ; +function getInitiallyFocusedOptionKey(isAlwaysSelected: boolean, isNeverSelected: boolean): ValueOf { + if (isAlwaysSelected) { + return CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.ALWAYS; + } + + if (isNeverSelected) { + return CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.NEVER; + } + + return CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.DEFAULT; +} + +function performAction(policyID: string, categoryName: string, value: number | null) { + if (typeof value === 'number') { + Category.setPolicyCategoryReceiptsRequired(policyID, categoryName, value); + return; + } + + Category.removePolicyCategoryReceiptsRequired(policyID, categoryName); +} + function CategoryRequireReceiptsOverPage({ route: { params: {policyID, categoryName}, @@ -25,7 +49,31 @@ function CategoryRequireReceiptsOverPage({ const {translate} = useLocalize(); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); - const {inputCallbackRef} = useAutoFocusInput(); + const isAlwaysSelected = policyCategories?.[categoryName]?.maxExpenseAmountNoReceipt === 0; + const isNeverSelected = policyCategories?.[categoryName]?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE; + + const requireReceiptsOverListData = [ + { + value: null, + text: translate(`workspace.rules.categoryRules.requireReceiptsOverList.default`), + keyForList: CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.DEFAULT, + isSelected: !isAlwaysSelected && !isNeverSelected, + }, + { + value: CONST.DISABLED_MAX_EXPENSE_VALUE, + text: translate(`workspace.rules.categoryRules.requireReceiptsOverList.never`), + keyForList: CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.NEVER, + isSelected: isNeverSelected, + }, + { + value: 0, + text: translate(`workspace.rules.categoryRules.requireReceiptsOverList.always`), + keyForList: CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.ALWAYS, + isSelected: isAlwaysSelected, + }, + ]; + + const initiallyFocusedOptionKey = getInitiallyFocusedOptionKey(isAlwaysSelected, isNeverSelected); return ( Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> + { + performAction(policyID, categoryName, item.value); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); + }} + shouldSingleExecuteRowSelect + containerStyle={[styles.pt3]} + initiallyFocusedOptionKey={initiallyFocusedOptionKey} + /> ); From b9cbddb552f526e76e3c10c7eed3b8336858df22 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 26 Aug 2024 10:08:09 +0200 Subject: [PATCH 06/31] Add link to enable workflows --- .../workspace/categories/CategorySettingsPage.tsx | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 21da1ab31152..85544d379f78 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -12,6 +12,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Switch from '@components/Switch'; import Text from '@components/Text'; +import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -225,6 +226,18 @@ function CategorySettingsPage({ shouldShowRightIcon /> + {!policy?.areWorkflowsEnabled && ( + + Go to{' '} + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} + > + more features + {' '} + and enable workflows, then add approvals to unlock this feature. + + )} Date: Mon, 26 Aug 2024 11:54:49 +0200 Subject: [PATCH 07/31] Add AmountForm to CategoryFlagAmountsOverPage --- src/components/AmountForm.tsx | 69 +++++++++++++++- src/languages/en.ts | 7 ++ src/libs/API/types.ts | 2 + src/libs/actions/Policy/Category.ts | 78 ++++++++++++++++++- .../CategoryDescriptionHintPage.tsx | 2 +- .../CategoryFlagAmountsOverPage.tsx | 35 +++++++++ 6 files changed, 188 insertions(+), 5 deletions(-) diff --git a/src/components/AmountForm.tsx b/src/components/AmountForm.tsx index 1eb272dce49a..8ad01d4437ae 100644 --- a/src/components/AmountForm.tsx +++ b/src/components/AmountForm.tsx @@ -1,7 +1,7 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import {View} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; @@ -12,6 +12,7 @@ import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; import CONST from '@src/CONST'; import BigNumberPad from './BigNumberPad'; import FormHelpMessage from './FormHelpMessage'; +import TextInput from './TextInput'; import isTextInputFocused from './TextInput/BaseTextInput/isTextInputFocused'; import type {BaseTextInputProps, BaseTextInputRef} from './TextInput/BaseTextInput/types'; import TextInputWithCurrencySymbol from './TextInputWithCurrencySymbol'; @@ -41,6 +42,10 @@ type AmountFormProps = { /** Custom max amount length. It defaults to CONST.IOU.AMOUNT_MAX_LENGTH */ amountMaxLength?: number; + + label?: string; + + displayAsTextInput?: boolean; } & Pick & Pick; @@ -57,7 +62,19 @@ const NUM_PAD_CONTAINER_VIEW_ID = 'numPadContainerView'; const NUM_PAD_VIEW_ID = 'numPadView'; function AmountForm( - {value: amount, currency = CONST.CURRENCY.USD, extraDecimals = 0, amountMaxLength, errorText, onInputChange, onCurrencyButtonPress, isCurrencyPressable = true, ...rest}: AmountFormProps, + { + value: amount, + currency = CONST.CURRENCY.USD, + extraDecimals = 0, + amountMaxLength, + errorText, + onInputChange, + onCurrencyButtonPress, + displayAsTextInput = false, + isCurrencyPressable = true, + label, + ...rest + }: AmountFormProps, forwardedRef: ForwardedRef, ) { const styles = useThemeStyles(); @@ -124,6 +141,29 @@ function AmountForm( [amountMaxLength, currentAmount, decimals, onInputChange, selection], ); + /** + * Set a new amount value properly formatted + * + * @param text - Changed text from user input + */ + const setFormattedAmount = (text: string) => { + // Remove spaces from the newAmount value because Safari on iOS adds spaces when pasting a copied value + // More info: https://github.com/Expensify/App/issues/16974 + const newAmountWithoutSpaces = MoneyRequestUtils.stripSpacesFromAmount(text); + const replacedCommasAmount = MoneyRequestUtils.replaceCommasWithPeriod(newAmountWithoutSpaces); + const withLeadingZero = MoneyRequestUtils.addLeadingZero(replacedCommasAmount); + + if (!MoneyRequestUtils.validateAmount(withLeadingZero, decimals, amountMaxLength)) { + setSelection((prevSelection) => ({...prevSelection})); + return; + } + + const strippedAmount = MoneyRequestUtils.stripCommaFromAmount(withLeadingZero); + const isForwardDelete = currentAmount.length > strippedAmount.length && forwardDeletePressedRef.current; + setSelection(getNewSelection(selection, isForwardDelete ? strippedAmount.length : currentAmount.length, strippedAmount.length)); + onInputChange?.(strippedAmount); + }; + // Modifies the amount to match the decimals for changed currency. useEffect(() => { // If the changed currency supports decimals, we can return @@ -195,6 +235,31 @@ function AmountForm( const formattedAmount = MoneyRequestUtils.replaceAllDigits(currentAmount, toLocaleDigit); const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); + if (displayAsTextInput) { + return ( + { + if (typeof forwardedRef === 'function') { + forwardedRef(ref); + } else if (forwardedRef && 'current' in forwardedRef) { + // eslint-disable-next-line no-param-reassign + forwardedRef.current = ref; + } + textInput.current = ref; + }} + prefixCharacter={currency} + prefixStyle={styles.colorMuted} + keyboardType={CONST.KEYBOARD_TYPE.DECIMAL_PAD} + inputMode={CONST.INPUT_MODE.DECIMAL} + // eslint-disable-next-line react/jsx-props-no-spreading + {...rest} + /> + ); + } + return ( <> `Applies to the category “${categoryName}”.`, + flagAmountsOverSubtitle: 'This overrides the max amount for all expenses.', + expenseLimitTypes: { + expense: 'Individual expense', + expenseSubtitle: 'Flag expense amounts by category. This rule overrides the general workspace rule for max expense amount.', + daily: 'Category total', + dailySubtitle: 'Flag total category spend per expense report.', + }, requireReceiptsOver: 'Require receipts over', requireReceiptsOverList: { default: '$75 Default', diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 4ae2e5fc0c7b..8f5e07aee828 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -206,6 +206,7 @@ const WRITE_COMMANDS = { SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT: 'SetWorkspaceCategoryDescriptionHint', SET_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'SetPolicyCategoryReceiptsRequired', REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'RemoveWorkspaceCategoryReceiptsRequired', + SET_POLICY_CATEGORY_MAX_AMOUNT: 'SetPolicyCategoryMaxAmount', SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax', SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax', SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName', @@ -542,6 +543,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT]: Parameters.SetWorkspaceCategoryDescriptionHintParams; [WRITE_COMMANDS.SET_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.SetPolicyCategoryReceiptsRequiredParams; [WRITE_COMMANDS.REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.RemovePolicyCategoryReceiptsRequiredParams; + [WRITE_COMMANDS.SET_POLICY_CATEGORY_MAX_AMOUNT]: Parameters.SetPolicyCategoryMaxAmountParams; [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 5f18b37ffabf..8abf69f76b9a 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -7,12 +7,14 @@ import type { OpenPolicyCategoriesPageParams, RemovePolicyCategoryReceiptsRequiredParams, SetPolicyCategoryDescriptionRequiredParams, + SetPolicyCategoryMaxAmountParams, SetPolicyCategoryReceiptsRequiredParams, SetPolicyDistanceRatesDefaultCategoryParams, SetWorkspaceCategoryDescriptionHintParams, UpdatePolicyCategoryGLCodeParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; import Log from '@libs/Log'; @@ -23,6 +25,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyCategory, RecentlyUsedCategories, Report} from '@src/types/onyx'; import type {CustomUnit} from '@src/types/onyx/Policy'; +import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; import type {OnyxData} from '@src/types/onyx/Request'; const allPolicies: OnyxCollection = {}; @@ -946,7 +949,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn API.write(WRITE_COMMANDS.SET_POLICY_DISTANCE_RATES_DEFAULT_CATEGORY, params, {optimisticData, successData, failureData}); } -function SetWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) { +function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) { const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; const onyxData: OnyxData = { @@ -1010,12 +1013,82 @@ function SetWorkspaceCategoryDescriptionHint(policyID: string, categoryName: str API.write(WRITE_COMMANDS.SET_WORKSPACE_CATEGORY_DESCRIPTION_HINT, parameters, onyxData); } +function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxExpenseAmount: string, expenseLimitType: PolicyCategoryExpenseLimitType) { + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + const parsedMaxExpenseAmount = maxExpenseAmount === '' ? CONST.DISABLED_MAX_EXPENSE_VALUE : CurrencyUtils.convertToBackendAmount(parseFloat(maxExpenseAmount)); + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + maxExpenseAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + expenseLimitType: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + maxExpenseAmount: parsedMaxExpenseAmount, + expenseLimitType, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: null, + pendingFields: { + maxExpenseAmount: null, + expenseLimitType: null, + }, + maxExpenseAmount: parsedMaxExpenseAmount, + expenseLimitType, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + pendingAction: null, + pendingFields: { + maxExpenseAmount: null, + expenseLimitType: null, + }, + }, + }, + }, + ], + }; + + const parameters: SetPolicyCategoryMaxAmountParams = { + policyID, + categoryName, + maxExpenseAmount: parsedMaxExpenseAmount, + expenseLimitType, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_MAX_AMOUNT, parameters, onyxData); +} + export { openPolicyCategoriesPage, buildOptimisticPolicyRecentlyUsedCategories, setWorkspaceCategoryEnabled, setPolicyCategoryDescriptionRequired, - SetWorkspaceCategoryDescriptionHint, + setWorkspaceCategoryDescriptionHint, setWorkspaceRequiresCategory, setPolicyCategoryPayrollCode, createPolicyCategory, @@ -1028,4 +1101,5 @@ export { buildOptimisticPolicyCategories, setPolicyCategoryReceiptsRequired, removePolicyCategoryReceiptsRequired, + setPolicyCategoryMaxAmount, }; diff --git a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx index 034e3def4f47..9cb794850ae9 100644 --- a/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx +++ b/src/pages/workspace/categories/CategoryDescriptionHintPage.tsx @@ -56,7 +56,7 @@ function CategoryDescriptionHintPage({ style={[styles.flexGrow1, styles.mh5]} formID={ONYXKEYS.FORMS.WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM} onSubmit={({commentHint}) => { - Category.SetWorkspaceCategoryDescriptionHint(policyID, categoryName, commentHint); + Category.setWorkspaceCategoryDescriptionHint(policyID, categoryName, commentHint); Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); }} submitButtonText={translate('common.save')} diff --git a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx index e90d2d1ba195..01b8ad1b9602 100644 --- a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx @@ -1,18 +1,27 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; +import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; +import AmountForm from '@components/AmountForm'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; 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 usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import INPUT_IDS from '@src/types/form/WorkspaceCategoryFlagAmountsOverForm'; type EditCategoryPageProps = StackScreenProps; @@ -21,6 +30,7 @@ function CategoryFlagAmountsOverPage({ params: {policyID, categoryName}, }, }: EditCategoryPageProps) { + const policy = usePolicy(policyID); const styles = useThemeStyles(); const {translate} = useLocalize(); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); @@ -43,6 +53,31 @@ function CategoryFlagAmountsOverPage({ title={translate('workspace.categories.flagAmountsOver')} onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> + { + Category.setPolicyCategoryMaxAmount(policyID, categoryName, maxExpenseAmount, 'daily'); + Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); + }} + submitButtonText={translate('workspace.editor.save')} + enabledWhenOffline + > + + {translate('workspace.rules.categoryRules.flagAmountsOverDescription', categoryName)} + + {translate('workspace.rules.categoryRules.flagAmountsOverSubtitle')} + + ); From d0e5dbb8254281d417a3ae9e8775c332395e6d08 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 26 Aug 2024 12:49:59 +0200 Subject: [PATCH 08/31] Add ExpenseLimitType selector --- src/CONST.ts | 4 + .../CategoryFlagAmountsOverPage.tsx | 18 ++++- .../ExpenseLimitTypeSelector.tsx | 69 +++++++++++++++++ .../ExpenseLimitTypeSelectorModal.tsx | 76 +++++++++++++++++++ 4 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx create mode 100644 src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelectorModal.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 8e0d8cf407c6..74c289669a3a 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2227,6 +2227,10 @@ const CONST = { NEVER: 'never', ALWAYS: 'always', }, + EXPENSE_LIMIT_TYPES: { + EXPENSE: 'expense', + DAILY: 'daily', + }, }, CUSTOM_UNITS: { diff --git a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx index 01b8ad1b9602..eafc8352171f 100644 --- a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import AmountForm from '@components/AmountForm'; @@ -22,6 +22,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import INPUT_IDS from '@src/types/form/WorkspaceCategoryFlagAmountsOverForm'; +import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; +import ExpenseLimitTypeSelector from './ExpenseLimitTypeSelector/ExpenseLimitTypeSelector'; type EditCategoryPageProps = StackScreenProps; @@ -34,6 +36,7 @@ function CategoryFlagAmountsOverPage({ const styles = useThemeStyles(); const {translate} = useLocalize(); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const [expenseLimitType, setExpenseLimitType] = useState(policyCategories?.[categoryName]?.expenseLimitType ?? CONST.POLICY.EXPENSE_LIMIT_TYPES.EXPENSE); const {inputCallbackRef} = useAutoFocusInput(); @@ -54,16 +57,17 @@ function CategoryFlagAmountsOverPage({ onBackButtonPress={() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> { - Category.setPolicyCategoryMaxAmount(policyID, categoryName, maxExpenseAmount, 'daily'); + Category.setPolicyCategoryMaxAmount(policyID, categoryName, maxExpenseAmount, expenseLimitType); Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); }} submitButtonText={translate('workspace.editor.save')} enabledWhenOffline + submitButtonStyles={styles.ph5} > - + {translate('workspace.rules.categoryRules.flagAmountsOverDescription', categoryName)} {translate('workspace.rules.categoryRules.flagAmountsOverSubtitle')} + diff --git a/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx b/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx new file mode 100644 index 000000000000..8bc59b4203e8 --- /dev/null +++ b/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx @@ -0,0 +1,69 @@ +import React, {useState} from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import {View} from 'react-native'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; +import ExpenseLimitTypeSelectorModal from './ExpenseLimitTypeSelectorModal'; + +type ExpenseLimitTypeSelectorProps = { + /** Function to call when the user selects an expense limit type */ + setNewExpenseLimitType: (value: PolicyCategoryExpenseLimitType) => void; + + /** Currently selected expense limit type */ + defaultValue: PolicyCategoryExpenseLimitType; + + /** Label to display on field */ + label: string; + + /** Any additional styles to apply */ + wrapperStyle: StyleProp; +}; + +function ExpenseLimitTypeSelector({defaultValue, wrapperStyle, label, setNewExpenseLimitType}: ExpenseLimitTypeSelectorProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const [isPickerVisible, setIsPickerVisible] = useState(false); + + const showPickerModal = () => { + setIsPickerVisible(true); + }; + + const hidePickerModal = () => { + setIsPickerVisible(false); + }; + + const updateExpenseLimitTypeInput = (expenseLimitType: PolicyCategoryExpenseLimitType) => { + setNewExpenseLimitType(expenseLimitType); + hidePickerModal(); + }; + + const title = translate(`workspace.rules.categoryRules.expenseLimitTypes.${defaultValue}`); + const descStyle = title.length === 0 ? styles.textNormal : null; + + return ( + + + + + ); +} + +ExpenseLimitTypeSelector.displayName = 'ExpenseLimitTypeSelector'; + +export default ExpenseLimitTypeSelector; diff --git a/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelectorModal.tsx b/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelectorModal.tsx new file mode 100644 index 000000000000..34bd47c484ba --- /dev/null +++ b/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelectorModal.tsx @@ -0,0 +1,76 @@ +import React from 'react'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Modal from '@components/Modal'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; + +type ExpenseLimitTypeSelectorModalProps = { + /** Whether the modal is visible */ + isVisible: boolean; + + /** Selected expense limit type */ + currentExpenseLimitType: PolicyCategoryExpenseLimitType; + + /** Function to call when the user selects an expense limit type */ + onExpenseLimitTypeSelected: (value: PolicyCategoryExpenseLimitType) => void; + + /** Function to call when the user closes the expense limit type selector modal */ + onClose: () => void; + + /** Label to display on field */ + label: string; +}; + +function ExpenseLimitTypeSelectorModal({isVisible, currentExpenseLimitType, onExpenseLimitTypeSelected, onClose, label}: ExpenseLimitTypeSelectorModalProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const expenseLimitTypes = Object.values(CONST.POLICY.EXPENSE_LIMIT_TYPES).map((value) => ({ + value, + text: translate(`workspace.rules.categoryRules.expenseLimitTypes.${value}`), + alternateText: translate(`workspace.rules.categoryRules.expenseLimitTypes.${value}Subtitle`), + keyForList: value, + isSelected: currentExpenseLimitType === value, + })); + + return ( + + + + onExpenseLimitTypeSelected(item.value)} + shouldSingleExecuteRowSelect + containerStyle={[styles.pt3]} + initiallyFocusedOptionKey={currentExpenseLimitType} + /> + + + ); +} + +ExpenseLimitTypeSelectorModal.displayName = 'ExpenseLimitTypeSelectorModal'; + +export default ExpenseLimitTypeSelectorModal; From 04263e92c15d790acb484587714818540541e214 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 28 Aug 2024 11:50:59 +0200 Subject: [PATCH 09/31] Add taxes list --- src/languages/en.ts | 1 + src/libs/API/types.ts | 4 + src/libs/actions/Policy/Category.ts | 75 ++++++++ .../categories/CategoryDefaultTaxRatePage.tsx | 55 +++++- .../categories/CategorySettingsPage.tsx | 170 ++++++++++-------- 5 files changed, 231 insertions(+), 74 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index d1dd63dcd6e8..91f61856e70d 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3669,6 +3669,7 @@ export default { never: 'Never require receipts', always: 'Always require receipts', }, + defaultTaxRate: 'Default tax rate', }, }, }, diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index ba1577e9ec4d..0de3d5af01eb 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -212,6 +212,8 @@ const WRITE_COMMANDS = { SET_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'SetPolicyCategoryReceiptsRequired', REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED: 'RemoveWorkspaceCategoryReceiptsRequired', SET_POLICY_CATEGORY_MAX_AMOUNT: 'SetPolicyCategoryMaxAmount', + SET_POLICY_CATEGORY_APPROVER: 'SetPolicyCategoryApprover', + SET_POLICY_CATEGORY_TAX: 'SetPolicyCategoryTax', SET_POLICY_TAXES_CURRENCY_DEFAULT: 'SetPolicyCurrencyDefaultTax', SET_POLICY_TAXES_FOREIGN_CURRENCY_DEFAULT: 'SetPolicyForeignCurrencyDefaultTax', SET_POLICY_CUSTOM_TAX_NAME: 'SetPolicyCustomTaxName', @@ -553,6 +555,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.SetPolicyCategoryReceiptsRequiredParams; [WRITE_COMMANDS.REMOVE_POLICY_CATEGORY_RECEIPTS_REQUIRED]: Parameters.RemovePolicyCategoryReceiptsRequiredParams; [WRITE_COMMANDS.SET_POLICY_CATEGORY_MAX_AMOUNT]: Parameters.SetPolicyCategoryMaxAmountParams; + [WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER]: Parameters.SetPolicyCategoryApproverParams; + [WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX]: Parameters.SetPolicyCategoryTaxParams; [WRITE_COMMANDS.JOIN_POLICY_VIA_INVITE_LINK]: Parameters.JoinPolicyInviteLinkParams; [WRITE_COMMANDS.ACCEPT_JOIN_REQUEST]: Parameters.AcceptJoinRequestParams; [WRITE_COMMANDS.DECLINE_JOIN_REQUEST]: Parameters.DeclineJoinRequestParams; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 8abf69f76b9a..40db14cf5478 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -6,6 +6,7 @@ import type { EnablePolicyCategoriesParams, OpenPolicyCategoriesPageParams, RemovePolicyCategoryReceiptsRequiredParams, + SetPolicyCategoryApproverParams, SetPolicyCategoryDescriptionRequiredParams, SetPolicyCategoryMaxAmountParams, SetPolicyCategoryReceiptsRequiredParams, @@ -269,6 +270,8 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: string, areCommentsRequired: boolean) { const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + // When areCommentsRequired is set to false, commentHint has to be reset + const updatedCommentHint = areCommentsRequired ? allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]?.commentHint : ''; const onyxData: OnyxData = { optimisticData: [ @@ -283,6 +286,7 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st areCommentsRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, areCommentsRequired, + commentHint: updatedCommentHint, }, }, }, @@ -299,6 +303,7 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st areCommentsRequired: null, }, areCommentsRequired, + commentHint: updatedCommentHint, }, }, }, @@ -1083,6 +1088,74 @@ function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxE API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_MAX_AMOUNT, parameters, onyxData); } +function setPolicyCategoryApprover(policyID: string, categoryName: string, approver: string) { + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + approver: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + approver, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: null, + pendingFields: { + approver: null, + }, + approver, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), + pendingAction: null, + pendingFields: { + maxExpenseAmount: null, + expenseLimitType: null, + }, + }, + }, + }, + ], + }; + + const parameters: SetPolicyCategoryApproverParams = { + policyID, + categoryName, + approver, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER, parameters, onyxData); +} + +function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: string) { + console.log({policyID, categoryName, taxID}); +} + export { openPolicyCategoriesPage, buildOptimisticPolicyRecentlyUsedCategories, @@ -1102,4 +1175,6 @@ export { setPolicyCategoryReceiptsRequired, removePolicyCategoryReceiptsRequired, setPolicyCategoryMaxAmount, + setPolicyCategoryApprover, + setPolicyCategoryTax, }; diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx index 9e253b7d7748..c4f670efce19 100644 --- a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -1,18 +1,24 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React from 'react'; +import React, {useCallback, useMemo} from 'react'; import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import type {ListItem} from '@components/SelectionList/types'; import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import type {TaxRate} from '@src/types/onyx'; type EditCategoryPageProps = StackScreenProps; @@ -24,9 +30,42 @@ function CategoryDefaultTaxRatePage({ const styles = useThemeStyles(); const {translate} = useLocalize(); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const policy = usePolicy(policyID); + const defaultExternalID = policy?.taxRates?.defaultExternalID; + const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; const {inputCallbackRef} = useAutoFocusInput(); + const textForDefault = useCallback( + (taxID: string, taxRate: TaxRate): string => { + let suffix; + if (taxID === defaultExternalID && taxID === foreignTaxDefault) { + suffix = translate('common.default'); + } else if (taxID === defaultExternalID) { + suffix = translate('workspace.taxes.workspaceDefault'); + } else if (taxID === foreignTaxDefault) { + suffix = translate('workspace.taxes.foreignDefault'); + } + return `${taxRate.name} ${CONST.DOT_SEPARATOR} ${taxRate.value}${suffix ? ` ${CONST.DOT_SEPARATOR} ${suffix}` : ``}`; + }, + [defaultExternalID, foreignTaxDefault, translate], + ); + + const taxesList = useMemo(() => { + if (!policy) { + return []; + } + return Object.entries(policy.taxRates?.taxes ?? {}) + .map(([key, value]) => ({ + text: textForDefault(key, value), + keyForList: key, + isSelected: false, + isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + pendingAction: value.pendingAction ?? (Object.keys(value.pendingFields ?? {}).length > 0 ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null), + })) + .sort((a, b) => (a.text ?? a.keyForList ?? '').localeCompare(b.text ?? b.keyForList ?? '')); + }, [policy, textForDefault]); + return ( Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> + { + if (!item.keyForList) { + return; + } + Category.setPolicyCategoryTax(policyID, categoryName, item.keyForList); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); + }} + shouldSingleExecuteRowSelect + containerStyle={[styles.pt3]} + // initiallyFocusedOptionKey={initiallyFocusedOptionKey} + /> ); diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 85544d379f78..51f5efbc4d1b 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -1,5 +1,5 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useEffect, useState} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -67,6 +67,14 @@ function CategorySettingsPage({ navigation.setParams({categoryName: policyCategory?.name}); }, [categoryName, navigation, policyCategory]); + const flagAmountsOverText = useMemo(() => { + if (policyCategory?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE || !policyCategory?.maxExpenseAmount) { + return ''; + } + + return ``; + }, [policy?.maxExpenseAmount, translate]); + if (!policyCategory) { return ; } @@ -183,81 +191,97 @@ function CategorySettingsPage({ /> - - {translate('workspace.rules.categoryRules.title')} - - - Category.clearCategoryErrors(policyID, categoryName)} - > - - - {translate('workspace.rules.categoryRules.requireDescription')} - Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired)} - /> + {policy?.areRulesEnabled && ( + <> + + {translate('workspace.rules.categoryRules.title')} - - - {policyCategory?.areCommentsRequired && ( - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - )} - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - {!policy?.areWorkflowsEnabled && ( - - Go to{' '} - Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} + Category.clearCategoryErrors(policyID, categoryName)} > - more features - {' '} - and enable workflows, then add approvals to unlock this feature. - + + + {translate('workspace.rules.categoryRules.requireDescription')} + Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired)} + /> + + + + {policyCategory?.areCommentsRequired && ( + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + )} + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + {policy?.tax?.trackingEnabled && ( + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + )} + {!policy?.areWorkflowsEnabled && ( + + Go to{' '} + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} + > + more features + {' '} + and enable workflows, then add approvals to unlock this feature. + + )} + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + )} - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - + {!isThereAnyAccountingConnection && ( Date: Wed, 28 Aug 2024 14:00:45 +0200 Subject: [PATCH 10/31] Add WorkspaceMembersSelectionList --- .../WorkspaceMembersSelectionList.tsx | 115 ++++++++++++++++++ .../categories/CategoryApproverPage.tsx | 13 +- 2 files changed, 125 insertions(+), 3 deletions(-) create mode 100644 src/components/WorkspaceMembersSelectionList.tsx diff --git a/src/components/WorkspaceMembersSelectionList.tsx b/src/components/WorkspaceMembersSelectionList.tsx new file mode 100644 index 000000000000..f2ce215fc500 --- /dev/null +++ b/src/components/WorkspaceMembersSelectionList.tsx @@ -0,0 +1,115 @@ +import React, {useMemo} from 'react'; +import type {SectionListData} from 'react-native'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import useScreenWrapperTranstionStatus from '@hooks/useScreenWrapperTransitionStatus'; +import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import type {Icon} from '@src/types/onyx/OnyxCommon'; +import Badge from './Badge'; +import {FallbackAvatar} from './Icon/Expensicons'; +import {usePersonalDetails} from './OnyxProvider'; +import SelectionList from './SelectionList'; +import InviteMemberListItem from './SelectionList/InviteMemberListItem'; +import type {Section} from './SelectionList/types'; + +type WorkspaceMembersSelectionListProps = { + policyID: string; + selectedApprover: string; + setApprover: (email: string) => void; +}; + +type SelectionListApprover = { + text: string; + alternateText: string; + keyForList: string; + isSelected: boolean; + login: string; + rightElement?: React.ReactNode; + icons: Icon[]; +}; +type ApproverSection = SectionListData>; + +function WorkspaceMembersSelectionList({policyID, selectedApprover, setApprover}: WorkspaceMembersSelectionListProps) { + const {translate} = useLocalize(); + const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const personalDetails = usePersonalDetails(); + const policy = usePolicy(policyID); + + const sections: ApproverSection[] = useMemo(() => { + const approvers: SelectionListApprover[] = []; + + if (policy?.employeeList) { + const availableApprovers = Object.values(policy.employeeList) + .map((employee): SelectionListApprover | null => { + const isAdmin = employee?.role === CONST.REPORT.ROLE.ADMIN; + const email = employee.email; + + if (!email) { + return null; + } + + const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(policy?.employeeList); + const accountID = Number(policyMemberEmailsToAccountIDs[email] ?? ''); + const {avatar, displayName = email} = personalDetails?.[accountID] ?? {}; + + return { + text: displayName, + alternateText: email, + keyForList: email, + isSelected: selectedApprover === email, + login: email, + icons: [{source: avatar ?? FallbackAvatar, type: CONST.ICON_TYPE_AVATAR, name: displayName, id: accountID}], + rightElement: isAdmin ? : undefined, + }; + }) + .filter((approver): approver is SelectionListApprover => !!approver); + + approvers.push(...availableApprovers); + } + + const filteredApprovers = + debouncedSearchTerm !== '' + ? approvers.filter((option) => { + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm); + const isPartOfSearchTerm = !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); + return isPartOfSearchTerm; + }) + : approvers; + + return [ + { + title: undefined, + data: OptionsListUtils.sortAlphabetically(filteredApprovers, 'text'), + shouldShow: true, + }, + ]; + }, [debouncedSearchTerm, personalDetails, policy?.employeeList, selectedApprover, translate]); + + const handleOnSelectRow = (approver: SelectionListApprover) => { + setApprover(approver.login); + }; + + const headerMessage = useMemo(() => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''), [searchTerm, sections, translate]); + + return ( + + ); +} + +export default WorkspaceMembersSelectionList; diff --git a/src/pages/workspace/categories/CategoryApproverPage.tsx b/src/pages/workspace/categories/CategoryApproverPage.tsx index 8415742ca8c2..ae882ceef149 100644 --- a/src/pages/workspace/categories/CategoryApproverPage.tsx +++ b/src/pages/workspace/categories/CategoryApproverPage.tsx @@ -3,12 +3,13 @@ import React from 'react'; import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import WorkspaceMembersSelectionList from '@components/WorkspaceMembersSelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -25,8 +26,6 @@ function CategoryApproverPage({ const {translate} = useLocalize(); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); - const {inputCallbackRef} = useAutoFocusInput(); - return ( Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> + { + Category.setPolicyCategoryApprover(policyID, categoryName, email); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); + }} + /> ); From fff496331468435802a437ca74826e8a7b7f2c7a Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Wed, 28 Aug 2024 17:46:19 +0200 Subject: [PATCH 11/31] Add rules types, cleanup category rules pages --- src/libs/TaxUtils.ts | 26 +++++++ src/libs/actions/Policy/Category.ts | 67 +++---------------- .../categories/CategoryDefaultTaxRatePage.tsx | 24 +------ .../CategoryFlagAmountsOverPage.tsx | 9 ++- .../categories/CategorySettingsPage.tsx | 32 ++++++--- src/types/onyx/Policy.ts | 46 +++++++++++++ src/types/onyx/PolicyCategory.ts | 3 - 7 files changed, 115 insertions(+), 92 deletions(-) create mode 100644 src/libs/TaxUtils.ts diff --git a/src/libs/TaxUtils.ts b/src/libs/TaxUtils.ts new file mode 100644 index 000000000000..28997f57ed94 --- /dev/null +++ b/src/libs/TaxUtils.ts @@ -0,0 +1,26 @@ +import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import CONST from '@src/CONST'; +import type {TaxRate, TaxRatesWithDefault} from '@src/types/onyx'; + +function formatTaxText(translate: LocaleContextProps['translate'], taxID: string, taxRate: TaxRate, policyTaxRates?: TaxRatesWithDefault) { + const taxRateText = `${taxRate.name} ${CONST.DOT_SEPARATOR} ${taxRate.value}`; + + if (!policyTaxRates) { + return taxRateText; + } + + const {defaultExternalID, foreignTaxDefault} = policyTaxRates; + let suffix; + + if (taxID === defaultExternalID && taxID === foreignTaxDefault) { + suffix = translate('common.default'); + } else if (taxID === defaultExternalID) { + suffix = translate('workspace.taxes.workspaceDefault'); + } else if (taxID === foreignTaxDefault) { + suffix = translate('workspace.taxes.foreignDefault'); + } + return `${taxRateText}${suffix ? ` ${CONST.DOT_SEPARATOR} ${suffix}` : ``}`; +} + +// eslint-disable-next-line import/prefer-default-export +export {formatTaxText}; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 40db14cf5478..d260556f95d4 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -10,6 +10,7 @@ import type { SetPolicyCategoryDescriptionRequiredParams, SetPolicyCategoryMaxAmountParams, SetPolicyCategoryReceiptsRequiredParams, + SetPolicyCategoryTaxParams, SetPolicyDistanceRatesDefaultCategoryParams, SetWorkspaceCategoryDescriptionHintParams, UpdatePolicyCategoryGLCodeParams, @@ -1020,7 +1021,7 @@ function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: str function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxExpenseAmount: string, expenseLimitType: PolicyCategoryExpenseLimitType) { const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; - const parsedMaxExpenseAmount = maxExpenseAmount === '' ? CONST.DISABLED_MAX_EXPENSE_VALUE : CurrencyUtils.convertToBackendAmount(parseFloat(maxExpenseAmount)); + const parsedMaxExpenseAmount = maxExpenseAmount === '' ? null : CurrencyUtils.convertToBackendAmount(parseFloat(maxExpenseAmount)); const onyxData: OnyxData = { optimisticData: [ @@ -1089,71 +1090,23 @@ function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxE } function setPolicyCategoryApprover(policyID: string, categoryName: string, approver: string) { - const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; - - const onyxData: OnyxData = { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - ...policyCategoryToUpdate, - pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - pendingFields: { - approver: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - approver, - }, - }, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - ...policyCategoryToUpdate, - pendingAction: null, - pendingFields: { - approver: null, - }, - approver, - }, - }, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, - value: { - [categoryName]: { - ...policyCategoryToUpdate, - errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - pendingAction: null, - pendingFields: { - maxExpenseAmount: null, - expenseLimitType: null, - }, - }, - }, - }, - ], - }; - const parameters: SetPolicyCategoryApproverParams = { policyID, categoryName, approver, }; - API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER, parameters, onyxData); + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER, parameters); } function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: string) { - console.log({policyID, categoryName, taxID}); + const parameters: SetPolicyCategoryTaxParams = { + policyID, + categoryName, + taxID, + }; + + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX, parameters); } export { diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx index c4f670efce19..9dcc6ab88440 100644 --- a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -1,21 +1,19 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React, {useCallback, useMemo} from 'react'; -import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import type {ListItem} from '@components/SelectionList/types'; -import useAutoFocusInput from '@hooks/useAutoFocusInput'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import * as TaxUtils from '@libs/TaxUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; import type {TaxRate} from '@src/types/onyx'; @@ -29,27 +27,9 @@ function CategoryDefaultTaxRatePage({ }: EditCategoryPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const policy = usePolicy(policyID); - const defaultExternalID = policy?.taxRates?.defaultExternalID; - const foreignTaxDefault = policy?.taxRates?.foreignTaxDefault; - const {inputCallbackRef} = useAutoFocusInput(); - - const textForDefault = useCallback( - (taxID: string, taxRate: TaxRate): string => { - let suffix; - if (taxID === defaultExternalID && taxID === foreignTaxDefault) { - suffix = translate('common.default'); - } else if (taxID === defaultExternalID) { - suffix = translate('workspace.taxes.workspaceDefault'); - } else if (taxID === foreignTaxDefault) { - suffix = translate('workspace.taxes.foreignDefault'); - } - return `${taxRate.name} ${CONST.DOT_SEPARATOR} ${taxRate.value}${suffix ? ` ${CONST.DOT_SEPARATOR} ${suffix}` : ``}`; - }, - [defaultExternalID, foreignTaxDefault, translate], - ); + const textForDefault = useCallback((taxID: string, taxRate: TaxRate) => TaxUtils.formatTaxText(translate, taxID, taxRate, policy?.taxRates), [policy?.taxRates, translate]); const taxesList = useMemo(() => { if (!policy) { diff --git a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx index eafc8352171f..b34e490f41be 100644 --- a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx @@ -40,6 +40,13 @@ function CategoryFlagAmountsOverPage({ const {inputCallbackRef} = useAutoFocusInput(); + const policyCategoryMaxExpenseAmount = policyCategories?.[categoryName]?.maxExpenseAmount; + + const defaultValue = + policyCategoryMaxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE || !policyCategoryMaxExpenseAmount + ? '' + : CurrencyUtils.convertToFrontendAmountAsString(policyCategoryMaxExpenseAmount, policy?.outputCurrency); + return ( category.previousCategoryName === categoryName); + const policyCurrency = policy?.outputCurrency ?? CONST.CURRENCY.USD; + const policyCategoryExpenseLimitType = policyCategory?.expenseLimitType ?? CONST.POLICY.EXPENSE_LIMIT_TYPES.EXPENSE; const areCommentsRequired = policyCategory?.areCommentsRequired ?? false; @@ -72,8 +76,18 @@ function CategorySettingsPage({ return ''; } - return ``; - }, [policy?.maxExpenseAmount, translate]); + return `${CurrencyUtils.convertToDisplayString(policyCategory?.maxExpenseAmount, policyCurrency)} ${CONST.DOT_SEPARATOR} ${translate( + `workspace.rules.categoryRules.expenseLimitTypes.${policyCategoryExpenseLimitType}`, + )}`; + }, [policyCategory?.maxExpenseAmount, policyCategoryExpenseLimitType, policyCurrency, translate]); + + const approverText = useMemo(() => { + return policy?.rules?.approvalRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.approver ?? ''; + }, [categoryName, policy?.rules?.approvalRules]); + + const defaultTaxRateText = useMemo(() => { + return policy?.rules?.expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID ?? ''; + }, [categoryName, policy?.rules?.expenseRules]); if (!policyCategory) { return ; @@ -124,7 +138,7 @@ function CategorySettingsPage({ cancelText={translate('common.cancel')} danger /> - + )} - + { Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); @@ -236,9 +250,9 @@ function CategorySettingsPage({ /> {policy?.tax?.trackingEnabled && ( - + { Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); @@ -289,7 +303,7 @@ function CategorySettingsPage({ onPress={() => setDeleteCategoryConfirmModalVisible(true)} /> )} - + ); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 0260b3354f79..897ce2f6f739 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1343,6 +1343,43 @@ type PendingJoinRequestPolicy = { >; }; +type ExpenseRule = { + tax: { + field_id_TAX: { + externalID: string; + }; + }; + /** Set of conditions under which the expense rule should be applied */ + applyWhen: ApplyRulesWhen[]; + + /** An id of the rule */ + id: string; +}; + +/** Data informing when a given rule should be applied */ +type ApplyRulesWhen = { + /** The condition for applying the rule to the workspace */ + condition: 'matches'; + + /** The target field to which the rule is applied */ + field: 'category'; + + /** The value of the target field */ + value: string; +}; + +/** Approval rule data model */ +type ApprovalRule = { + /** The approver's email */ + approver: string; + + /** Set of conditions under which the approval rule should be applied */ + applyWhen: ApplyRulesWhen[]; + + /** An id of the rule */ + id: string; +}; + /** Model of policy data */ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { @@ -1480,6 +1517,15 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Collection of tax rates attached to a policy */ taxRates?: TaxRatesWithDefault; + /** A set of rules related to the workpsace */ + rules?: { + /** A set of rules related to the workpsace approvals */ + approvalRules?: ApprovalRule[]; + + /** A set of rules related to the workpsace expenses */ + expenseRules?: ExpenseRule[]; + }; + /** ReportID of the admins room for this workspace */ chatReportIDAdmins?: number; diff --git a/src/types/onyx/PolicyCategory.ts b/src/types/onyx/PolicyCategory.ts index 5dfcc5ca345a..a14a535b4378 100644 --- a/src/types/onyx/PolicyCategory.ts +++ b/src/types/onyx/PolicyCategory.ts @@ -36,9 +36,6 @@ type PolicyCategory = OnyxCommon.OnyxValueWithOfflineFeedback<{ /** A list of errors keyed by microtime */ errors?: OnyxCommon.Errors | null; - /** The approver of the policy category */ - approver?: string; - commentHint?: string; maxExpenseAmount?: number; From 06e7480ad24773f9a28503324156f6d2edd9dc2a Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 29 Aug 2024 10:17:00 +0200 Subject: [PATCH 12/31] Add CateogryUtils --- src/languages/en.ts | 2 +- src/libs/CategoryUtils.ts | 46 +++++++++++++++++++ .../FULL_SCREEN_TO_RHP_MAPPING.ts | 2 +- src/libs/TaxUtils.ts | 26 ----------- .../categories/CategoryDefaultTaxRatePage.tsx | 7 ++- .../CategoryRequireReceiptsOverPage.tsx | 9 +++- .../categories/CategorySettingsPage.tsx | 26 +++++++++-- 7 files changed, 84 insertions(+), 34 deletions(-) create mode 100644 src/libs/CategoryUtils.ts delete mode 100644 src/libs/TaxUtils.ts diff --git a/src/languages/en.ts b/src/languages/en.ts index 91f61856e70d..0416d554c583 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3665,7 +3665,7 @@ export default { }, requireReceiptsOver: 'Require receipts over', requireReceiptsOverList: { - default: '$75 Default', + default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Default`, never: 'Never require receipts', always: 'Always require receipts', }, diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts new file mode 100644 index 000000000000..90d53f9a4e51 --- /dev/null +++ b/src/libs/CategoryUtils.ts @@ -0,0 +1,46 @@ +import type {LocaleContextProps} from '@components/LocaleContextProvider'; +import CONST from '@src/CONST'; +import type {Policy, TaxRate, TaxRatesWithDefault} from '@src/types/onyx'; +import * as CurrencyUtils from './CurrencyUtils'; + +function formatDefaultTaxRateText(translate: LocaleContextProps['translate'], taxID: string, taxRate: TaxRate, policyTaxRates?: TaxRatesWithDefault) { + const taxRateText = `${taxRate.name} ${CONST.DOT_SEPARATOR} ${taxRate.value}`; + + if (!policyTaxRates) { + return taxRateText; + } + + const {defaultExternalID, foreignTaxDefault} = policyTaxRates; + let suffix; + + if (taxID === defaultExternalID && taxID === foreignTaxDefault) { + suffix = translate('common.default'); + } else if (taxID === defaultExternalID) { + suffix = translate('workspace.taxes.workspaceDefault'); + } else if (taxID === foreignTaxDefault) { + suffix = translate('workspace.taxes.foreignDefault'); + } + return `${taxRateText}${suffix ? ` ${CONST.DOT_SEPARATOR} ${suffix}` : ``}`; +} + +function formatRequireReceiptsOverText(translate: LocaleContextProps['translate'], policy: Policy, categoryMaxExpenseAmountNoReceipt?: number | null) { + const isAlwaysSelected = categoryMaxExpenseAmountNoReceipt === 0; + const isNeverSelected = categoryMaxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE; + + if (isAlwaysSelected) { + return translate(`workspace.rules.categoryRules.requireReceiptsOverList.always`); + } + + if (isNeverSelected) { + return translate(`workspace.rules.categoryRules.requireReceiptsOverList.never`); + } + + const maxExpenseAmountToDisplay = policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmount; + + return translate( + `workspace.rules.categoryRules.requireReceiptsOverList.default`, + CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD), + ); +} + +export {formatDefaultTaxRateText, formatRequireReceiptsOverText}; diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index f9ef9b5c05be..94a564abbdd6 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -145,7 +145,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER, SCREENS.WORKSPACE.CATEGORY_DESCRIPTION_HINT, SCREENS.WORKSPACE.CATEGORY_APPROVER, - SCREENS.WORKSPACE.CATEGORY_FLAG_AMOUNTS_OVER, + SCREENS.WORKSPACE.CATEGORY_REQUIRE_RECEIPTS_OVER, ], [SCREENS.WORKSPACE.DISTANCE_RATES]: [ SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, diff --git a/src/libs/TaxUtils.ts b/src/libs/TaxUtils.ts deleted file mode 100644 index 28997f57ed94..000000000000 --- a/src/libs/TaxUtils.ts +++ /dev/null @@ -1,26 +0,0 @@ -import type {LocaleContextProps} from '@components/LocaleContextProvider'; -import CONST from '@src/CONST'; -import type {TaxRate, TaxRatesWithDefault} from '@src/types/onyx'; - -function formatTaxText(translate: LocaleContextProps['translate'], taxID: string, taxRate: TaxRate, policyTaxRates?: TaxRatesWithDefault) { - const taxRateText = `${taxRate.name} ${CONST.DOT_SEPARATOR} ${taxRate.value}`; - - if (!policyTaxRates) { - return taxRateText; - } - - const {defaultExternalID, foreignTaxDefault} = policyTaxRates; - let suffix; - - if (taxID === defaultExternalID && taxID === foreignTaxDefault) { - suffix = translate('common.default'); - } else if (taxID === defaultExternalID) { - suffix = translate('workspace.taxes.workspaceDefault'); - } else if (taxID === foreignTaxDefault) { - suffix = translate('workspace.taxes.foreignDefault'); - } - return `${taxRateText}${suffix ? ` ${CONST.DOT_SEPARATOR} ${suffix}` : ``}`; -} - -// eslint-disable-next-line import/prefer-default-export -export {formatTaxText}; diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx index 9dcc6ab88440..dd417b808a69 100644 --- a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -8,8 +8,8 @@ import type {ListItem} from '@components/SelectionList/types'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CategoryUtils from '@libs/CategoryUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as TaxUtils from '@libs/TaxUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Category from '@userActions/Policy/Category'; @@ -29,7 +29,10 @@ function CategoryDefaultTaxRatePage({ const {translate} = useLocalize(); const policy = usePolicy(policyID); - const textForDefault = useCallback((taxID: string, taxRate: TaxRate) => TaxUtils.formatTaxText(translate, taxID, taxRate, policy?.taxRates), [policy?.taxRates, translate]); + const textForDefault = useCallback( + (taxID: string, taxRate: TaxRate) => CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates), + [policy?.taxRates, translate], + ); const taxesList = useMemo(() => { if (!policy) { diff --git a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx index 7ba441a5ae37..c4240d7de9c6 100644 --- a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx @@ -7,7 +7,9 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -47,15 +49,20 @@ function CategoryRequireReceiptsOverPage({ }: EditCategoryPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + const policy = usePolicy(policyID); const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); const isAlwaysSelected = policyCategories?.[categoryName]?.maxExpenseAmountNoReceipt === 0; const isNeverSelected = policyCategories?.[categoryName]?.maxExpenseAmountNoReceipt === CONST.DISABLED_MAX_EXPENSE_VALUE; + const maxExpenseAmountToDisplay = policy?.maxExpenseAmount === CONST.DISABLED_MAX_EXPENSE_VALUE ? 0 : policy?.maxExpenseAmount; const requireReceiptsOverListData = [ { value: null, - text: translate(`workspace.rules.categoryRules.requireReceiptsOverList.default`), + text: translate( + `workspace.rules.categoryRules.requireReceiptsOverList.default`, + CurrencyUtils.convertToShortDisplayString(maxExpenseAmountToDisplay, policy?.outputCurrency ?? CONST.CURRENCY.USD), + ), keyForList: CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.DEFAULT, isSelected: !isAlwaysSelected && !isNeverSelected, }, diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 8b11e6b62cbe..873d8a2d48a0 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -17,6 +17,7 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CategoryUtils from '@libs/CategoryUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; @@ -86,8 +87,27 @@ function CategorySettingsPage({ }, [categoryName, policy?.rules?.approvalRules]); const defaultTaxRateText = useMemo(() => { - return policy?.rules?.expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID ?? ''; - }, [categoryName, policy?.rules?.expenseRules]); + const taxID = policy?.rules?.expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID; + + if (!taxID) { + return ''; + } + + const taxRate = policy?.taxRates?.taxes[taxID]; + + if (!taxRate) { + return ''; + } + + return CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates); + }, [categoryName, policy?.rules?.expenseRules, policy?.taxRates, translate]); + + const requireReceiptsOverText = useMemo(() => { + if (!policy) { + return ''; + } + return CategoryUtils.formatRequireReceiptsOverText(translate, policy, policyCategory?.maxExpenseAmountNoReceipt); + }, [policy, policyCategory?.maxExpenseAmountNoReceipt, translate]); if (!policyCategory) { return ; @@ -285,7 +305,7 @@ function CategorySettingsPage({ { Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.getRoute(policyID, policyCategory.name)); From b8579b1e761474109a94d3d93d84ac12972110ac Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Thu, 29 Aug 2024 13:40:56 +0200 Subject: [PATCH 13/31] Handle sending onyx data when updating category approver and default tax rate --- .../SetPolicyCategoryMaxAmountParams.ts | 2 +- src/libs/CategoryUtils.ts | 11 +- src/libs/actions/Policy/Category.ts | 158 +++++++++++++++++- .../categories/CategoryApproverPage.tsx | 10 +- .../categories/CategoryDefaultTaxRatePage.tsx | 8 +- .../categories/CategorySettingsPage.tsx | 9 +- src/types/onyx/Policy.ts | 6 +- 7 files changed, 184 insertions(+), 20 deletions(-) diff --git a/src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts b/src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts index c887b8b831e0..6132f0a69b1b 100644 --- a/src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts +++ b/src/libs/API/parameters/SetPolicyCategoryMaxAmountParams.ts @@ -3,7 +3,7 @@ import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategor type SetPolicyCategoryMaxAmountParams = { policyID: string; categoryName: string; - maxExpenseAmount: number; + maxExpenseAmount: number | null; expenseLimitType: PolicyCategoryExpenseLimitType; }; diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index 90d53f9a4e51..bc11937f51f6 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -1,6 +1,7 @@ import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; import type {Policy, TaxRate, TaxRatesWithDefault} from '@src/types/onyx'; +import type {ApprovalRule, ExpenseRule} from '@src/types/onyx/Policy'; import * as CurrencyUtils from './CurrencyUtils'; function formatDefaultTaxRateText(translate: LocaleContextProps['translate'], taxID: string, taxRate: TaxRate, policyTaxRates?: TaxRatesWithDefault) { @@ -43,4 +44,12 @@ function formatRequireReceiptsOverText(translate: LocaleContextProps['translate' ); } -export {formatDefaultTaxRateText, formatRequireReceiptsOverText}; +function getCategoryApprover(approvalRules: ApprovalRule[], categoryName: string) { + return approvalRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.approver; +} + +function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: string) { + return expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID; +} + +export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApprover, getCategoryDefaultTaxRate}; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index d260556f95d4..c0576b222307 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -26,7 +26,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, PolicyCategories, PolicyCategory, RecentlyUsedCategories, Report} from '@src/types/onyx'; -import type {CustomUnit} from '@src/types/onyx/Policy'; +import type {ApprovalRule, CustomUnit, ExpenseRule} from '@src/types/onyx/Policy'; import type {PolicyCategoryExpenseLimitType} from '@src/types/onyx/PolicyCategory'; import type {OnyxData} from '@src/types/onyx/Request'; @@ -1090,23 +1090,171 @@ function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxE } function setPolicyCategoryApprover(policyID: string, categoryName: string, approver: string) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const approvalRules = policy?.rules?.approvalRules ?? []; + const existingCategoryApproverRule = approvalRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); + const updatedApprovalRules: ApprovalRule[] = [...approvalRules]; + let newApprover = approver; + + if (!existingCategoryApproverRule) { + updatedApprovalRules.push({ + approver, + applyWhen: [ + { + condition: 'matches', + field: 'category', + value: categoryName, + }, + ], + }); + } else if (existingCategoryApproverRule?.approver === approver) { + updatedApprovalRules.filter((rule) => rule.approver === approver); + newApprover = ''; + } else { + const indexToUpdate = updatedApprovalRules.indexOf(existingCategoryApproverRule); + updatedApprovalRules[indexToUpdate].approver = approver; + } + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + ...policy?.rules, + approvalRules: updatedApprovalRules, + }, + pendingFields: { + rules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: { + rules: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + ...policy?.rules, + approvalRules, + }, + pendingFields: { + rules: null, + }, + }, + }, + ], + }; + const parameters: SetPolicyCategoryApproverParams = { policyID, categoryName, - approver, + approver: newApprover, }; - API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER, parameters); + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_APPROVER, parameters, onyxData); } function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: string) { + const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; + const expenseRules = policy?.rules?.expenseRules ?? []; + const existingCategoryExpenseRule = expenseRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); + const updatedExpenseRules: ExpenseRule[] = [...expenseRules]; + let newTaxID = taxID; + + if (!existingCategoryExpenseRule) { + updatedExpenseRules.push({ + tax: { + // eslint-disable-next-line @typescript-eslint/naming-convention + field_id_TAX: { + externalID: taxID, + }, + }, + applyWhen: [ + { + condition: 'matches', + field: 'category', + value: categoryName, + }, + ], + }); + } else if (existingCategoryExpenseRule?.tax?.field_id_TAX?.externalID === taxID) { + updatedExpenseRules.filter((rule) => rule.tax.field_id_TAX.externalID === taxID); + newTaxID = ''; + } else { + const indexToUpdate = updatedExpenseRules.indexOf(existingCategoryExpenseRule); + updatedExpenseRules[indexToUpdate].tax.field_id_TAX.externalID = taxID; + } + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + ...policy?.rules, + expenseRules: updatedExpenseRules, + }, + pendingFields: { + rules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + + value: { + rules: { + ...policy?.rules, + expenseRules: updatedExpenseRules, + }, + pendingFields: { + rules: null, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + rules: { + ...policy?.rules, + expenseRules, + }, + pendingFields: { + rules: null, + }, + }, + }, + ], + }; + const parameters: SetPolicyCategoryTaxParams = { policyID, categoryName, - taxID, + taxID: newTaxID, }; - API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX, parameters); + API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX, parameters, onyxData); } export { diff --git a/src/pages/workspace/categories/CategoryApproverPage.tsx b/src/pages/workspace/categories/CategoryApproverPage.tsx index ae882ceef149..4754c6645cca 100644 --- a/src/pages/workspace/categories/CategoryApproverPage.tsx +++ b/src/pages/workspace/categories/CategoryApproverPage.tsx @@ -1,17 +1,17 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import {useOnyx} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import WorkspaceMembersSelectionList from '@components/WorkspaceMembersSelectionList'; import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CategoryUtils from '@libs/CategoryUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import * as Category from '@userActions/Policy/Category'; import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; @@ -24,7 +24,9 @@ function CategoryApproverPage({ }: EditCategoryPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`); + const policy = usePolicy(policyID); + + const selectedApprover = CategoryUtils.getCategoryApprover(policy?.rules?.approvalRules ?? [], categoryName) ?? ''; return ( { Category.setPolicyCategoryApprover(policyID, categoryName, email); Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx index dd417b808a69..57ba37cadf4b 100644 --- a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -29,6 +29,8 @@ function CategoryDefaultTaxRatePage({ const {translate} = useLocalize(); const policy = usePolicy(policyID); + const selectedTaxRate = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName); + const textForDefault = useCallback( (taxID: string, taxRate: TaxRate) => CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates), [policy?.taxRates, translate], @@ -42,12 +44,12 @@ function CategoryDefaultTaxRatePage({ .map(([key, value]) => ({ text: textForDefault(key, value), keyForList: key, - isSelected: false, + isSelected: key === selectedTaxRate, isDisabled: value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, pendingAction: value.pendingAction ?? (Object.keys(value.pendingFields ?? {}).length > 0 ? CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE : null), })) .sort((a, b) => (a.text ?? a.keyForList ?? '').localeCompare(b.text ?? b.keyForList ?? '')); - }, [policy, textForDefault]); + }, [policy, selectedTaxRate, textForDefault]); return ( diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 873d8a2d48a0..e6d00d2baf3a 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -83,11 +83,12 @@ function CategorySettingsPage({ }, [policyCategory?.maxExpenseAmount, policyCategoryExpenseLimitType, policyCurrency, translate]); const approverText = useMemo(() => { - return policy?.rules?.approvalRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.approver ?? ''; - }, [categoryName, policy?.rules?.approvalRules]); + const categoryApprover = CategoryUtils.getCategoryApprover(policy?.rules?.approvalRules ?? [], categoryName); + return categoryApprover ?? ''; + }, [categoryName, policy]); const defaultTaxRateText = useMemo(() => { - const taxID = policy?.rules?.expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID; + const taxID = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName); if (!taxID) { return ''; @@ -100,7 +101,7 @@ function CategorySettingsPage({ } return CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates); - }, [categoryName, policy?.rules?.expenseRules, policy?.taxRates, translate]); + }, [categoryName, policy, translate]); const requireReceiptsOverText = useMemo(() => { if (!policy) { diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 897ce2f6f739..bc71e929e951 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1353,7 +1353,7 @@ type ExpenseRule = { applyWhen: ApplyRulesWhen[]; /** An id of the rule */ - id: string; + id?: string; }; /** Data informing when a given rule should be applied */ @@ -1377,7 +1377,7 @@ type ApprovalRule = { applyWhen: ApplyRulesWhen[]; /** An id of the rule */ - id: string; + id?: string; }; /** Model of policy data */ @@ -1683,4 +1683,6 @@ export type { SageIntacctConnectionsConfig, SageIntacctExportConfig, ACHAccount, + ApprovalRule, + ExpenseRule, }; From 40c7959d9e85bc5d360635106f9c435270e48385 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 30 Aug 2024 14:15:31 +0200 Subject: [PATCH 14/31] Small fixes, add missing docs and translations --- src/languages/en.ts | 8 +- src/languages/es.ts | 29 ++ .../categories/CategorySettingsPage.tsx | 295 +++++++++--------- src/types/onyx/Policy.ts | 31 +- 4 files changed, 196 insertions(+), 167 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 9b22aea67118..6feca888b3d1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2843,11 +2843,6 @@ export default { updatePayrollCodeFailureMessage: 'An error occurred while updating the payroll code, please try again.', glCode: 'GL code', updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', - requireDescription: 'Require description', - defaultTaxRate: 'Default tax rate', - flagAmountsOver: 'Flag amounts over', - descriptionHint: 'Description hint', - approver: 'Approver', }, moreFeatures: { spendSection: { @@ -3651,6 +3646,7 @@ export default { }, categoryRules: { title: 'Category rules', + approver: 'Approver', requireDescription: 'Require description', descriptionHint: 'Description hint', descriptionHintDescription: (categoryName: string) => @@ -3674,6 +3670,8 @@ export default { always: 'Always require receipts', }, defaultTaxRate: 'Default tax rate', + goTo: 'Go to', + andEnableWorkflows: 'and enable workflows, then add approvals to unlock this feature.', }, }, }, diff --git a/src/languages/es.ts b/src/languages/es.ts index f2a1a5b339b9..8fba2f6457bb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3693,6 +3693,35 @@ export default { title: 'Informes de gastos', subtitle: 'Automatiza el cumplimiento, la aprobación y el pago de los informes de gastos.', }, + categoryRules: { + title: 'Reglas de categoría', + approver: 'Aprobador', + requireDescription: 'Requerir descripción', + descriptionHint: 'Sugerencia de descripción', + descriptionHintDescription: (categoryName: string) => + `Recuerda a los empleados proporcionar información adicional para el gasto en “${categoryName}”. Esta sugerencia aparece en el campo de descripción en los gastos.`, + descriptionHintLabel: 'Sugerencia', + descriptionHintSubtitle: 'Consejo: ¡Cuanto más corta, mejor!', + maxAmount: 'Monto máximo', + flagAmountsOver: 'Marcar montos superiores a', + flagAmountsOverDescription: (categoryName: string) => `Aplica a la categoría “${categoryName}”.`, + flagAmountsOverSubtitle: 'Esto anula el monto máximo para todos los gastos.', + expenseLimitTypes: { + expense: 'Gasto individual', + expenseSubtitle: 'Marcar montos de gastos por categoría. Esta regla anula la regla general del espacio de trabajo para el monto máximo de gasto.', + daily: 'Total por categoría', + dailySubtitle: 'Marcar el gasto total por categoría en cada informe de gastos.', + }, + requireReceiptsOver: 'Requerir recibos para montos superiores a', + requireReceiptsOverList: { + default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Predeterminado`, + never: 'Nunca requerir recibos', + always: 'Requerir recibos siempre', + }, + defaultTaxRate: 'Tasa de impuesto predeterminada', + goTo: 'Ve a', + andEnableWorkflows: 'y habilita flujos de trabajo, luego agrega aprobaciones para desbloquear esta función.', + }, }, }, getAssistancePage: { diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index e6d00d2baf3a..ac13884bff66 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -145,186 +145,183 @@ function CategorySettingsPage({ style={[styles.defaultModalContainer]} testID={CategorySettingsPage.displayName} > - - setDeleteCategoryConfirmModalVisible(false)} - title={translate('workspace.categories.deleteCategory')} - prompt={translate('workspace.categories.deleteCategoryPrompt')} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - - Category.clearCategoryErrors(policyID, categoryName)} - > - - - {translate('workspace.categories.enableCategory')} - - - - - - - - - { - if (!isControlPolicy(policy)) { - Navigation.navigate( - ROUTES.WORKSPACE_UPGRADE.getRoute( - policyID, - CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias, - ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(policyID, policyCategory.name), - ), - ); - return; - } - Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon + {({safeAreaPaddingBottomStyle}) => ( + <> + - - - { - if (!isControlPolicy(policy)) { - Navigation.navigate( - ROUTES.WORKSPACE_UPGRADE.getRoute( - policyID, - CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias, - ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(policyID, policyCategory.name), - ), - ); - return; - } - Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon + setDeleteCategoryConfirmModalVisible(false)} + title={translate('workspace.categories.deleteCategory')} + prompt={translate('workspace.categories.deleteCategoryPrompt')} + confirmText={translate('common.delete')} + cancelText={translate('common.cancel')} + danger /> - - - {policy?.areRulesEnabled && ( - <> - - {translate('workspace.rules.categoryRules.title')} - + Category.clearCategoryErrors(policyID, categoryName)} > - {translate('workspace.rules.categoryRules.requireDescription')} + {translate('workspace.categories.enableCategory')} Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired)} + isOn={policyCategory.enabled} + accessibilityLabel={translate('workspace.categories.enableCategory')} + onToggle={updateWorkspaceRequiresCategory} /> - {policyCategory?.areCommentsRequired && ( - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - )} - + { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); - }} + title={policyCategory.name} + description={translate('common.name')} + onPress={navigateToEditCategory} shouldShowRightIcon /> - {policy?.tax?.trackingEnabled && ( - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - )} - {!policy?.areWorkflowsEnabled && ( - - Go to{' '} - Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} - > - more features - {' '} - and enable workflows, then add approvals to unlock this feature. - - )} - + { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER.getRoute(policyID, policyCategory.name)); + if (!isControlPolicy(policy)) { + Navigation.navigate( + ROUTES.WORKSPACE_UPGRADE.getRoute( + policyID, + CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias, + ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(policyID, policyCategory.name), + ), + ); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(policyID, policyCategory.name)); }} shouldShowRightIcon /> - + { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.getRoute(policyID, policyCategory.name)); + if (!isControlPolicy(policy)) { + Navigation.navigate( + ROUTES.WORKSPACE_UPGRADE.getRoute( + policyID, + CONST.UPGRADE_FEATURE_INTRO_MAPPING.glAndPayrollCodes.alias, + ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(policyID, policyCategory.name), + ), + ); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_PAYROLL_CODE.getRoute(policyID, policyCategory.name)); }} shouldShowRightIcon /> - - )} - {!isThereAnyAccountingConnection && ( - setDeleteCategoryConfirmModalVisible(true)} - /> - )} - + {policy?.areRulesEnabled && ( + <> + + {translate('workspace.rules.categoryRules.title')} + + + + + {translate('workspace.rules.categoryRules.requireDescription')} + Category.setPolicyCategoryDescriptionRequired(policyID, categoryName, !areCommentsRequired)} + /> + + + + {policyCategory?.areCommentsRequired && ( + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DESCRIPTION_HINT.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + )} + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + {policy?.tax?.trackingEnabled && ( + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + )} + {!policy?.areWorkflowsEnabled && ( + + {translate('workspace.rules.categoryRules.goTo')}{' '} + Navigation.navigate(ROUTES.WORKSPACE_MORE_FEATURES.getRoute(policyID))} + > + {translate('workspace.common.moreFeatures')} + {' '} + {translate('workspace.rules.categoryRules.andEnableWorkflows')} + + )} + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_FLAG_AMOUNTS_OVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_REQUIRE_RECEIPTS_OVER.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + + )} + + {!isThereAnyAccountingConnection && ( + setDeleteCategoryConfirmModalVisible(true)} + /> + )} + + + )} ); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index bc71e929e951..3d3ab4f4a4a4 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1343,19 +1343,6 @@ type PendingJoinRequestPolicy = { >; }; -type ExpenseRule = { - tax: { - field_id_TAX: { - externalID: string; - }; - }; - /** Set of conditions under which the expense rule should be applied */ - applyWhen: ApplyRulesWhen[]; - - /** An id of the rule */ - id?: string; -}; - /** Data informing when a given rule should be applied */ type ApplyRulesWhen = { /** The condition for applying the rule to the workspace */ @@ -1380,6 +1367,24 @@ type ApprovalRule = { id?: string; }; +/** Expense rule data model */ +type ExpenseRule = { + /** Object containing information about the tax field id and its external identifier */ + tax: { + /** Object wrapping the external tax id */ + // eslint-disable-next-line @typescript-eslint/naming-convention + field_id_TAX: { + /** The external id of the tax field. */ + externalID: string; + }; + }; + /** Set of conditions under which the expense rule should be applied */ + applyWhen: ApplyRulesWhen[]; + + /** An id of the rule */ + id?: string; +}; + /** Model of policy data */ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< { From 8e6aaae5e007a7be922ac8436869b5585cbe7ed3 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 30 Aug 2024 14:27:34 +0200 Subject: [PATCH 15/31] Fix old translation keys and add missing draft forms for new rules pages --- src/ONYXKEYS.ts | 2 ++ src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx | 2 +- src/pages/workspace/categories/CategoryDescriptionHintPage.tsx | 2 +- src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx | 2 +- 4 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 5870c44d1ac5..59dc09b3e81d 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -492,7 +492,9 @@ const ONYXKEYS = { WORKSPACE_CATEGORY_FORM: 'workspaceCategoryForm', WORKSPACE_CATEGORY_FORM_DRAFT: 'workspaceCategoryFormDraft', WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM: 'workspaceCategoryDescriptionHintForm', + WORKSPACE_CATEGORY_DESCRIPTION_HINT_FORM_DRAFT: 'workspaceCategoryDescriptionHintFormDraft', WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM: 'workspaceCategoryFlagAmountsOverForm', + WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM_DRAFT: 'workspaceCategoryFlagAmountsOverFormDraft', WORKSPACE_TAG_FORM: 'workspaceTagForm', WORKSPACE_TAG_FORM_DRAFT: 'workspaceTagFormDraft', WORKSPACE_SETTINGS_FORM_DRAFT: 'workspaceSettingsFormDraft', diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx index 57ba37cadf4b..f617b5cad5d5 100644 --- a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -64,7 +64,7 @@ function CategoryDefaultTaxRatePage({ shouldEnableMaxHeight > Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> Date: Fri, 30 Aug 2024 15:09:13 +0200 Subject: [PATCH 16/31] Add missing docs to new rules fields --- src/pages/workspace/categories/CategoryApproverPage.tsx | 2 +- src/types/onyx/PolicyCategory.ts | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/CategoryApproverPage.tsx b/src/pages/workspace/categories/CategoryApproverPage.tsx index 4754c6645cca..390a577d9cf8 100644 --- a/src/pages/workspace/categories/CategoryApproverPage.tsx +++ b/src/pages/workspace/categories/CategoryApproverPage.tsx @@ -41,7 +41,7 @@ function CategoryApproverPage({ shouldEnableMaxHeight > Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))} /> ; From f13b4bae401ecb61c65088ad23d74d30e052d453 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 30 Aug 2024 15:59:49 +0200 Subject: [PATCH 17/31] Refactor textInputLabel in WorkspaceMembersSelectionList --- src/components/WorkspaceMembersSelectionList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/WorkspaceMembersSelectionList.tsx b/src/components/WorkspaceMembersSelectionList.tsx index f2ce215fc500..0c0dc73af826 100644 --- a/src/components/WorkspaceMembersSelectionList.tsx +++ b/src/components/WorkspaceMembersSelectionList.tsx @@ -100,7 +100,7 @@ function WorkspaceMembersSelectionList({policyID, selectedApprover, setApprover} Date: Fri, 30 Aug 2024 16:10:54 +0200 Subject: [PATCH 18/31] Category rules cr fixes --- src/components/WorkspaceMembersSelectionList.tsx | 12 ++++++------ .../CategoryRequireReceiptsOverPage.tsx | 15 +++++---------- .../ExpenseLimitTypeSelector.tsx | 2 +- 3 files changed, 12 insertions(+), 17 deletions(-) diff --git a/src/components/WorkspaceMembersSelectionList.tsx b/src/components/WorkspaceMembersSelectionList.tsx index 0c0dc73af826..99f949903a2b 100644 --- a/src/components/WorkspaceMembersSelectionList.tsx +++ b/src/components/WorkspaceMembersSelectionList.tsx @@ -16,12 +16,6 @@ import SelectionList from './SelectionList'; import InviteMemberListItem from './SelectionList/InviteMemberListItem'; import type {Section} from './SelectionList/types'; -type WorkspaceMembersSelectionListProps = { - policyID: string; - selectedApprover: string; - setApprover: (email: string) => void; -}; - type SelectionListApprover = { text: string; alternateText: string; @@ -33,6 +27,12 @@ type SelectionListApprover = { }; type ApproverSection = SectionListData>; +type WorkspaceMembersSelectionListProps = { + policyID: string; + selectedApprover: string; + setApprover: (email: string) => void; +}; + function WorkspaceMembersSelectionList({policyID, selectedApprover, setApprover}: WorkspaceMembersSelectionListProps) { const {translate} = useLocalize(); const {didScreenTransitionEnd} = useScreenWrapperTranstionStatus(); diff --git a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx index c4240d7de9c6..fa6b6555ec53 100644 --- a/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryRequireReceiptsOverPage.tsx @@ -33,15 +33,6 @@ function getInitiallyFocusedOptionKey(isAlwaysSelected: boolean, isNeverSelected return CONST.POLICY.REQUIRE_RECEIPTS_OVER_OPTIONS.DEFAULT; } -function performAction(policyID: string, categoryName: string, value: number | null) { - if (typeof value === 'number') { - Category.setPolicyCategoryReceiptsRequired(policyID, categoryName, value); - return; - } - - Category.removePolicyCategoryReceiptsRequired(policyID, categoryName); -} - function CategoryRequireReceiptsOverPage({ route: { params: {policyID, categoryName}, @@ -102,7 +93,11 @@ function CategoryRequireReceiptsOverPage({ sections={[{data: requireReceiptsOverListData}]} ListItem={RadioListItem} onSelectRow={(item) => { - performAction(policyID, categoryName, item.value); + if (typeof item.value === 'number') { + Category.setPolicyCategoryReceiptsRequired(policyID, categoryName, item.value); + } else { + Category.removePolicyCategoryReceiptsRequired(policyID, categoryName); + } Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); }} shouldSingleExecuteRowSelect diff --git a/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx b/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx index 8bc59b4203e8..e6c30a4913e1 100644 --- a/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx +++ b/src/pages/workspace/categories/ExpenseLimitTypeSelector/ExpenseLimitTypeSelector.tsx @@ -41,7 +41,7 @@ function ExpenseLimitTypeSelector({defaultValue, wrapperStyle, label, setNewExpe }; const title = translate(`workspace.rules.categoryRules.expenseLimitTypes.${defaultValue}`); - const descStyle = title.length === 0 ? styles.textNormal : null; + const descStyle = !title ? styles.textNormal : null; return ( From 23ccd4dd7c60d7e5886e4926cae5eb910369eb6a Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Fri, 30 Aug 2024 17:02:16 +0200 Subject: [PATCH 19/31] Add fixes to setting the default tax rate and approver --- src/libs/actions/Policy/Category.ts | 4 ---- src/pages/workspace/categories/CategorySettingsPage.tsx | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index c0576b222307..618118db1f95 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1221,10 +1221,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - rules: { - ...policy?.rules, - expenseRules: updatedExpenseRules, - }, pendingFields: { rules: null, }, diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index ac13884bff66..261040edd49c 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -85,7 +85,7 @@ function CategorySettingsPage({ const approverText = useMemo(() => { const categoryApprover = CategoryUtils.getCategoryApprover(policy?.rules?.approvalRules ?? [], categoryName); return categoryApprover ?? ''; - }, [categoryName, policy]); + }, [categoryName, policy?.rules?.approvalRules]); const defaultTaxRateText = useMemo(() => { const taxID = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName); @@ -101,7 +101,7 @@ function CategorySettingsPage({ } return CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates); - }, [categoryName, policy, translate]); + }, [categoryName, policy?.rules?.expenseRules, policy?.taxRates, translate]); const requireReceiptsOverText = useMemo(() => { if (!policy) { From 444dee155cc8a93906fd2be598fd9318a460a9fb Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 11:27:38 +0200 Subject: [PATCH 20/31] Handle pendingFields in policy.rules --- src/libs/actions/Policy/Category.ts | 36 ++++++++++--------- .../categories/CategorySettingsPage.tsx | 30 ++++++++-------- src/types/onyx/Policy.ts | 4 +-- 3 files changed, 38 insertions(+), 32 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 618118db1f95..efc8a62f7546 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1124,9 +1124,9 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro rules: { ...policy?.rules, approvalRules: updatedApprovalRules, - }, - pendingFields: { - rules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + approvalRules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, }, }, }, @@ -1136,8 +1136,10 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - pendingFields: { - rules: null, + rules: { + pendingFields: { + approvalRules: null, + }, }, }, }, @@ -1150,9 +1152,9 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro rules: { ...policy?.rules, approvalRules, - }, - pendingFields: { - rules: null, + pendingFields: { + approvalRules: null, + }, }, }, }, @@ -1208,9 +1210,9 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str rules: { ...policy?.rules, expenseRules: updatedExpenseRules, - }, - pendingFields: { - rules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + pendingFields: { + expenseRules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, }, }, }, @@ -1221,8 +1223,10 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - pendingFields: { - rules: null, + rules: { + pendingFields: { + expenseRules: null, + }, }, }, }, @@ -1235,9 +1239,9 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str rules: { ...policy?.rules, expenseRules, - }, - pendingFields: { - rules: null, + pendingFields: { + expenseRules: null, + }, }, }, }, diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 261040edd49c..34b8c1bf2675 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -257,25 +257,27 @@ function CategorySettingsPage({ /> )} - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - {policy?.tax?.trackingEnabled && ( + { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); }} shouldShowRightIcon /> + + {policy?.tax?.trackingEnabled && ( + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + )} {!policy?.areWorkflowsEnabled && ( diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 3d3ab4f4a4a4..566921d7fc89 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1523,13 +1523,13 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< taxRates?: TaxRatesWithDefault; /** A set of rules related to the workpsace */ - rules?: { + rules?: OnyxCommon.OnyxValueWithOfflineFeedback<{ /** A set of rules related to the workpsace approvals */ approvalRules?: ApprovalRule[]; /** A set of rules related to the workpsace expenses */ expenseRules?: ExpenseRule[]; - }; + }>; /** ReportID of the admins room for this workspace */ chatReportIDAdmins?: number; From f7a497e917d07541a1e352aef132485d956966c6 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 11:39:57 +0200 Subject: [PATCH 21/31] Fix removing rules --- src/libs/actions/Policy/Category.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index efc8a62f7546..2d8a888ca729 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1093,7 +1093,7 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const approvalRules = policy?.rules?.approvalRules ?? []; const existingCategoryApproverRule = approvalRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); - const updatedApprovalRules: ApprovalRule[] = [...approvalRules]; + let updatedApprovalRules: ApprovalRule[] = [...approvalRules]; let newApprover = approver; if (!existingCategoryApproverRule) { @@ -1108,7 +1108,7 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro ], }); } else if (existingCategoryApproverRule?.approver === approver) { - updatedApprovalRules.filter((rule) => rule.approver === approver); + updatedApprovalRules = updatedApprovalRules.filter((rule) => rule.approver === approver); newApprover = ''; } else { const indexToUpdate = updatedApprovalRules.indexOf(existingCategoryApproverRule); @@ -1174,7 +1174,7 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const expenseRules = policy?.rules?.expenseRules ?? []; const existingCategoryExpenseRule = expenseRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); - const updatedExpenseRules: ExpenseRule[] = [...expenseRules]; + let updatedExpenseRules: ExpenseRule[] = [...expenseRules]; let newTaxID = taxID; if (!existingCategoryExpenseRule) { @@ -1194,7 +1194,7 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str ], }); } else if (existingCategoryExpenseRule?.tax?.field_id_TAX?.externalID === taxID) { - updatedExpenseRules.filter((rule) => rule.tax.field_id_TAX.externalID === taxID); + updatedExpenseRules = updatedExpenseRules.filter((rule) => rule.tax.field_id_TAX.externalID === taxID); newTaxID = ''; } else { const indexToUpdate = updatedExpenseRules.indexOf(existingCategoryExpenseRule); From 2e29d7f6a50f2bf350e280239e914a3705aabcce Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 11:49:46 +0200 Subject: [PATCH 22/31] Fix disabling fields in category settings page --- .../categories/CategorySettingsPage.tsx | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 34b8c1bf2675..c4d4126ed613 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -265,20 +265,9 @@ function CategorySettingsPage({ Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_APPROVER.getRoute(policyID, policyCategory.name)); }} shouldShowRightIcon + disabled={!policy?.areWorkflowsEnabled} /> - {policy?.tax?.trackingEnabled && ( - - { - Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); - }} - shouldShowRightIcon - /> - - )} {!policy?.areWorkflowsEnabled && ( {translate('workspace.rules.categoryRules.goTo')}{' '} @@ -291,6 +280,19 @@ function CategorySettingsPage({ {translate('workspace.rules.categoryRules.andEnableWorkflows')} )} + {policy?.tax?.trackingEnabled && ( + + { + Navigation.navigate(ROUTES.WORSKPACE_CATEGORY_DEFAULT_TAX_RATE.getRoute(policyID, policyCategory.name)); + }} + shouldShowRightIcon + /> + + )} + Date: Mon, 2 Sep 2024 12:07:29 +0200 Subject: [PATCH 23/31] Fix showing default tax rate --- src/libs/CategoryUtils.ts | 11 +++++++++-- .../categories/CategoryDefaultTaxRatePage.tsx | 2 +- .../workspace/categories/CategorySettingsPage.tsx | 2 +- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index bc11937f51f6..cddd6b3c75b9 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -48,8 +48,15 @@ function getCategoryApprover(approvalRules: ApprovalRule[], categoryName: string return approvalRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.approver; } -function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: string) { - return expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID; +function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: string, policyTaxRates?: TaxRatesWithDefault) { + const categoryDefaultTaxRate = expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID; + + // If the default taxRate is not found in expenseRules, use the default value for policy + if (!categoryDefaultTaxRate) { + return policyTaxRates?.defaultExternalID; + } + + return categoryDefaultTaxRate; } export {formatDefaultTaxRateText, formatRequireReceiptsOverText, getCategoryApprover, getCategoryDefaultTaxRate}; diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx index f617b5cad5d5..7bfbadd672cd 100644 --- a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -29,7 +29,7 @@ function CategoryDefaultTaxRatePage({ const {translate} = useLocalize(); const policy = usePolicy(policyID); - const selectedTaxRate = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName); + const selectedTaxRate = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates); const textForDefault = useCallback( (taxID: string, taxRate: TaxRate) => CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates), diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index c4d4126ed613..8b2f45d838a8 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -88,7 +88,7 @@ function CategorySettingsPage({ }, [categoryName, policy?.rules?.approvalRules]); const defaultTaxRateText = useMemo(() => { - const taxID = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName); + const taxID = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates); if (!taxID) { return ''; From 703e1059c4bbf19f784535439b55a2f4fdf6e957 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 12:21:43 +0200 Subject: [PATCH 24/31] Fix setting the default tax rate --- .../workspace/categories/CategoryDefaultTaxRatePage.tsx | 8 +++++++- src/pages/workspace/categories/CategorySettingsPage.tsx | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx index 7bfbadd672cd..16ea5b9bd2a7 100644 --- a/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx +++ b/src/pages/workspace/categories/CategoryDefaultTaxRatePage.tsx @@ -29,7 +29,7 @@ function CategoryDefaultTaxRatePage({ const {translate} = useLocalize(); const policy = usePolicy(policyID); - const selectedTaxRate = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates); + const selectedTaxRate = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates?.defaultExternalID); const textForDefault = useCallback( (taxID: string, taxRate: TaxRate) => CategoryUtils.formatDefaultTaxRateText(translate, taxID, taxRate, policy?.taxRates), @@ -74,6 +74,12 @@ function CategoryDefaultTaxRatePage({ if (!item.keyForList) { return; } + + if (item.keyForList === selectedTaxRate) { + Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName)); + return; + } + Category.setPolicyCategoryTax(policyID, categoryName, item.keyForList); Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); }} diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index 8b2f45d838a8..0cac6b8a7bda 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -88,7 +88,7 @@ function CategorySettingsPage({ }, [categoryName, policy?.rules?.approvalRules]); const defaultTaxRateText = useMemo(() => { - const taxID = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates); + const taxID = CategoryUtils.getCategoryDefaultTaxRate(policy?.rules?.expenseRules ?? [], categoryName, policy?.taxRates?.defaultExternalID); if (!taxID) { return ''; From b0834d34fce64705b431149d00208e2b04181f70 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 12:27:49 +0200 Subject: [PATCH 25/31] Fix getCategoryDefaultTaxRate types --- src/libs/CategoryUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/CategoryUtils.ts b/src/libs/CategoryUtils.ts index cddd6b3c75b9..7f971f37d3fa 100644 --- a/src/libs/CategoryUtils.ts +++ b/src/libs/CategoryUtils.ts @@ -48,12 +48,12 @@ function getCategoryApprover(approvalRules: ApprovalRule[], categoryName: string return approvalRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.approver; } -function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: string, policyTaxRates?: TaxRatesWithDefault) { +function getCategoryDefaultTaxRate(expenseRules: ExpenseRule[], categoryName: string, defaultTaxRate?: string) { const categoryDefaultTaxRate = expenseRules?.find((rule) => rule.applyWhen.some((when) => when.value === categoryName))?.tax?.field_id_TAX?.externalID; // If the default taxRate is not found in expenseRules, use the default value for policy if (!categoryDefaultTaxRate) { - return policyTaxRates?.defaultExternalID; + return defaultTaxRate; } return categoryDefaultTaxRate; From b6f62d03484cbdd7f583ef2ead816f0d1d72b38b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 13:14:39 +0200 Subject: [PATCH 26/31] Cleanup category actions --- src/libs/actions/Policy/Category.ts | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 2d8a888ca729..ff05e6e66e95 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1122,7 +1122,6 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { rules: { - ...policy?.rules, approvalRules: updatedApprovalRules, pendingFields: { approvalRules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -1150,7 +1149,6 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { rules: { - ...policy?.rules, approvalRules, pendingFields: { approvalRules: null, @@ -1174,8 +1172,7 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const expenseRules = policy?.rules?.expenseRules ?? []; const existingCategoryExpenseRule = expenseRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); - let updatedExpenseRules: ExpenseRule[] = [...expenseRules]; - let newTaxID = taxID; + const updatedExpenseRules: ExpenseRule[] = [...expenseRules]; if (!existingCategoryExpenseRule) { updatedExpenseRules.push({ @@ -1193,9 +1190,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str }, ], }); - } else if (existingCategoryExpenseRule?.tax?.field_id_TAX?.externalID === taxID) { - updatedExpenseRules = updatedExpenseRules.filter((rule) => rule.tax.field_id_TAX.externalID === taxID); - newTaxID = ''; } else { const indexToUpdate = updatedExpenseRules.indexOf(existingCategoryExpenseRule); updatedExpenseRules[indexToUpdate].tax.field_id_TAX.externalID = taxID; @@ -1208,7 +1202,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { rules: { - ...policy?.rules, expenseRules: updatedExpenseRules, pendingFields: { expenseRules: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -1221,7 +1214,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - value: { rules: { pendingFields: { @@ -1237,7 +1229,6 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { rules: { - ...policy?.rules, expenseRules, pendingFields: { expenseRules: null, @@ -1251,7 +1242,7 @@ function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: str const parameters: SetPolicyCategoryTaxParams = { policyID, categoryName, - taxID: newTaxID, + taxID, }; API.write(WRITE_COMMANDS.SET_POLICY_CATEGORY_TAX, parameters, onyxData); From 82dc46379cf2798e5ae87c7790a2fbb7bdaf3cca Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 13:31:23 +0200 Subject: [PATCH 27/31] Update category rules es translations --- src/languages/es.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 8fba2f6457bb..cbe184b298c6 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3699,20 +3699,20 @@ export default { requireDescription: 'Requerir descripción', descriptionHint: 'Sugerencia de descripción', descriptionHintDescription: (categoryName: string) => - `Recuerda a los empleados proporcionar información adicional para el gasto en “${categoryName}”. Esta sugerencia aparece en el campo de descripción en los gastos.`, + `Recuerda a los empleados que deben proporcionar información adicional para los gastos de “${categoryName}”. Esta sugerencia aparece en el campo de descripción en los gastos.`, descriptionHintLabel: 'Sugerencia', descriptionHintSubtitle: 'Consejo: ¡Cuanto más corta, mejor!', - maxAmount: 'Monto máximo', - flagAmountsOver: 'Marcar montos superiores a', + maxAmount: 'Importe máximo', + flagAmountsOver: 'Señala importes superiores a', flagAmountsOverDescription: (categoryName: string) => `Aplica a la categoría “${categoryName}”.`, - flagAmountsOverSubtitle: 'Esto anula el monto máximo para todos los gastos.', + flagAmountsOverSubtitle: 'Esto anula el importe máximo para todos los gastos.', expenseLimitTypes: { expense: 'Gasto individual', - expenseSubtitle: 'Marcar montos de gastos por categoría. Esta regla anula la regla general del espacio de trabajo para el monto máximo de gasto.', + expenseSubtitle: 'Señala importes de gastos por categoría. Esta regla anula la regla general del espacio de trabajo para el importe máximo de gastos.', daily: 'Total por categoría', dailySubtitle: 'Marcar el gasto total por categoría en cada informe de gastos.', }, - requireReceiptsOver: 'Requerir recibos para montos superiores a', + requireReceiptsOver: 'Requerir recibos para importes superiores a', requireReceiptsOverList: { default: (defaultAmount: string) => `${defaultAmount} ${CONST.DOT_SEPARATOR} Predeterminado`, never: 'Nunca requerir recibos', @@ -3720,7 +3720,7 @@ export default { }, defaultTaxRate: 'Tasa de impuesto predeterminada', goTo: 'Ve a', - andEnableWorkflows: 'y habilita flujos de trabajo, luego agrega aprobaciones para desbloquear esta función.', + andEnableWorkflows: 'y habilita los flujos de trabajo, luego añade aprobaciones para desbloquear esta función.', }, }, }, From 7086acb65f8e0ab939f08fe45e7b5c089686f074 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 16:25:58 +0200 Subject: [PATCH 28/31] Fix cloning approval and expense rules --- src/libs/actions/Policy/Category.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index ff05e6e66e95..78a59f99428a 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1,3 +1,4 @@ +import lodashCloneDeep from 'lodash/cloneDeep'; import lodashUnion from 'lodash/union'; import type {NullishDeep, OnyxCollection, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; @@ -1092,8 +1093,8 @@ function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxE function setPolicyCategoryApprover(policyID: string, categoryName: string, approver: string) { const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const approvalRules = policy?.rules?.approvalRules ?? []; - const existingCategoryApproverRule = approvalRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); - let updatedApprovalRules: ApprovalRule[] = [...approvalRules]; + let updatedApprovalRules: ApprovalRule[] = lodashCloneDeep(approvalRules); + const existingCategoryApproverRule = updatedApprovalRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); let newApprover = approver; if (!existingCategoryApproverRule) { @@ -1171,8 +1172,8 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro function setPolicyCategoryTax(policyID: string, categoryName: string, taxID: string) { const policy = allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]; const expenseRules = policy?.rules?.expenseRules ?? []; - const existingCategoryExpenseRule = expenseRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); - const updatedExpenseRules: ExpenseRule[] = [...expenseRules]; + const updatedExpenseRules: ExpenseRule[] = lodashCloneDeep(expenseRules); + const existingCategoryExpenseRule = updatedExpenseRules.find((rule) => rule.applyWhen.some((when) => when.value === categoryName)); if (!existingCategoryExpenseRule) { updatedExpenseRules.push({ From dbffc752e95983f1de07243e13d6ff0757dc7726 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 17:34:33 +0200 Subject: [PATCH 29/31] Fix navigating back from CategoryFlagAmountsOverPage --- src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx index 299742d87721..1db409c9aaef 100644 --- a/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx +++ b/src/pages/workspace/categories/CategoryFlagAmountsOverPage.tsx @@ -68,7 +68,7 @@ function CategoryFlagAmountsOverPage({ formID={ONYXKEYS.FORMS.WORKSPACE_CATEGORY_FLAG_AMOUNTS_OVER_FORM} onSubmit={({maxExpenseAmount}) => { Category.setPolicyCategoryMaxAmount(policyID, categoryName, maxExpenseAmount, expenseLimitType); - Navigation.setNavigationActionToMicrotaskQueue(Navigation.goBack); + Navigation.setNavigationActionToMicrotaskQueue(() => Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(policyID, categoryName))); }} submitButtonText={translate('workspace.editor.save')} enabledWhenOffline From 6d6e9d596440e2c68f87662890397d71ac690f49 Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Mon, 2 Sep 2024 17:41:42 +0200 Subject: [PATCH 30/31] Fix removing category approver --- src/libs/actions/Policy/Category.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 78a59f99428a..ede0be488ee3 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1109,7 +1109,7 @@ function setPolicyCategoryApprover(policyID: string, categoryName: string, appro ], }); } else if (existingCategoryApproverRule?.approver === approver) { - updatedApprovalRules = updatedApprovalRules.filter((rule) => rule.approver === approver); + updatedApprovalRules = updatedApprovalRules.filter((rule) => rule.approver !== approver); newApprover = ''; } else { const indexToUpdate = updatedApprovalRules.indexOf(existingCategoryApproverRule); From 1145150e106822eae87d42990894d6a35007af1b Mon Sep 17 00:00:00 2001 From: Wojciech Boman Date: Tue, 3 Sep 2024 08:38:26 +0200 Subject: [PATCH 31/31] Refactor category actions connected with rules --- src/libs/actions/Policy/Category.ts | 38 +++++++++++++---------------- 1 file changed, 17 insertions(+), 21 deletions(-) diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index ede0be488ee3..04daffada0c2 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -271,7 +271,10 @@ function setWorkspaceCategoryEnabled(policyID: string, categoriesToUpdate: Recor } function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: string, areCommentsRequired: boolean) { - const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]; + const originalAreCommentsRequired = policyCategoryToUpdate?.areCommentsRequired; + const originalCommentHint = policyCategoryToUpdate?.commentHint; + // When areCommentsRequired is set to false, commentHint has to be reset const updatedCommentHint = areCommentsRequired ? allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]?.commentHint : ''; @@ -282,7 +285,6 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: { areCommentsRequired: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -299,7 +301,6 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: null, pendingFields: { areCommentsRequired: null, @@ -316,12 +317,13 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), pendingAction: null, pendingFields: { areCommentsRequired: null, }, + areCommentsRequired: originalAreCommentsRequired, + commentHint: originalCommentHint, }, }, }, @@ -338,7 +340,7 @@ function setPolicyCategoryDescriptionRequired(policyID: string, categoryName: st } function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: string, maxExpenseAmountNoReceipt: number) { - const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + const originalMaxExpenseAmountNoReceipt = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]?.maxExpenseAmountNoReceipt; const onyxData: OnyxData = { optimisticData: [ @@ -347,7 +349,6 @@ function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: strin key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: { maxExpenseAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -363,7 +364,6 @@ function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: strin key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: null, pendingFields: { maxExpenseAmountNoReceipt: null, @@ -379,12 +379,12 @@ function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: strin key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), pendingAction: null, pendingFields: { maxExpenseAmountNoReceipt: null, }, + maxExpenseAmountNoReceipt: originalMaxExpenseAmountNoReceipt, }, }, }, @@ -401,7 +401,7 @@ function setPolicyCategoryReceiptsRequired(policyID: string, categoryName: strin } function removePolicyCategoryReceiptsRequired(policyID: string, categoryName: string) { - const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + const originalMaxExpenseAmountNoReceipt = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]?.maxExpenseAmountNoReceipt; const onyxData: OnyxData = { optimisticData: [ @@ -410,7 +410,6 @@ function removePolicyCategoryReceiptsRequired(policyID: string, categoryName: st key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: { maxExpenseAmountNoReceipt: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -426,7 +425,6 @@ function removePolicyCategoryReceiptsRequired(policyID: string, categoryName: st key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: null, pendingFields: { maxExpenseAmountNoReceipt: null, @@ -442,12 +440,12 @@ function removePolicyCategoryReceiptsRequired(policyID: string, categoryName: st key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), pendingAction: null, pendingFields: { maxExpenseAmountNoReceipt: null, }, + maxExpenseAmountNoReceipt: originalMaxExpenseAmountNoReceipt, }, }, }, @@ -957,7 +955,7 @@ function setPolicyDistanceRatesDefaultCategory(policyID: string, currentCustomUn } function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) { - const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + const originalCommentHint = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]?.commentHint; const onyxData: OnyxData = { optimisticData: [ @@ -966,7 +964,6 @@ function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: str key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: { commentHint: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -982,12 +979,10 @@ function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: str key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: null, pendingFields: { commentHint: null, }, - commentHint, }, }, @@ -999,12 +994,12 @@ function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: str key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), pendingAction: null, pendingFields: { commentHint: null, }, + commentHint: originalCommentHint, }, }, }, @@ -1021,7 +1016,9 @@ function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: str } function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxExpenseAmount: string, expenseLimitType: PolicyCategoryExpenseLimitType) { - const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName] ?? {}; + const policyCategoryToUpdate = allPolicyCategories?.[`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`]?.[categoryName]; + const originalMaxExpenseAmount = policyCategoryToUpdate?.maxExpenseAmount; + const originalExpenseLimitType = policyCategoryToUpdate?.expenseLimitType; const parsedMaxExpenseAmount = maxExpenseAmount === '' ? null : CurrencyUtils.convertToBackendAmount(parseFloat(maxExpenseAmount)); const onyxData: OnyxData = { @@ -1031,7 +1028,6 @@ function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxE key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, pendingFields: { maxExpenseAmount: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, @@ -1049,7 +1045,6 @@ function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxE key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, pendingAction: null, pendingFields: { maxExpenseAmount: null, @@ -1067,13 +1062,14 @@ function setPolicyCategoryMaxAmount(policyID: string, categoryName: string, maxE key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, value: { [categoryName]: { - ...policyCategoryToUpdate, errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), pendingAction: null, pendingFields: { maxExpenseAmount: null, expenseLimitType: null, }, + maxExpenseAmount: originalMaxExpenseAmount, + expenseLimitType: originalExpenseLimitType, }, }, },