diff --git a/src/ROUTES.ts b/src/ROUTES.ts index d9f0c6658a2b..8de1757fc1b4 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -497,6 +497,10 @@ const ROUTES = { route: 'workspace/:policyID/workflows', getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const, }, + WORKSPACE_WORKFLOWS_PAYER: { + route: 'workspace/:policyID/settings/workflows/payer', + getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/payer` as const, + }, WORKSPACE_WORKFLOWS_APPROVER: { route: 'workspace/:policyID/settings/workflows/approver', getRoute: (policyId: string) => `workspace/${policyId}/settings/workflows/approver` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index a0e06b98da2b..e5cf783b553f 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -220,6 +220,7 @@ const SCREENS = { TAGS_EDIT: 'Tags_Edit', CURRENCY: 'Workspace_Profile_Currency', WORKFLOWS: 'Workspace_Workflows', + WORKFLOWS_PAYER: 'Workspace_Workflows_Payer', WORKFLOWS_APPROVER: 'Workspace_Workflows_Approver', WORKFLOWS_AUTO_REPORTING_FREQUENCY: 'Workspace_Workflows_Auto_Reporting_Frequency', WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET: 'Workspace_Workflows_Auto_Reporting_Monthly_Offset', diff --git a/src/languages/en.ts b/src/languages/en.ts index 5023708c53b7..bd7130000076 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1070,6 +1070,11 @@ export default { }, }, }, + workflowsPayerPage: { + title: 'Authorized payer', + genericErrorMessage: 'The authorized payer could not be changed. Please try again.', + admins: 'Admins', + }, reportFraudPage: { title: 'Report virtual card fraud', description: 'If your virtual card details have been stolen or compromised, we’ll permanently deactivate your existing card and provide you with a new virtual card and number.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 0a5ff9d539d9..e16b66aab8ca 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1066,6 +1066,11 @@ export default { }, }, }, + workflowsPayerPage: { + title: 'Pagador autorizado', + genericErrorMessage: 'El pagador autorizado no se pudo cambiar. Por favor, inténtalo mas tarde.', + admins: 'Administradores', + }, reportFraudPage: { title: 'Reportar fraude con la tarjeta virtual', description: diff --git a/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts new file mode 100644 index 000000000000..eea0788b3927 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyWorkflowsPageParams.ts @@ -0,0 +1,5 @@ +type OpenPolicyWorkflowsPageParams = { + policyID: string; +}; + +export default OpenPolicyWorkflowsPageParams; diff --git a/src/libs/API/parameters/SetWorkspacePayerParams.ts b/src/libs/API/parameters/SetWorkspacePayerParams.ts new file mode 100644 index 000000000000..d1c976c31dd3 --- /dev/null +++ b/src/libs/API/parameters/SetWorkspacePayerParams.ts @@ -0,0 +1,6 @@ +type SetWorkspacePayerParams = { + policyID: string; + reimburserEmail: string; +}; + +export default SetWorkspacePayerParams; diff --git a/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts new file mode 100644 index 000000000000..f96f6385f541 --- /dev/null +++ b/src/libs/API/parameters/SetWorkspaceReimbursementParams.ts @@ -0,0 +1,9 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type SetWorkspaceReimbursementParams = { + policyID: string; + reimbursementChoice: ValueOf; +}; + +export default SetWorkspaceReimbursementParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 7e0e9b6e4a96..f482b4dc79ab 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -156,6 +156,8 @@ export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAut export type {default as SetWorkspaceAutoReportingFrequencyParams} from './SetWorkspaceAutoReportingFrequencyParams'; export type {default as SetWorkspaceAutoReportingMonthlyOffsetParams} from './SetWorkspaceAutoReportingMonthlyOffsetParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; +export type {default as SetWorkspacePayerParams} from './SetWorkspacePayerParams'; +export type {default as SetWorkspaceReimbursementParams} from './SetWorkspaceReimbursementParams'; export type {default as SetPolicyRequiresTag} from './SetPolicyRequiresTag'; export type {default as RenamePolicyTaglist} from './RenamePolicyTaglist'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; @@ -169,4 +171,5 @@ export type {default as EnablePolicyReportFieldsParams} from './EnablePolicyRepo export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest'; export type {default as DeclineJoinRequestParams} from './DeclineJoinRequest'; export type {default as JoinPolicyInviteLinkParams} from './JoinPolicyInviteLink'; +export type {default as OpenPolicyWorkflowsPageParams} from './OpenPolicyWorkflowsPageParams'; export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index bf215e09e37d..9392c458baf4 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -12,6 +12,8 @@ const WRITE_COMMANDS = { SET_WORKSPACE_AUTO_REPORTING_FREQUENCY: 'SetWorkspaceAutoReportingFrequency', SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET: 'UpdatePolicy', SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode', + SET_WORKSPACE_PAYER: 'SetWorkspacePayer', + SET_WORKSPACE_REIMBURSEMENT: 'SetWorkspaceReimbursement', DISMISS_REFERRAL_BANNER: 'DismissReferralBanner', UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale', OPEN_APP: 'OpenApp', @@ -325,6 +327,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_FREQUENCY]: Parameters.SetWorkspaceAutoReportingFrequencyParams; [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING_MONTHLY_OFFSET]: Parameters.SetWorkspaceAutoReportingMonthlyOffsetParams; [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; + [WRITE_COMMANDS.SET_WORKSPACE_PAYER]: Parameters.SetWorkspacePayerParams; + [WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT]: Parameters.SetWorkspaceReimbursementParams; [WRITE_COMMANDS.SWITCH_TO_OLD_DOT]: Parameters.SwitchToOldDotParams; [WRITE_COMMANDS.ENABLE_POLICY_CATEGORIES]: Parameters.EnablePolicyCategoriesParams; [WRITE_COMMANDS.ENABLE_POLICY_CONNECTIONS]: Parameters.EnablePolicyConnectionsParams; @@ -370,6 +374,7 @@ const READ_COMMANDS = { OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', + OPEN_POLICY_WORKFLOWS_PAGE: 'OpenPolicyWorkflowsPage', OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', } as const; @@ -407,6 +412,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams; [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; + [READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE]: Parameters.OpenPolicyWorkflowsPageParams; [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index d56e38564149..3f18e0c3c30c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -245,6 +245,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../pages/workspace/WorkspaceInvitePage').default as React.ComponentType, [SCREENS.WORKSPACE.WORKFLOWS_APPROVER]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsApproverPage').default as React.ComponentType, [SCREENS.WORKSPACE.INVITE_MESSAGE]: () => require('../../../pages/workspace/WorkspaceInviteMessagePage').default as React.ComponentType, + [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: () => require('../../../pages/workspace/workflows/WorkspaceWorkflowsPayerPage').default as React.ComponentType, [SCREENS.WORKSPACE.NAME]: () => require('../../../pages/workspace/WorkspaceNamePage').default as React.ComponentType, [SCREENS.WORKSPACE.DESCRIPTION]: () => require('../../../pages/workspace/WorkspaceProfileDescriptionPage').default as React.ComponentType, [SCREENS.WORKSPACE.SHARE]: () => require('../../../pages/workspace/WorkspaceProfileSharePage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts index 743bf2e0cff1..69c23b2ea07a 100755 --- a/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/CENTRAL_PANE_TO_RHP_MAPPING.ts @@ -5,7 +5,12 @@ const CENTRAL_PANE_TO_RHP_MAPPING: Partial> = [SCREENS.WORKSPACE.PROFILE]: [SCREENS.WORKSPACE.NAME, SCREENS.WORKSPACE.CURRENCY, SCREENS.WORKSPACE.DESCRIPTION, SCREENS.WORKSPACE.SHARE], [SCREENS.WORKSPACE.REIMBURSE]: [SCREENS.WORKSPACE.RATE_AND_UNIT, SCREENS.WORKSPACE.RATE_AND_UNIT_RATE, SCREENS.WORKSPACE.RATE_AND_UNIT_UNIT], [SCREENS.WORKSPACE.MEMBERS]: [SCREENS.WORKSPACE.INVITE, SCREENS.WORKSPACE.INVITE_MESSAGE, SCREENS.WORKSPACE.MEMBER_DETAILS, SCREENS.WORKSPACE.MEMBER_DETAILS_ROLE_SELECTION], - [SCREENS.WORKSPACE.WORKFLOWS]: [SCREENS.WORKSPACE.WORKFLOWS_APPROVER, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET], + [SCREENS.WORKSPACE.WORKFLOWS]: [ + SCREENS.WORKSPACE.WORKFLOWS_APPROVER, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY, + SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET, + SCREENS.WORKSPACE.WORKFLOWS_PAYER, + ], [SCREENS.WORKSPACE.TAGS]: [SCREENS.WORKSPACE.TAGS_SETTINGS, SCREENS.WORKSPACE.TAGS_EDIT], [SCREENS.WORKSPACE.CATEGORIES]: [SCREENS.WORKSPACE.CATEGORY_CREATE, SCREENS.WORKSPACE.CATEGORY_SETTINGS, SCREENS.WORKSPACE.CATEGORIES_SETTINGS], }; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 97d7650a9043..98a32dcd62fc 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -288,6 +288,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES_SETTINGS]: { path: ROUTES.WORKSPACE_CATEGORIES_SETTINGS.route, }, + [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: { + path: ROUTES.WORKSPACE_WORKFLOWS_PAYER.route, + }, [SCREENS.WORKSPACE.MEMBER_DETAILS]: { path: ROUTES.WORKSPACE_MEMBER_DETAILS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 33e79b637cc4..27f434fe10c1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -176,6 +176,9 @@ type SettingsNavigatorParamList = { /** domain passed via route /settings/wallet/card/:domain */ domain: string; }; + [SCREENS.WORKSPACE.WORKFLOWS_PAYER]: { + policyID: string; + }; [SCREENS.SETTINGS.WALLET.TRANSFER_BALANCE]: undefined; [SCREENS.SETTINGS.WALLET.CHOOSE_TRANSFER_ACCOUNT]: undefined; [SCREENS.SETTINGS.WALLET.ENABLE_PAYMENTS]: undefined; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index b55efc2afff1..87918d85b636 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -26,6 +26,7 @@ import type { OpenPolicyCategoriesPageParams, OpenPolicyDistanceRatesPageParams, OpenPolicyTagsPageParams, + OpenPolicyWorkflowsPageParams, OpenWorkspaceInvitePageParams, OpenWorkspaceMembersPageParams, OpenWorkspaceParams, @@ -34,6 +35,8 @@ import type { SetWorkspaceAutoReportingFrequencyParams, SetWorkspaceAutoReportingMonthlyOffsetParams, SetWorkspaceAutoReportingParams, + SetWorkspacePayerParams, + SetWorkspaceReimbursementParams, UpdateWorkspaceAvatarParams, UpdateWorkspaceCustomUnitAndRateParams, UpdateWorkspaceDescriptionParams, @@ -589,6 +592,106 @@ function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMo API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData}); } +function setWorkspacePayer(policyID: string, reimburserEmail: string, reimburserAccountID: number) { + const policy = ReportUtils.getPolicy(policyID); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimburserEmail, + reimburserAccountID, + errorFields: {reimburserEmail: null}, + pendingFields: {reimburserEmail: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + errorFields: {reimburserEmail: null}, + pendingFields: {reimburserEmail: null}, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimburserEmail: policy.reimburserEmail, + reimburserAccountID: policy.reimburserAccountID, + errorFields: {reimburserEmail: ErrorUtils.getMicroSecondOnyxError('workflowsPayerPage.genericErrorMessage')}, + pendingFields: {reimburserEmail: null}, + }, + }, + ]; + + const params: SetWorkspacePayerParams = {policyID, reimburserEmail}; + + API.write(WRITE_COMMANDS.SET_WORKSPACE_PAYER, params, {optimisticData, failureData, successData}); +} + +function clearWorkspacePayerError(policyID: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimburserEmail: null}}); +} + +function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserAccountID: number, reimburserEmail: string) { + const policy = ReportUtils.getPolicy(policyID); + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimbursementChoice, + reimburserAccountID, + reimburserEmail, + errorFields: {reimbursementChoice: null}, + pendingFields: {reimbursementChoice: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + errorFields: {reimbursementChoice: null}, + pendingFields: {reimbursementChoice: null}, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + reimbursementChoice: policy.reimbursementChoice, + reimburserAccountID: policy.reimburserAccountID, + reimburserEmail: policy.reimburserEmail, + errorFields: {reimbursementChoice: ErrorUtils.getMicroSecondOnyxError('common.genericErrorMessage')}, + pendingFields: {reimbursementChoice: null}, + }, + }, + ]; + + const params: SetWorkspaceReimbursementParams = {policyID, reimbursementChoice}; + + API.write(WRITE_COMMANDS.SET_WORKSPACE_REIMBURSEMENT, params, {optimisticData, failureData, successData}); +} + +function clearWorkspaceReimbursementErrors(policyID: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {errorFields: {reimbursementChoice: null}}); +} + /** * Build optimistic data for removing users from the announcement room */ @@ -1887,6 +1990,50 @@ function openWorkspaceReimburseView(policyID: string) { API.read(READ_COMMANDS.OPEN_WORKSPACE_REIMBURSE_VIEW, params, {successData, failureData}); } +function openPolicyWorkflowsPage(policyID: string) { + if (!policyID) { + Log.warn('openPolicyWorkflowsPage invalid params', {policyID}); + return; + } + + const onyxData: OnyxData = { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`, + value: { + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + key: `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${policyID}`, + value: { + isLoading: false, + }, + }, + ], + }; + + const params: OpenPolicyWorkflowsPageParams = {policyID}; + + API.read(READ_COMMANDS.OPEN_POLICY_WORKFLOWS_PAGE, params, onyxData); +} + function setPolicyIDForReimburseView(policyID: string) { Onyx.merge(ONYXKEYS.WORKSPACE_RATE_AND_UNIT, {policyID, rate: null, unit: null}); } @@ -3292,6 +3439,10 @@ export { declineJoinRequest, createPolicyCategory, clearCategoryErrors, + setWorkspacePayer, + clearWorkspacePayerError, + setWorkspaceReimbursement, + openPolicyWorkflowsPage, setPolicyRequiresTag, renamePolicyTaglist, enablePolicyCategories, @@ -3302,4 +3453,5 @@ export { enablePolicyTaxes, enablePolicyWorkflows, openPolicyDistanceRatesPage, + clearWorkspaceReimbursementErrors, }; diff --git a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js index 9855090e70d1..473b36571390 100644 --- a/src/pages/ReimbursementAccount/ReimbursementAccountPage.js +++ b/src/pages/ReimbursementAccount/ReimbursementAccountPage.js @@ -337,9 +337,9 @@ function ReimbursementAccountPage({reimbursementAccount, route, onfidoToken, pol BankAccounts.hideBankAccountErrors(); } - const backTo = lodashGet(route.params, 'backTo'); // eslint-disable-next-line no-shadow const policyID = lodashGet(route.params, 'policyID'); + const backTo = lodashGet(route.params, 'backTo'); Navigation.navigate(ROUTES.BANK_ACCOUNT_WITH_STEP_TO_OPEN.getRoute(getRouteForCurrentStep(currentStep), policyID, backTo)); }, diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 240a148110f7..58738a5c8f1c 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -160,6 +160,7 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r icon: Expensicons.Workflows, action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.WORKFLOWS, + brickRoadIndicator: !isEmptyObject(policy?.errorFields?.reimburserEmail ?? {}) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, }); } diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 32b76ef46202..fb01a858642c 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -81,6 +81,9 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & * taller header on desktop and different font of the title. * */ icon?: IconAsset; + + /** Whether the page is loading, example any other API call in progres */ + isLoading?: boolean; }; function fetchData(policyID: string, skipVBBACal?: boolean) { @@ -110,12 +113,14 @@ function WorkspacePageWithSections({ shouldShowOfflineIndicatorInWideScreen = false, shouldShowNonAdmin = false, shouldShowNotFoundPage = false, + isLoading: isPageLoading = false, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); const policyID = route.params?.policyID ?? ''; useNetwork({onReconnect: () => fetchData(policyID, shouldSkipVBBACall)}); - const isLoading = reimbursementAccount?.isLoading ?? true; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const isLoading = (reimbursementAccount?.isLoading || isPageLoading) ?? true; const achState = reimbursementAccount?.achData?.state ?? ''; const isUsingECard = user?.isUsingExpensifyCard ?? false; const hasVBA = achState === BankAccount.STATE.OPEN; diff --git a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx index c5c4465937b9..da995de1d5d5 100644 --- a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx +++ b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx @@ -5,7 +5,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Switch from '@components/Switch'; import Text from '@components/Text'; import useThemeStyles from '@hooks/useThemeStyles'; -import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import type IconAsset from '@src/types/utils/IconAsset'; type ToggleSettingOptionRowProps = { @@ -23,14 +23,23 @@ type ToggleSettingOptionRowProps = { subMenuItems?: React.ReactNode; /** If there is a pending action, we will grey out the option */ pendingAction?: PendingAction; + /** Any error message to show */ + errors?: Errors; + /** Callback to close the error messages */ + onCloseError?: () => void; }; const ICON_SIZE = 48; -function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction}: ToggleSettingOptionRowProps) { +function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction, errors, onCloseError}: ToggleSettingOptionRowProps) { const styles = useThemeStyles(); return ( - + diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index ee3934cacc06..34446fecdaa2 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -1,17 +1,19 @@ import type {StackScreenProps} from '@react-navigation/stack'; -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useEffect, useMemo} from 'react'; import {FlatList, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import * as Illustrations from '@components/Icon/Illustrations'; import MenuItem from '@components/MenuItem'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; import Section from '@components/Section'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as ErrorUtils from '@libs/ErrorUtils'; +import BankAccount from '@libs/models/BankAccount'; import Navigation from '@libs/Navigation/Navigation'; import Permissions from '@libs/Permissions'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; @@ -21,11 +23,12 @@ import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicy from '@pages/workspace/withPolicy'; import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; import * as Policy from '@userActions/Policy'; +import {navigateToBankAccountRoute} from '@userActions/ReimbursementAccount'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Beta} from '@src/types/onyx'; +import type {Beta, ReimbursementAccount, Session} from '@src/types/onyx'; import ToggleSettingOptionRow from './ToggleSettingsOptionRow'; import type {ToggleSettingOptionRowProps} from './ToggleSettingsOptionRow'; import {getAutoReportingFrequencyDisplayNames} from './WorkspaceAutoReportingFrequencyPage'; @@ -34,25 +37,49 @@ import type {AutoReportingFrequencyKey} from './WorkspaceAutoReportingFrequencyP type WorkspaceWorkflowsPageOnyxProps = { /** Beta features list */ betas: OnyxEntry; + /** Reimbursement account details */ + reimbursementAccount: OnyxEntry; + /** Policy details */ + session: OnyxEntry; }; type WorkspaceWorkflowsPageProps = WithPolicyProps & WorkspaceWorkflowsPageOnyxProps & StackScreenProps; -function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPageProps) { +function WorkspaceWorkflowsPage({policy, betas, route, reimbursementAccount, session}: WorkspaceWorkflowsPageProps) { const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); - const {isOffline} = useNetwork(); const policyApproverEmail = policy?.approver; const policyApproverName = useMemo(() => PersonalDetailsUtils.getPersonalDetailByEmail(policyApproverEmail ?? '')?.displayName ?? policyApproverEmail, [policyApproverEmail]); const containerStyle = useMemo(() => [styles.ph8, styles.mhn8, styles.ml11, styles.pv3, styles.pr0, styles.pl4, styles.mr0, styles.widthAuto, styles.mt4], [styles]); const canUseDelayedSubmission = Permissions.canUseWorkflowsDelayedSubmission(betas); + const displayNameForAuthorizedPayer = useMemo(() => { + const personalDetails = PersonalDetailsUtils.getPersonalDetailsByIDs([policy?.reimburserAccountID ?? 0], session?.accountID ?? 0); + const displayNameFromReimburserEmail = PersonalDetailsUtils.getPersonalDetailByEmail(policy?.reimburserEmail ?? '')?.displayName ?? policy?.reimburserEmail; + return displayNameFromReimburserEmail ?? personalDetails?.[0]?.displayName; + }, [policy?.reimburserAccountID, policy?.reimburserEmail, session?.accountID]); + const onPressAutoReportingFrequency = useCallback(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY.getRoute(policy?.id ?? '')), [policy?.id]); - const items: ToggleSettingOptionRowProps[] = useMemo( - () => [ + const fetchData = () => { + Policy.openPolicyWorkflowsPage(policy?.id ?? route.params.policyID); + }; + + useNetwork({onReconnect: fetchData}); + + useEffect(() => { + fetchData(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const optionItems: ToggleSettingOptionRowProps[] = useMemo(() => { + const {accountNumber, state, bankName} = reimbursementAccount?.achData ?? {}; + const hasVBA = state === BankAccount.STATE.OPEN; + const bankDisplayName = bankName ? `${bankName} ${accountNumber ? `${accountNumber.slice(-5)}` : ''}` : ''; + const hasReimburserEmailError = !!policy?.errorFields?.reimburserEmail; + + return [ ...(canUseDelayedSubmission ? [ { @@ -115,41 +142,71 @@ function WorkspaceWorkflowsPage({policy, betas, route}: WorkspaceWorkflowsPagePr title: translate('workflowsPage.makeOrTrackPaymentsTitle'), subtitle: translate('workflowsPage.makeOrTrackPaymentsDescription'), onToggle: () => { - // TODO will be done in https://github.com/Expensify/Expensify/issues/368335 + const isActive = policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; + const newReimbursementChoice = isActive ? CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_MANUAL : CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES; + const newReimburserAccountID = + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + PersonalDetailsUtils.getPersonalDetailByEmail(policy?.reimburserEmail ?? '')?.accountID || policy?.reimburserAccountID || policy?.ownerAccountID; + const newReimburserEmail = PersonalDetailsUtils.getPersonalDetailsByIDs([newReimburserAccountID ?? 0], session?.accountID ?? 0)?.[0]?.login; + Policy.setWorkspaceReimbursement(policy?.id ?? '', newReimbursementChoice, newReimburserAccountID ?? 0, newReimburserEmail ?? ''); }, subMenuItems: ( - Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_CONNECT_BANK_ACCOUNT.getRoute(route.params.policyID))} - // TODO will be done in https://github.com/Expensify/Expensify/issues/368335 - shouldShowRightIcon - wrapperStyle={containerStyle} - hoverAndPressStyle={[styles.mr0, styles.br2]} - /> + <> + navigateToBankAccountRoute(route.params.policyID, ROUTES.WORKSPACE_WORKFLOWS.getRoute(route.params.policyID))} + shouldShowRightIcon + wrapperStyle={containerStyle} + hoverAndPressStyle={[styles.mr0, styles.br2]} + /> + {hasVBA && ( + Policy.clearWorkspacePayerError(policy?.id ?? '')} + errorRowStyles={[styles.ml7]} + > + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_PAYER.getRoute(route.params.policyID))} + shouldShowRightIcon + wrapperStyle={containerStyle} + hoverAndPressStyle={[styles.mr0, styles.br2]} + brickRoadIndicator={hasReimburserEmailError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} + /> + + )} + ), isEndOptionRow: true, - isActive: false, // TODO will be done in https://github.com/Expensify/Expensify/issues/368335 + isActive: policy?.reimbursementChoice === CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + pendingAction: policy?.pendingFields?.reimbursementChoice, + errors: ErrorUtils.getLatestErrorField(policy ?? {}, 'reimbursementChoice'), + onCloseError: () => Policy.clearWorkspaceReimbursementErrors(policy?.id ?? ''), }, - ], - [ - policyApproverName, - policy, - route.params.policyID, - styles, - translate, - containerStyle, - isOffline, - StyleUtils, - onPressAutoReportingFrequency, - preferredLocale, - canUseDelayedSubmission, - ], - ); + ]; + }, [ + policy, + route.params.policyID, + styles, + translate, + policyApproverName, + containerStyle, + onPressAutoReportingFrequency, + preferredLocale, + canUseDelayedSubmission, + reimbursementAccount?.achData, + displayNameForAuthorizedPayer, + session?.accountID, + ]); - const renderItem = ({item}: {item: ToggleSettingOptionRowProps}) => ( + const renderOptionItem = ({item}: {item: ToggleSettingOptionRowProps}) => ( ); const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const isLoading = reimbursementAccount?.isLoading ?? true; return (
{translate('workflowsPage.workflowDescription')} item.title} /> @@ -202,5 +264,12 @@ export default withPolicy( betas: { key: ONYXKEYS.BETAS, }, + reimbursementAccount: { + // @ts-expect-error: ONYXKEYS.REIMBURSEMENT_ACCOUNT is conflicting with ONYXKEYS.FORMS.REIMBURSEMENT_ACCOUNT_FORM + key: ({route}) => `${ONYXKEYS.REIMBURSEMENT_ACCOUNT}${route.params.policyID}`, + }, + session: { + key: ONYXKEYS.SESSION, + }, })(WorkspaceWorkflowsPage), ); diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx new file mode 100644 index 000000000000..9934de95878b --- /dev/null +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPayerPage.tsx @@ -0,0 +1,238 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useMemo, useState} from 'react'; +import type {SectionListData} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Badge from '@components/Badge'; +import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import type {ListItem, Section} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import compose from '@libs/compose'; +import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; +import Log from '@libs/Log'; +import Navigation from '@libs/Navigation/Navigation'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as UserUtils from '@libs/UserUtils'; +import type {SettingsNavigatorParamList} from '@navigation/types'; +import AdminPolicyAccessOrNotFoundWrapper from '@pages/workspace/AdminPolicyAccessOrNotFoundWrapper'; +import PaidPolicyAccessOrNotFoundWrapper from '@pages/workspace/PaidPolicyAccessOrNotFoundWrapper'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetailsList, PolicyMember} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type WorkspaceWorkflowsPayerPageOnyxProps = { + /** All of the personal details for everyone */ + personalDetails: OnyxEntry; +}; + +type WorkspaceWorkflowsPayerPageProps = WorkspaceWorkflowsPayerPageOnyxProps & + WithPolicyAndFullscreenLoadingProps & + StackScreenProps; +type MemberOption = Omit & {accountID: number}; +type MembersSection = SectionListData>; + +function WorkspaceWorkflowsPayerPage({route, policy, policyMembers, personalDetails, isLoadingReportData = true}: WorkspaceWorkflowsPayerPageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const policyName = policy?.name ?? ''; + const {isOffline} = useNetwork(); + + const [searchTerm, setSearchTerm] = useState(''); + + const isDeletedPolicyMember = useCallback( + (policyMember: PolicyMember) => !isOffline && policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && isEmptyObject(policyMember.errors), + [isOffline], + ); + + const [formattedPolicyAdmins, formattedAuthorizedPayer] = useMemo(() => { + const policyAdminDetails: MemberOption[] = []; + const authorizedPayerDetails: MemberOption[] = []; + + Object.entries(policyMembers ?? {}).forEach(([accountIDKey, policyMember]) => { + const accountID = Number(accountIDKey); + const details = personalDetails?.[accountID]; + if (!details) { + Log.hmmm(`[WorkspaceMembersPage] no personal details found for policy member with accountID: ${accountID}`); + return; + } + + const isOwner = policy?.owner === details?.login; + const isAdmin = policyMember.role === CONST.POLICY.ROLE.ADMIN; + const shouldSkipMember = isDeletedPolicyMember(policyMember) || PolicyUtils.isExpensifyTeam(details?.login) || (!isOwner && !isAdmin); + + if (shouldSkipMember) { + return; + } + + const roleBadge = ( + + ); + + const isAuthorizedPayer = policy?.reimburserEmail === details?.login ?? policy?.reimburserAccountID === accountID; + + const formattedMember = { + keyForList: accountIDKey, + accountID, + isSelected: isAuthorizedPayer, + isDisabled: policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !isEmptyObject(policyMember.errors), + text: formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), + alternateText: formatPhoneNumber(details?.login ?? ''), + rightElement: roleBadge, + icons: [ + { + source: UserUtils.getAvatar(details?.avatar, accountID), + name: formatPhoneNumber(details?.login ?? ''), + type: CONST.ICON_TYPE_AVATAR, + id: accountID, + }, + ], + errors: policyMember.errors, + pendingAction: policyMember.pendingAction ?? isAuthorizedPayer ? policy?.pendingFields?.reimburserEmail : null, + }; + + if (policy?.reimburserEmail === details?.login ?? policy?.reimburserAccountID === accountID) { + authorizedPayerDetails.push(formattedMember); + } else { + policyAdminDetails.push(formattedMember); + } + }); + return [policyAdminDetails, authorizedPayerDetails]; + }, [ + personalDetails, + policyMembers, + translate, + policy?.reimburserEmail, + isDeletedPolicyMember, + policy?.owner, + styles, + StyleUtils, + policy?.reimburserAccountID, + policy?.pendingFields?.reimburserEmail, + ]); + + const sections: MembersSection[] = useMemo(() => { + const sectionsArray: MembersSection[] = []; + + if (searchTerm !== '') { + const filteredOptions = [...formattedPolicyAdmins, ...formattedAuthorizedPayer].filter((option) => { + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(searchTerm); + return !!option.text?.toLowerCase().includes(searchValue) || !!option.login?.toLowerCase().includes(searchValue); + }); + return [ + { + title: undefined, + data: filteredOptions, + shouldShow: true, + }, + ]; + } + + sectionsArray.push({ + data: formattedAuthorizedPayer, + shouldShow: true, + indexOffset: formattedPolicyAdmins.length, + }); + + sectionsArray.push({ + title: translate('workflowsPayerPage.admins'), + data: formattedPolicyAdmins, + shouldShow: true, + indexOffset: 0, + }); + return sectionsArray; + }, [formattedPolicyAdmins, formattedAuthorizedPayer, translate, searchTerm]); + + const headerMessage = useMemo( + () => (searchTerm && !sections[0].data.length ? translate('common.noResultsFound') : ''), + + // eslint-disable-next-line react-hooks/exhaustive-deps + [translate, sections], + ); + + const setPolicyAuthorizedPayer = (member: MemberOption) => { + const authorizedPayerEmail = personalDetails?.[member.accountID]?.login ?? ''; + if (policy?.reimburserEmail === authorizedPayerEmail) { + return; + } + + if (policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES) { + return; + } + const authorizedPayerAccountID = member.accountID; + Policy.setWorkspacePayer(policy?.id ?? '', authorizedPayerEmail, authorizedPayerAccountID); + Navigation.goBack(); + }; + + // eslint-disable-next-line rulesdir/no-negated-variables + const shouldShowNotFoundPage = useMemo( + () => + (isEmptyObject(policy) && !isLoadingReportData) || + PolicyUtils.isPendingDeletePolicy(policy) || + policy?.reimbursementChoice !== CONST.POLICY.REIMBURSEMENT_CHOICES.REIMBURSEMENT_YES, + [policy, isLoadingReportData], + ); + + return ( + + + + + + + + + + + ); +} + +WorkspaceWorkflowsPayerPage.displayName = 'WorkspaceWorkflowsPayerPage'; + +export default compose( + withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + }), + withPolicyAndFullscreenLoading, +)(WorkspaceWorkflowsPayerPage); diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index 4f4ea821e794..56000a851e7b 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -167,6 +167,10 @@ export default { marginLeft: 24, }, + ml7: { + marginLeft: 28, + }, + ml8: { marginLeft: 32, }, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index cf959194be5f..ed30f4b7d201 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -321,6 +321,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Email of the reimburser when reimbursement is set direct */ reimburserEmail?: string; + /** AccountID of the reimburser when reimbursement is set direct */ + reimburserAccountID?: number; + /** ReportID of the admins room for this workspace */ chatReportIDAdmins?: number;