diff --git a/assets/images/simple-illustrations/simple-illustration__approval.svg b/assets/images/simple-illustrations/simple-illustration__approval.svg new file mode 100644 index 000000000000..bdef2436958b --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__approval.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg b/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg new file mode 100644 index 000000000000..fc7082e9932c --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg @@ -0,0 +1 @@ + diff --git a/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg b/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg new file mode 100644 index 000000000000..33d1fc0fa044 --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__wallet-alt.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/simple-illustrations/simple-illustration__workflows.svg b/assets/images/simple-illustrations/simple-illustration__workflows.svg new file mode 100644 index 000000000000..47d30d54310f --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__workflows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/workflows.svg b/assets/images/workflows.svg new file mode 100644 index 000000000000..24156c66eb69 --- /dev/null +++ b/assets/images/workflows.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/CONST.ts b/src/CONST.ts index e3cf8f6b172e..fcbb97cfdb01 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1558,6 +1558,7 @@ const CONST = { WORKSPACE_INVOICES: 'WorkspaceSendInvoices', WORKSPACE_TRAVEL: 'WorkspaceBookTravel', WORKSPACE_MEMBERS: 'WorkspaceManageMembers', + WORKSPACE_WORKFLOWS: 'WorkspaceWorkflows', WORKSPACE_BANK_ACCOUNT: 'WorkspaceBankAccount', WORKSPACE_SETTINGS: 'WorkspaceSettings', }, diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 080d8cdd5655..11ffd06e0808 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -471,6 +471,10 @@ const ROUTES = { route: 'workspace/:policyID/settings/currency', getRoute: (policyID: string) => `workspace/${policyID}/settings/currency` as const, }, + WORKSPACE_WORKFLOWS: { + route: 'workspace/:policyID/workflows', + getRoute: (policyID: string) => `workspace/${policyID}/workflows` as const, + }, WORKSPACE_CARD: { route: 'workspace/:policyID/card', getRoute: (policyID: string) => `workspace/${policyID}/card` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 50ced3ff256a..cdc22e9be69e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -209,6 +209,7 @@ const SCREENS = { INVITE_MESSAGE: 'Workspace_Invite_Message', CATEGORIES: 'Workspace_Categories', CURRENCY: 'Workspace_Profile_Currency', + WORKFLOWS: 'Workspace_Workflows', DESCRIPTION: 'Workspace_Profile_Description', SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', diff --git a/src/components/Icon/Expensicons.ts b/src/components/Icon/Expensicons.ts index 6a2fb1c6b1f6..2a7ed30abf1a 100644 --- a/src/components/Icon/Expensicons.ts +++ b/src/components/Icon/Expensicons.ts @@ -145,6 +145,7 @@ import Users from '@assets/images/users.svg'; import VolumeHigh from '@assets/images/volume-high.svg'; import VolumeLow from '@assets/images/volume-low.svg'; import Wallet from '@assets/images/wallet.svg'; +import Workflows from '@assets/images/workflows.svg'; import Workspace from '@assets/images/workspace-default-avatar.svg'; import Wrench from '@assets/images/wrench.svg'; import Zoom from '@assets/images/zoom.svg'; @@ -289,6 +290,7 @@ export { VolumeHigh, VolumeLow, Wallet, + Workflows, Workspace, Zoom, Twitter, diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 299b694df3f2..9caa52bcc3bc 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -27,6 +27,7 @@ import TadaBlue from '@assets/images/product-illustrations/tada--blue.svg'; import TadaYellow from '@assets/images/product-illustrations/tada--yellow.svg'; import TeleScope from '@assets/images/product-illustrations/telescope.svg'; import ToddBehindCloud from '@assets/images/product-illustrations/todd-behind-cloud.svg'; +import Approval from '@assets/images/simple-illustrations/simple-illustration__approval.svg'; import BankArrow from '@assets/images/simple-illustrations/simple-illustration__bank-arrow.svg'; import BigRocket from '@assets/images/simple-illustrations/simple-illustration__bigrocket.svg'; import PinkBill from '@assets/images/simple-illustrations/simple-illustration__bill.svg'; @@ -57,6 +58,7 @@ import OpenSafe from '@assets/images/simple-illustrations/simple-illustration__o import PalmTree from '@assets/images/simple-illustrations/simple-illustration__palmtree.svg'; import Profile from '@assets/images/simple-illustrations/simple-illustration__profile.svg'; import QRCode from '@assets/images/simple-illustrations/simple-illustration__qr-code.svg'; +import ReceiptEnvelope from '@assets/images/simple-illustrations/simple-illustration__receipt-envelope.svg'; import ReceiptWrangler from '@assets/images/simple-illustrations/simple-illustration__receipt-wrangler.svg'; import SanFrancisco from '@assets/images/simple-illustrations/simple-illustration__sanfrancisco.svg'; import ShieldYellow from '@assets/images/simple-illustrations/simple-illustration__shield.svg'; @@ -65,6 +67,8 @@ import ThumbsUpStars from '@assets/images/simple-illustrations/simple-illustrati import TrackShoe from '@assets/images/simple-illustrations/simple-illustration__track-shoe.svg'; import TrashCan from '@assets/images/simple-illustrations/simple-illustration__trashcan.svg'; import TreasureChest from '@assets/images/simple-illustrations/simple-illustration__treasurechest.svg'; +import WalletAlt from '@assets/images/simple-illustrations/simple-illustration__wallet-alt.svg'; +import Workflows from '@assets/images/simple-illustrations/simple-illustration__workflows.svg'; export { Abracadabra, @@ -133,5 +137,9 @@ export { LockClosed, Gears, QRCode, + ReceiptEnvelope, + Approval, + WalletAlt, + Workflows, House, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index ffb764b40e6a..1626419985b6 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1030,6 +1030,25 @@ export default { }, cardDetailsLoadingFailure: 'An error occurred while loading the card details. Please check your internet connection and try again.', }, + workflowsPage: { + workflowTitle: 'Spend', + workflowDescription: 'Configure a workflow from the moment spend occurs, including approval and payment.', + delaySubmissionTitle: 'Delay submissions', + delaySubmissionDescription: 'Expenses are shared right away for better spend visibility. Set a slower cadence if needed.', + submissionFrequency: 'Submission frequency', + weeklyFrequency: 'Weekly', + monthlyFrequency: 'Monthly', + twiceAMonthFrequency: 'Twice a month', + byTripFrequency: 'By trip', + manuallyFrequency: 'Manually', + dailyFrequency: 'Daily', + addApprovalsTitle: 'Add approvals', + approver: 'Approver', + connectBankAccount: 'Connect bank account', + addApprovalsDescription: 'Require additional approval before authorizing a payment.', + makeOrTrackPaymentsTitle: 'Make or track payments', + makeOrTrackPaymentsDescription: 'Add an authorized payer for payments made in Expensify, or simply track payments made elsewhere.', + }, 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.', @@ -1683,6 +1702,7 @@ export default { workspace: { common: { card: 'Cards', + workflows: 'Workflows', workspace: 'Workspace', edit: 'Edit workspace', enabled: 'Enabled', diff --git a/src/languages/es.ts b/src/languages/es.ts index b03cbdd3772b..5cf0487eadd5 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1026,6 +1026,25 @@ export default { }, cardDetailsLoadingFailure: 'Se ha producido un error al cargar los datos de la tarjeta. Comprueba tu conexión a Internet e inténtalo de nuevo.', }, + workflowsPage: { + workflowTitle: 'Gasto', + workflowDescription: 'Configure un flujo de trabajo desde el momento en que se produce el gasto, incluida la aprobación y el pago', + delaySubmissionTitle: 'Retrasar envíos', + delaySubmissionDescription: 'Los gastos se comparten de inmediato para una mejor visibilidad del gasto. Establece una cadencia más lenta si es necesario.', + submissionFrequency: 'Frecuencia de envíos', + weeklyFrequency: 'Semanal', + monthlyFrequency: 'Mensual', + twiceAMonthFrequency: 'Dos veces al mes', + byTripFrequency: 'Por viaje', + manuallyFrequency: 'Manual', + dailyFrequency: 'Diaria', + addApprovalsTitle: 'Requerir aprobaciones', + approver: 'Aprobador', + connectBankAccount: 'Conectar cuenta bancaria', + addApprovalsDescription: 'Requiere una aprobación adicional antes de autorizar un pago.', + makeOrTrackPaymentsTitle: 'Realizar o seguir pagos', + makeOrTrackPaymentsDescription: 'Añade un pagador autorizado para los pagos realizados en Expensify, o simplemente realiza un seguimiento de los pagos realizados en otro lugar.', + }, reportFraudPage: { title: 'Reportar fraude con la tarjeta virtual', description: @@ -1707,6 +1726,7 @@ export default { workspace: { common: { card: 'Tarjetas', + workflows: 'Flujos de trabajo', workspace: 'Espacio de trabajo', edit: 'Editar espacio de trabajo', enabled: 'Activada', diff --git a/src/libs/API/parameters/SetWorkspaceApprovalModeParams.ts b/src/libs/API/parameters/SetWorkspaceApprovalModeParams.ts new file mode 100644 index 000000000000..df84fbabbf95 --- /dev/null +++ b/src/libs/API/parameters/SetWorkspaceApprovalModeParams.ts @@ -0,0 +1,6 @@ +type SetWorkspaceApprovalModeParams = { + policyID: string; + value: string; +}; + +export default SetWorkspaceApprovalModeParams; diff --git a/src/libs/API/parameters/SetWorkspaceAutoReportingParams.ts b/src/libs/API/parameters/SetWorkspaceAutoReportingParams.ts new file mode 100644 index 000000000000..a87817986ffa --- /dev/null +++ b/src/libs/API/parameters/SetWorkspaceAutoReportingParams.ts @@ -0,0 +1,6 @@ +type SetWorkspaceAutoReportingParams = { + policyID: string; + enabled: boolean; +}; + +export default SetWorkspaceAutoReportingParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 2633d795b561..66c6692b19fb 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -145,3 +145,5 @@ export type {default as UnHoldMoneyRequestParams} from './UnHoldMoneyRequestPara export type {default as CancelPaymentParams} from './CancelPaymentParams'; export type {default as AcceptACHContractForBankAccount} from './AcceptACHContractForBankAccount'; export type {default as UpdateWorkspaceDescriptionParams} from './UpdateWorkspaceDescriptionParams'; +export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams'; +export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 35b03f21c841..571fab3404f1 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -8,6 +8,8 @@ import type UpdateBeneficialOwnersForBankAccountParams from './parameters/Update type ApiRequest = ValueOf; const WRITE_COMMANDS = { + SET_WORKSPACE_AUTO_REPORTING: 'SetWorkspaceAutoReporting', + SET_WORKSPACE_APPROVAL_MODE: 'SetWorkspaceApprovalMode', DISMISS_REFERRAL_BANNER: 'DismissReferralBanner', UPDATE_PREFERRED_LOCALE: 'UpdatePreferredLocale', RECONNECT_APP: 'ReconnectApp', @@ -292,6 +294,8 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CANCEL_PAYMENT]: Parameters.CancelPaymentParams; [WRITE_COMMANDS.ACCEPT_ACH_CONTRACT_FOR_BANK_ACCOUNT]: Parameters.AcceptACHContractForBankAccount; [WRITE_COMMANDS.UPDATE_WORKSPACE_DESCRIPTION]: Parameters.UpdateWorkspaceDescriptionParams; + [WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING]: Parameters.SetWorkspaceAutoReportingParams; + [WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE]: Parameters.SetWorkspaceApprovalModeParams; }; const READ_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index 5e14ad9fca29..262a93da9e33 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -17,6 +17,7 @@ const workspaceSettingsScreens = { [SCREENS.SETTINGS.WORKSPACES]: () => require('../../../../../pages/workspace/WorkspacesListPage').default as React.ComponentType, [SCREENS.WORKSPACE.PROFILE]: () => require('../../../../../pages/workspace/WorkspaceProfilePage').default as React.ComponentType, [SCREENS.WORKSPACE.CARD]: () => require('../../../../../pages/workspace/card/WorkspaceCardPage').default as React.ComponentType, + [SCREENS.WORKSPACE.WORKFLOWS]: () => require('../../../../../pages/workspace/workflows/WorkspaceWorkflowsPage').default as React.ComponentType, [SCREENS.WORKSPACE.REIMBURSE]: () => require('../../../../../pages/workspace/reimburse/WorkspaceReimbursePage').default as React.ComponentType, [SCREENS.WORKSPACE.BILLS]: () => require('../../../../../pages/workspace/bills/WorkspaceBillsPage').default as React.ComponentType, [SCREENS.WORKSPACE.INVOICES]: () => require('../../../../../pages/workspace/invoices/WorkspaceInvoicesPage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts index 47b646f4d150..f4316009b70b 100755 --- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts @@ -7,6 +7,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = { [SCREENS.WORKSPACE.INITIAL]: [ SCREENS.WORKSPACE.PROFILE, SCREENS.WORKSPACE.CARD, + SCREENS.WORKSPACE.WORKFLOWS, SCREENS.WORKSPACE.REIMBURSE, SCREENS.WORKSPACE.BILLS, SCREENS.WORKSPACE.INVOICES, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 9428379430dd..d98e19bb155e 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -46,6 +46,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CARD]: { path: ROUTES.WORKSPACE_CARD.route, }, + [SCREENS.WORKSPACE.WORKFLOWS]: { + path: ROUTES.WORKSPACE_WORKFLOWS.route, + }, [SCREENS.WORKSPACE.REIMBURSE]: { path: ROUTES.WORKSPACE_REIMBURSE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 1aae7dae1a7f..2e00099b7966 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -59,6 +59,9 @@ type CentralPaneNavigatorParamList = { [SCREENS.WORKSPACE.CARD]: { policyID: string; }; + [SCREENS.WORKSPACE.WORKFLOWS]: { + policyID: string; + }; [SCREENS.WORKSPACE.REIMBURSE]: { policyID: string; }; diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index 874ddb6804d7..57cd4a6fc071 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -6,6 +6,7 @@ import lodashClone from 'lodash/clone'; import lodashUnion from 'lodash/union'; import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; import * as API from '@libs/API'; import type { AddMembersToWorkspaceParams, @@ -19,6 +20,8 @@ import type { OpenWorkspaceMembersPageParams, OpenWorkspaceParams, OpenWorkspaceReimburseViewParams, + SetWorkspaceApprovalModeParams, + SetWorkspaceAutoReportingParams, UpdateWorkspaceAvatarParams, UpdateWorkspaceCustomUnitAndRateParams, UpdateWorkspaceDescriptionParams, @@ -381,6 +384,87 @@ function buildAnnounceRoomMembersOnyxData(policyID: string, accountIDs: number[] return announceRoomMembers; } +function setWorkspaceAutoReporting(policyID: string, enabled: boolean) { + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + autoReporting: enabled, + pendingFields: {isAutoApprovalEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + autoReporting: !enabled, + pendingFields: {isAutoApprovalEnabled: null}, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: {isAutoApprovalEnabled: null}, + }, + }, + ]; + + const params: SetWorkspaceAutoReportingParams = {policyID, enabled}; + API.write(WRITE_COMMANDS.SET_WORKSPACE_AUTO_REPORTING, params, {optimisticData, failureData, successData}); +} + +function setWorkspaceApprovalMode(policyID: string, approver: string, approvalMode: ValueOf) { + const isAutoApprovalEnabled = approvalMode === CONST.POLICY.APPROVAL_MODE.BASIC; + + const value = { + approver, + approvalMode, + isAutoApprovalEnabled, + }; + + const optimisticData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + ...value, + pendingFields: {approvalMode: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE}, + }, + }, + ]; + + const failureData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: {approvalMode: null}, + }, + }, + ]; + + const successData: OnyxUpdate[] = [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + value: { + pendingFields: {approvalMode: null}, + }, + }, + ]; + + const params: SetWorkspaceApprovalModeParams = {policyID, value: JSON.stringify(value)}; + API.write(WRITE_COMMANDS.SET_WORKSPACE_APPROVAL_MODE, params, {optimisticData, failureData, successData}); +} + /** * Build optimistic data for removing users from the announcement room */ @@ -2134,5 +2218,7 @@ export { buildOptimisticPolicyRecentlyUsedTags, createDraftInitialWorkspace, setWorkspaceInviteMessageDraft, + setWorkspaceAutoReporting, + setWorkspaceApprovalMode, updateWorkspaceDescription, }; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 746ab08fd861..571e4cafce74 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -96,7 +96,6 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r const hasMembersError = PolicyUtils.hasPolicyMemberError(policyMembers); const hasGeneralSettingsError = !isEmptyObject(policy?.errorFields?.generalSettings ?? {}) || !isEmptyObject(policy?.errorFields?.avatar ?? {}); - const shouldShowProtectedItems = PolicyUtils.isPolicyAdmin(policy); const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); const isFreeGroupPolicy = PolicyUtils.isFreeGroupPolicy(policy); @@ -151,6 +150,12 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r ]; const protectedCollectPolicyMenuItems: WorkspaceMenuItem[] = [ + { + translationKey: 'workspace.common.workflows', + icon: Expensicons.Workflows, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.WORKFLOWS, + }, { translationKey: 'workspace.common.members', icon: Expensicons.Users, diff --git a/src/pages/workspace/WorkspacePageWithSections.tsx b/src/pages/workspace/WorkspacePageWithSections.tsx index 0f008a749072..b9b14e27d01d 100644 --- a/src/pages/workspace/WorkspacePageWithSections.tsx +++ b/src/pages/workspace/WorkspacePageWithSections.tsx @@ -42,7 +42,7 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & headerText: string; /** Main content of the page */ - children: (hasVBA: boolean, policyID: string, isUsingECard: boolean) => ReactNode; + children: ((hasVBA: boolean, policyID: string, isUsingECard: boolean) => ReactNode) | ReactNode; /** Content to be added as fixed footer */ footer?: ReactNode; @@ -68,6 +68,9 @@ type WorkspacePageWithSectionsProps = WithPolicyAndFullscreenLoadingProps & /** Whether to show this page to non admin policy members */ shouldShowNonAdmin?: boolean; + /** Whether to show the not found page */ + shouldShowNotFoundPage?: boolean; + /** Policy values needed in the component */ policy: OnyxEntry; @@ -91,6 +94,7 @@ function WorkspacePageWithSections({ backButtonRoute, children = () => null, footer = null, + icon = undefined, guidesCallTaskID = '', headerText, policy, @@ -104,7 +108,7 @@ function WorkspacePageWithSections({ shouldShowLoading = true, shouldShowOfflineIndicatorInWideScreen = false, shouldShowNonAdmin = false, - icon, + shouldShowNotFoundPage = false, }: WorkspacePageWithSectionsProps) { const styles = useThemeStyles(); const policyID = route.params?.policyID ?? ''; @@ -114,7 +118,7 @@ function WorkspacePageWithSections({ const achState = reimbursementAccount?.achData?.state ?? ''; const isUsingECard = user?.isUsingExpensifyCard ?? false; const hasVBA = achState === BankAccount.STATE.OPEN; - const content = children(hasVBA, policyID, isUsingECard); + const content = typeof children === 'function' ? children(hasVBA, policyID, isUsingECard) : children; const {isSmallScreenWidth} = useWindowDimensions(); const firstRender = useRef(true); @@ -129,7 +133,7 @@ function WorkspacePageWithSections({ const shouldShow = useMemo(() => { // If the policy object doesn't exist or contains only error data, we shouldn't display it. - if ((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) { + if (((isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors))) && isEmptyObject(policyDraft)) || shouldShowNotFoundPage) { return true; } @@ -157,7 +161,7 @@ function WorkspacePageWithSections({ guidesCallTaskID={guidesCallTaskID} shouldShowBackButton={isSmallScreenWidth || shouldShowBackButton} onBackButtonPress={() => Navigation.goBack(backButtonRoute ?? ROUTES.WORKSPACE_INITIAL.getRoute(policyID))} - icon={icon} + icon={icon ?? undefined} /> {(isLoading || firstRender.current) && shouldShowLoading ? ( diff --git a/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx new file mode 100644 index 000000000000..62f32992601a --- /dev/null +++ b/src/pages/workspace/workflows/ToggleSettingsOptionRow.tsx @@ -0,0 +1,85 @@ +import React, {useState} from 'react'; +import {View} from 'react-native'; +import type {SvgProps} from 'react-native-svg'; +import Icon from '@components/Icon'; +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'; + +type ToggleSettingOptionRowProps = { + /** Icon to be shown for the option */ + icon: React.FC; + /** Title of the option */ + title: string; + /** Subtitle of the option */ + subtitle: string; + /** Whether the option is enabled or not */ + isActive: boolean; + /** Callback to be called when the switch is toggled */ + onToggle: (isEnabled: boolean) => void; + /** SubMenuItems will be shown when the option is enabled */ + subMenuItems?: React.ReactNode; + /** If there is a pending action, we will grey out the option */ + pendingAction?: PendingAction; +}; +const ICON_SIZE = 48; + +function ToggleSettingOptionRow({icon, title, subtitle, onToggle, subMenuItems, isActive, pendingAction}: ToggleSettingOptionRowProps) { + const [isEnabled, setIsEnabled] = useState(isActive); + const styles = useThemeStyles(); + const toggleSwitch = () => { + setIsEnabled(!isEnabled); + onToggle(!isEnabled); + }; + + return ( + + + + + + + + {title} + + + {subtitle} + + + + + + {isEnabled && subMenuItems} + + + ); +} + +export type {ToggleSettingOptionRowProps}; +export default ToggleSettingOptionRow; diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx new file mode 100644 index 000000000000..d1cb3e066d99 --- /dev/null +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -0,0 +1,163 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useMemo} from 'react'; +import {FlatList, View} from 'react-native'; +import * as Illustrations from '@components/Icon/Illustrations'; +import MenuItem from '@components/MenuItem'; +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 OptionsListUtils from '@libs/OptionsListUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import withPolicy from '@pages/workspace/withPolicy'; +import type {WithPolicyProps} from '@pages/workspace/withPolicy'; +import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSections'; +import * as Policy from '@userActions/Policy'; +import CONST from '@src/CONST'; +import type SCREENS from '@src/SCREENS'; +import type {PendingAction} from '@src/types/onyx/OnyxCommon'; +import ToggleSettingOptionRow from './ToggleSettingsOptionRow'; +import type {ToggleSettingOptionRowProps} from './ToggleSettingsOptionRow'; + +type WorkspaceWorkflowsPageProps = WithPolicyProps & StackScreenProps; + +function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {isSmallScreenWidth} = useWindowDimensions(); + const {isOffline} = useNetwork(); + + const ownerPersonalDetails = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForAccountIDs([policy?.ownerAccountID ?? 0], CONST.EMPTY_OBJECT), false); + const policyOwnerDisplayName = ownerPersonalDetails[0]?.displayName; + const containerStyle = useMemo(() => [styles.ph8, styles.mhn8, styles.ml11, styles.pv3, styles.pr0, styles.pl4, styles.mr0, styles.widthAuto, styles.mt4], [styles]); + + const items: ToggleSettingOptionRowProps[] = useMemo( + () => [ + { + icon: Illustrations.ReceiptEnvelope, + title: translate('workflowsPage.delaySubmissionTitle'), + subtitle: translate('workflowsPage.delaySubmissionDescription'), + onToggle: (isEnabled: boolean) => { + Policy.setWorkspaceAutoReporting(route.params.policyID, isEnabled); + }, + subMenuItems: ( + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_AUTOREPORTING_FREQUENCY).getRoute(route.params.policyID))} + // TODO will be done in https://github.com/Expensify/Expensify/issues/368332 + description={translate('workflowsPage.weeklyFrequency')} + shouldShowRightIcon + wrapperStyle={containerStyle} + hoverAndPressStyle={[styles.mr0, styles.br2]} + /> + ), + isActive: policy?.harvesting?.enabled ?? false, + pendingAction: policy?.pendingFields?.isAutoApprovalEnabled as PendingAction, + }, + { + icon: Illustrations.Approval, + title: translate('workflowsPage.addApprovalsTitle'), + subtitle: translate('workflowsPage.addApprovalsDescription'), + onToggle: (isEnabled: boolean) => { + Policy.setWorkspaceApprovalMode(route.params.policyID, policy?.owner ?? '', isEnabled ? CONST.POLICY.APPROVAL_MODE.BASIC : CONST.POLICY.APPROVAL_MODE.OPTIONAL); + }, + subMenuItems: ( + Navigation.navigate(ROUTES.WORKSPACE_WORKFLOWS_APPROVER.getRoute(route.params.policyID))} + // TODO will be done in https://github.com/Expensify/Expensify/issues/368334 + shouldShowRightIcon + wrapperStyle={containerStyle} + hoverAndPressStyle={[styles.mr0, styles.br2]} + /> + ), + isActive: policy?.isAutoApprovalEnabled ?? false, + pendingAction: policy?.pendingFields?.approvalMode as PendingAction, + }, + { + icon: Illustrations.WalletAlt, + title: translate('workflowsPage.makeOrTrackPaymentsTitle'), + subtitle: translate('workflowsPage.makeOrTrackPaymentsDescription'), + onToggle: () => { + // TODO will be done in https://github.com/Expensify/Expensify/issues/368335 + }, + 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]} + /> + ), + isEndOptionRow: true, + isActive: false, // TODO will be done in https://github.com/Expensify/Expensify/issues/368335 + }, + ], + [policy, route.params.policyID, styles, translate, policyOwnerDisplayName, containerStyle, isOffline, StyleUtils], + ); + + const renderItem = ({item}: {item: ToggleSettingOptionRowProps}) => ( + + + + ); + + const isPaidGroupPolicy = PolicyUtils.isPaidGroupPolicy(policy); + const isPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + + return ( + + +
+ + {translate('workflowsPage.workflowDescription')} + item.title} + /> + +
+
+
+ ); +} + +WorkspaceWorkflowsPage.displayName = 'WorkspaceWorkflowsPage'; + +export default withPolicy(WorkspaceWorkflowsPage); diff --git a/src/styles/index.ts b/src/styles/index.ts index 62d50df5ef5e..d1d60bbe8a75 100644 --- a/src/styles/index.ts +++ b/src/styles/index.ts @@ -400,6 +400,11 @@ const styles = (theme: ThemeColors) => fontSize: variables.fontSizeNormal, }, + textNormalThemeText: { + color: theme.text, + fontSize: variables.fontSizeNormal, + }, + textLarge: { fontSize: variables.fontSizeLarge, }, @@ -487,6 +492,10 @@ const styles = (theme: ThemeColors) => opacity: 0, }, + opacitySemiTransparent: { + opacity: 0.5, + }, + opacity1: { opacity: 1, }, @@ -1235,6 +1244,13 @@ const styles = (theme: ThemeColors) => color: theme.textSupporting, }, + textLabelSupportingNormal: { + fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, + fontSize: variables.fontSizeLabel, + color: theme.textSupporting, + fontWeight: FontUtils.fontWeight.normal, + }, + textLabelError: { fontFamily: FontUtils.fontFamily.platform.EXP_NEUE, fontSize: variables.fontSizeLabel, @@ -4604,6 +4620,10 @@ const styles = (theme: ThemeColors) => width: variables.updateTextViewContainerWidth, }, + widthAuto: { + width: 'auto', + }, + workspaceTitleStyle: { fontFamily: FontUtils.fontFamily.platform.EXP_NEUE_BOLD, fontWeight: '500', diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 3ac9a7f8e718..72719e4795c4 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1487,6 +1487,12 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ ...(isDisabled && styles.cursorDisabled), ...(isDisabled && styles.buttonOpacityDisabled), }), + + // TODO: remove it when we'll implement the callback to handle this toggle in Expensify/Expensify#368335 + getWorkspaceWorkflowsOfflineDescriptionStyle: (descriptionTextStyle: TextStyle | TextStyle[]): StyleProp => ({ + ...StyleSheet.flatten(descriptionTextStyle), + opacity: styles.opacitySemiTransparent.opacity, + }), }); type StyleUtilsType = ReturnType; diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index f27b07f4eeee..0249b3c6dfc0 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -179,6 +179,10 @@ export default { marginLeft: 40, }, + ml11: { + marginLeft: 44, + }, + ml18: { marginLeft: 72, }, @@ -481,6 +485,10 @@ export default { paddingLeft: 12, }, + pl4: { + paddingLeft: 16, + }, + pl5: { paddingLeft: 20, }, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index c0f5a53d96d8..87974619ffc8 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -144,9 +144,15 @@ type Policy = { /** Whether policy is updating */ isPolicyUpdating?: boolean; + /** The approver of the policy */ + approver?: string; + /** The approval mode set up on this policy */ approvalMode?: ValueOf; + /** Whether the auto approval is enabled */ + isAutoApprovalEnabled?: boolean; + /** Whether transactions should be billable by default */ defaultBillable?: boolean;