diff --git a/src/CONST.ts b/src/CONST.ts index 50df9118a74e..a4e8c9585809 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2145,6 +2145,7 @@ const CONST = { ACCESS_VARIANTS: { PAID: 'paid', ADMIN: 'admin', + CONTROL: 'control', }, DEFAULT_MAX_EXPENSE_AGE: 90, DEFAULT_MAX_EXPENSE_AMOUNT: 200000, @@ -5243,6 +5244,7 @@ const CONST = { }, }, + MAX_LENGTH_256: 256, WORKSPACE_CARDS_LIST_LABEL_TYPE: { CURRENT_BALANCE: 'currentBalance', REMAINING_LIMIT: 'remainingLimit', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 2189522e45ea..a5533e29b450 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -704,6 +704,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/categories/:categoryName/edit', getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/edit` as const, }, + WORKSPACE_CATEGORY_GL_CODE: { + route: 'settings/workspaces/:policyID/categories/:categoryName/gl-code', + getRoute: (policyID: string, categoryName: string) => `settings/workspaces/${policyID}/categories/${encodeURIComponent(categoryName)}/gl-code` 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 0768ca8bb291..d1dd3c6e0eec 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -384,6 +384,7 @@ const SCREENS = { NAME: 'Workspace_Profile_Name', CATEGORY_CREATE: 'Category_Create', CATEGORY_EDIT: 'Category_Edit', + CATEGORY_GL_CODE: 'Category_GL_Code', CATEGORY_SETTINGS: 'Category_Settings', CATEGORIES_SETTINGS: 'Categories_Settings', MORE_FEATURES: 'Workspace_More_Features', diff --git a/src/languages/en.ts b/src/languages/en.ts index c7b5125d02fa..e92262511816 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2666,6 +2666,8 @@ export default { existingCategoryError: 'A category with this name already exists.', invalidCategoryName: 'Invalid category name.', importedFromAccountingSoftware: 'The categories below are imported from your', + glCode: 'GL code', + updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', }, moreFeatures: { spendSection: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 075903d0f324..87046cbce968 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2712,6 +2712,8 @@ export default { existingCategoryError: 'Ya existe una categoría con este nombre.', invalidCategoryName: 'Lo nombre de la categoría es invalido.', importedFromAccountingSoftware: 'Categorías importadas desde', + glCode: 'Código GL', + updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código GL. Inténtelo nuevamente.', }, moreFeatures: { spendSection: { diff --git a/src/libs/API/parameters/UpdatePolicyCategoryGLCodeParams.ts b/src/libs/API/parameters/UpdatePolicyCategoryGLCodeParams.ts new file mode 100644 index 000000000000..f5e4d4ab7eca --- /dev/null +++ b/src/libs/API/parameters/UpdatePolicyCategoryGLCodeParams.ts @@ -0,0 +1,7 @@ +type UpdatePolicyCategoryGLCodeParams = { + policyID: string; + categoryName: string; + glCode: string; +}; + +export default UpdatePolicyCategoryGLCodeParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index ff62d9b69ea6..647ed3807617 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -169,6 +169,7 @@ export type {default as CreateWorkspaceCategoriesParams} from './CreateWorkspace export type {default as RenameWorkspaceCategoriesParams} from './RenameWorkspaceCategoriesParams'; export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspaceRequiresCategoryParams'; export type {default as DeleteWorkspaceCategoriesParams} from './DeleteWorkspaceCategoriesParams'; +export type {default as UpdatePolicyCategoryGLCodeParams} from './UpdatePolicyCategoryGLCodeParams'; export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams'; export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 948ed7f76373..47245b802eb0 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -130,6 +130,7 @@ const WRITE_COMMANDS = { CREATE_POLICY_TAG: 'CreatePolicyTag', RENAME_POLICY_TAG: 'RenamePolicyTag', SET_WORKSPACE_REQUIRES_CATEGORY: 'SetWorkspaceRequiresCategory', + UPDATE_POLICY_CATEGORY_GL_CODE: 'UpdatePolicyCategoryGLCode', DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories', DELETE_POLICY_REPORT_FIELD: 'DeletePolicyReportField', SET_POLICY_TAGS_REQUIRED: 'SetPolicyTagsRequired', @@ -424,6 +425,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; + [WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE]: Parameters.UpdatePolicyCategoryGLCodeParams; [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.DeletePolicyReportField; [WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag; [WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ce43c78a6fee..ed13678887f8 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -237,6 +237,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/members/WorkspaceOwnerChangeErrorPage').default, [SCREENS.WORKSPACE.CATEGORY_CREATE]: () => require('../../../../pages/workspace/categories/CreateCategoryPage').default, [SCREENS.WORKSPACE.CATEGORY_EDIT]: () => require('../../../../pages/workspace/categories/EditCategoryPage').default, + [SCREENS.WORKSPACE.CATEGORY_GL_CODE]: () => require('../../../../pages/workspace/categories/CategoryGLCodePage').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 31b44a2681fd..dfdec34f948b 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -133,7 +133,13 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.TAG_EDIT, SCREENS.WORKSPACE.TAG_LIST_VIEW, ], - [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS, SCREENS.WORKSPACE.CATEGORY_EDIT], + [SCREENS.WORKSPACE.CATEGORIES]: [ + SCREENS.WORKSPACE.CATEGORY_CREATE, + SCREENS.WORKSPACE.CATEGORY_SETTINGS, + SCREENS.WORKSPACE.CATEGORIES_SETTINGS, + SCREENS.WORKSPACE.CATEGORY_EDIT, + SCREENS.WORKSPACE.CATEGORY_GL_CODE, + ], [SCREENS.WORKSPACE.DISTANCE_RATES]: [ SCREENS.WORKSPACE.CREATE_DISTANCE_RATE, SCREENS.WORKSPACE.DISTANCE_RATES_SETTINGS, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 5262850d4e81..cf72710ad96b 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -520,6 +520,12 @@ const config: LinkingOptions['config'] = { categoryName: (categoryName: string) => decodeURIComponent(categoryName), }, }, + [SCREENS.WORKSPACE.CATEGORY_GL_CODE]: { + path: ROUTES.WORKSPACE_CATEGORY_GL_CODE.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 00fd98dc51aa..f618289b3d26 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -201,6 +201,10 @@ type SettingsNavigatorParamList = { categoryName: string; backTo?: Routes; }; + [SCREENS.WORKSPACE.CATEGORY_GL_CODE]: { + policyID: string; + categoryName: string; + }; [SCREENS.WORKSPACE.CATEGORY_SETTINGS]: { policyID: string; categoryName: string; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 140916349c53..cccf477d5550 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -318,6 +318,10 @@ function isPaidGroupPolicy(policy: OnyxEntry): boolean { return policy?.type === CONST.POLICY.TYPE.TEAM || policy?.type === CONST.POLICY.TYPE.CORPORATE; } +function isControlPolicy(policy: OnyxEntry): boolean { + return policy?.type === CONST.POLICY.TYPE.CORPORATE; +} + function isTaxTrackingEnabled(isPolicyExpenseChat: boolean, policy: OnyxEntry, isDistanceRequest: boolean): boolean { const distanceUnit = getCustomUnit(policy); const customUnitID = distanceUnit?.customUnitID ?? 0; @@ -806,6 +810,7 @@ export { getIntegrationLastSuccessfulDate, getCurrentConnectionName, getCustomersOrJobsLabelNetSuite, + isControlPolicy, isNetSuiteCustomSegmentRecord, getNameFromNetSuiteCustomField, isNetSuiteCustomFieldPropertyEditable, diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 4f32eb0b81ba..3af4749569c9 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -2,7 +2,7 @@ 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} from '@libs/API/parameters'; +import type {EnablePolicyCategoriesParams, OpenPolicyCategoriesPageParams, 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'; @@ -331,6 +331,74 @@ function renamePolicyCategory(policyID: string, policyCategory: {oldName: string API.write(WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY, parameters, onyxData); } +function updatePolicyCategoryGLCode(policyID: string, categoryName: string, glCode: 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: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': glCode, + }, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + pendingAction: null, + pendingFields: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': null, + }, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': glCode, + }, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyID}`, + value: { + [categoryName]: { + ...policyCategoryToUpdate, + errors: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('workspace.categories.updateGLCodeFailureMessage'), + pendingAction: null, + pendingFields: { + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': null, + }, + }, + }, + }, + ], + }; + + const parameters: UpdatePolicyCategoryGLCodeParams = { + policyID, + categoryName, + glCode, + }; + + API.write(WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE, parameters, onyxData); +} + function setWorkspaceRequiresCategory(policyID: string, requiresCategory: boolean) { const onyxData: OnyxData = { optimisticData: [ @@ -618,6 +686,7 @@ export { setWorkspaceRequiresCategory, createPolicyCategory, renamePolicyCategory, + updatePolicyCategoryGLCode, clearCategoryErrors, enablePolicyCategories, setPolicyDistanceRatesDefaultCategory, diff --git a/src/pages/workspace/AccessOrNotFoundWrapper.tsx b/src/pages/workspace/AccessOrNotFoundWrapper.tsx index c7f89559fdda..770358335680 100644 --- a/src/pages/workspace/AccessOrNotFoundWrapper.tsx +++ b/src/pages/workspace/AccessOrNotFoundWrapper.tsx @@ -24,6 +24,7 @@ import {isEmptyObject} from '@src/types/utils/EmptyObject'; const ACCESS_VARIANTS = { [CONST.POLICY.ACCESS_VARIANTS.PAID]: (policy: OnyxEntry) => PolicyUtils.isPaidGroupPolicy(policy), [CONST.POLICY.ACCESS_VARIANTS.ADMIN]: (policy: OnyxEntry, login: string) => PolicyUtils.isPolicyAdmin(policy, login), + [CONST.POLICY.ACCESS_VARIANTS.CONTROL]: (policy: OnyxEntry) => PolicyUtils.isControlPolicy(policy), [CONST.IOU.ACCESS_VARIANTS.CREATE]: ( policy: OnyxEntry, login: string, diff --git a/src/pages/workspace/categories/CategoryGLCodePage.tsx b/src/pages/workspace/categories/CategoryGLCodePage.tsx new file mode 100644 index 000000000000..050ced2e497c --- /dev/null +++ b/src/pages/workspace/categories/CategoryGLCodePage.tsx @@ -0,0 +1,87 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +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/WorkspaceCategoryForm'; + +type EditCategoryPageProps = StackScreenProps; + +function CategoryGLCodePage({route}: EditCategoryPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const policyId = route.params.policyID ?? '-1'; + const [policyCategories] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policyId}`); + + const categoryName = route.params.categoryName; + const glCode = policyCategories?.[categoryName]?.['GL Code']; + const {inputCallbackRef} = useAutoFocusInput(); + + const editGLCode = useCallback( + (values: FormOnyxValues) => { + const newGLCode = values.glCode.trim(); + if (newGLCode !== glCode) { + Category.updatePolicyCategoryGLCode(route.params.policyID, categoryName, newGLCode); + } + Navigation.goBack(); + }, + [categoryName, glCode, route.params.policyID], + ); + + return ( + + + Navigation.goBack(ROUTES.WORKSPACE_CATEGORY_SETTINGS.getRoute(route.params.policyID, route.params.categoryName))} + /> + + + + + + ); +} + +CategoryGLCodePage.displayName = 'CategoryGLCodePage'; + +export default CategoryGLCodePage; diff --git a/src/pages/workspace/categories/CategorySettingsPage.tsx b/src/pages/workspace/categories/CategorySettingsPage.tsx index f5e6d3824a5f..54450a1117cd 100644 --- a/src/pages/workspace/categories/CategorySettingsPage.tsx +++ b/src/pages/workspace/categories/CategorySettingsPage.tsx @@ -134,6 +134,14 @@ function CategorySettingsPage({route, policyCategories, navigation}: CategorySet shouldShowRightIcon /> + + Navigation.navigate(ROUTES.WORKSPACE_CATEGORY_GL_CODE.getRoute(route.params.policyID, policyCategory.name))} + shouldShowRightIcon + /> + {!isThereAnyAccountingConnection && ( ; @@ -11,6 +12,7 @@ type WorkspaceCategoryForm = Form< InputID, { [INPUT_IDS.CATEGORY_NAME]: string; + [INPUT_IDS.GL_CODE]: string; } >;