From b2303ca8ca46c85de6b3add204dc64e4a483fef5 Mon Sep 17 00:00:00 2001 From: Michal Muzyk Date: Tue, 5 Mar 2024 09:01:30 +0100 Subject: [PATCH 01/13] feat: policy distance rate page --- .../simple-illustration__car-ice.svg | 53 +++++ src/ROUTES.ts | 4 + src/SCREENS.ts | 1 + src/components/Icon/Illustrations.ts | 2 + src/languages/en.ts | 19 ++ .../OpenPolicyDistanceRatesPageParams.ts | 5 + src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 2 + .../BaseCentralPaneNavigator.tsx | 1 + .../TAB_TO_CENTRAL_PANE_MAPPING.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/libs/actions/Policy.ts | 12 ++ src/pages/workspace/WorkspaceInitialPage.tsx | 9 +- .../distanceRates/PolicyDistanceRatesPage.tsx | 195 ++++++++++++++++++ 15 files changed, 310 insertions(+), 1 deletion(-) create mode 100644 assets/images/simple-illustrations/simple-illustration__car-ice.svg create mode 100644 src/libs/API/parameters/OpenPolicyDistanceRatesPageParams.ts create mode 100644 src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx diff --git a/assets/images/simple-illustrations/simple-illustration__car-ice.svg b/assets/images/simple-illustrations/simple-illustration__car-ice.svg new file mode 100644 index 000000000000..ba2b79bca6aa --- /dev/null +++ b/assets/images/simple-illustrations/simple-illustration__car-ice.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 22ebffd52eec..763318b05034 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -530,6 +530,10 @@ const ROUTES = { route: 'workspace/:policyID/categories/settings', getRoute: (policyID: string) => `workspace/${policyID}/categories/settings` as const, }, + WORKSPACE_DISTANCE_RATES: { + route: 'workspace/:policyID/distance-rates', + getRoute: (policyID: string) => `workspace/${policyID}/distance-rates` as const, + }, // Referral program promotion REFERRAL_DETAILS_MODAL: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ac75968e68b9..421be498ea67 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -220,6 +220,7 @@ const SCREENS = { SHARE: 'Workspace_Profile_Share', NAME: 'Workspace_Profile_Name', CATEGORIES_SETTINGS: 'Categories_Settings', + DISTANCE_RATES: 'Distance_Rates', }, EDIT_REQUEST: { diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index f8c048ebc4c0..bb6fe33c6213 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -33,6 +33,7 @@ import Approval from '@assets/images/simple-illustrations/simple-illustration__a 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'; +import CarIce from '@assets/images/simple-illustrations/simple-illustration__car-ice.svg'; import ChatBubbles from '@assets/images/simple-illustrations/simple-illustration__chatbubbles.svg'; import CoffeeMug from '@assets/images/simple-illustrations/simple-illustration__coffeemug.svg'; import CommentBubbles from '@assets/images/simple-illustrations/simple-illustration__commentbubbles.svg'; @@ -146,4 +147,5 @@ export { Workflows, ThreeLeggedLaptopWoman, House, + CarIce, }; diff --git a/src/languages/en.ts b/src/languages/en.ts index 4018eab57d2b..49a4d358aae4 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1733,6 +1733,7 @@ export default { workspaceType: 'Workspace type', workspaceAvatar: 'Workspace avatar', mustBeOnlineToViewMembers: 'You must be online in order to view members of this workspace.', + distanceRates: 'Distance rates', }, type: { free: 'Free', @@ -1866,6 +1867,24 @@ export default { welcomeNote: ({workspaceName}: WelcomeNoteParams) => `You have been invited to ${workspaceName || 'a workspace'}! Download the Expensify mobile app at use.expensify.com/download to start tracking your expenses.`, }, + distanceRates: { + distance: 'Distance', + centrallyManage: 'Centrally manage rates, choose to track in miles or kilometers, and set a default category.', + rate: 'Rate', + addRate: 'Add rate', + deleteRate: 'Delete rate', + deleteRates: 'Delete rates', + enableRate: 'Enable rate', + disableRates: 'Disable rates', + deleteDistanceRate: 'Delete distance rate', + areYouSureDelete: 'Are you sure you want to delete this rate?', + status: 'Status', + enabled: 'Enabled', + errors: { + toggleRateGenericFailureMessage: 'An error occurred while toggling the distance rate, please try again.', + deleteRateGenericFailureMessage: 'An error occurred while deleting the distance rate, please try again.', + }, + }, editor: { descriptionInputLabel: 'Description', nameInputLabel: 'Name', diff --git a/src/libs/API/parameters/OpenPolicyDistanceRatesPageParams.ts b/src/libs/API/parameters/OpenPolicyDistanceRatesPageParams.ts new file mode 100644 index 000000000000..6594e258fdb6 --- /dev/null +++ b/src/libs/API/parameters/OpenPolicyDistanceRatesPageParams.ts @@ -0,0 +1,5 @@ +type OpenPolicyDistanceRatesPageParams = { + policyID: string; +}; + +export default OpenPolicyDistanceRatesPageParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index fc24b97ff1f3..31c4a611c483 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -149,3 +149,4 @@ export type {default as SetWorkspaceRequiresCategoryParams} from './SetWorkspace export type {default as SetWorkspaceAutoReportingParams} from './SetWorkspaceAutoReportingParams'; export type {default as SetWorkspaceApprovalModeParams} from './SetWorkspaceApprovalModeParams'; export type {default as SwitchToOldDotParams} from './SwitchToOldDotParams'; +export type {default as OpenPolicyDistanceRatesPageParams} from './OpenPolicyDistanceRatesPageParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index c6fd1154fbf1..0cf918abd282 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -333,6 +333,7 @@ const READ_COMMANDS = { OPEN_WORKSPACE_MEMBERS_PAGE: 'OpenWorkspaceMembersPage', OPEN_WORKSPACE_INVITE_PAGE: 'OpenWorkspaceInvitePage', OPEN_DRAFT_WORKSPACE_REQUEST: 'OpenDraftWorkspaceRequest', + OPEN_POLICY_DISTANCE_RATES_PAGE: 'OpenPolicyDistanceRatesPage', } as const; type ReadCommand = ValueOf; @@ -366,6 +367,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_WORKSPACE_MEMBERS_PAGE]: Parameters.OpenWorkspaceMembersPageParams; [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; + [READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE]: Parameters.OpenPolicyDistanceRatesPageParams; }; const SIDE_EFFECT_REQUEST_COMMANDS = { diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx index 1e5d3639a32f..5dd147c4fbfe 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.tsx @@ -24,6 +24,7 @@ const workspaceSettingsScreens = { [SCREENS.WORKSPACE.TRAVEL]: () => require('../../../../../pages/workspace/travel/WorkspaceTravelPage').default as React.ComponentType, [SCREENS.WORKSPACE.MEMBERS]: () => require('../../../../../pages/workspace/WorkspaceMembersPage').default as React.ComponentType, [SCREENS.WORKSPACE.CATEGORIES]: () => require('../../../../../pages/workspace/categories/WorkspaceCategoriesPage').default as React.ComponentType, + [SCREENS.WORKSPACE.DISTANCE_RATES]: () => require('../../../../../pages/workspace/distanceRates/PolicyDistanceRatesPage').default as React.ComponentType, } satisfies Screens; function BaseCentralPaneNavigator() { 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 f4316009b70b..6641b2c88f1a 100755 --- a/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/TAB_TO_CENTRAL_PANE_MAPPING.ts @@ -14,6 +14,7 @@ const TAB_TO_CENTRAL_PANE_MAPPING: Record = { SCREENS.WORKSPACE.TRAVEL, SCREENS.WORKSPACE.MEMBERS, SCREENS.WORKSPACE.CATEGORIES, + SCREENS.WORKSPACE.DISTANCE_RATES, ], }; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 7a6211ebd283..111f99ef8f70 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -67,6 +67,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.CATEGORIES]: { path: ROUTES.WORKSPACE_CATEGORIES.route, }, + [SCREENS.WORKSPACE.DISTANCE_RATES]: { + path: ROUTES.WORKSPACE_DISTANCE_RATES.route, + }, }, }, [SCREENS.NOT_FOUND]: '*', diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e0bbbe95802f..9005ecf0af66 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -81,6 +81,9 @@ type CentralPaneNavigatorParamList = { [SCREENS.WORKSPACE.CATEGORIES]: { policyID: string; }; + [SCREENS.WORKSPACE.DISTANCE_RATES]: { + policyID: string; + }; }; type WorkspaceSwitcherNavigatorParamList = { diff --git a/src/libs/actions/Policy.ts b/src/libs/actions/Policy.ts index ce222940c7ca..5b9d2ab2fb16 100644 --- a/src/libs/actions/Policy.ts +++ b/src/libs/actions/Policy.ts @@ -16,6 +16,7 @@ import type { DeleteWorkspaceAvatarParams, DeleteWorkspaceParams, OpenDraftWorkspaceRequestParams, + OpenPolicyDistanceRatesPageParams, OpenWorkspaceInvitePageParams, OpenWorkspaceMembersPageParams, OpenWorkspaceParams, @@ -2248,6 +2249,16 @@ const setWorkspaceRequiresCategory = (policyID: string, requiresCategory: boolea API.write('SetWorkspaceRequiresCategory', parameters, onyxData); }; +function openPolicyDistanceRatesPage(policyID: string) { + if (!policyID) { + return; + } + + const params: OpenPolicyDistanceRatesPageParams = {policyID}; + + API.read(READ_COMMANDS.OPEN_POLICY_DISTANCE_RATES_PAGE, params); +} + export { removeMembers, addMembersToWorkspace, @@ -2292,4 +2303,5 @@ export { setWorkspaceApprovalMode, updateWorkspaceDescription, setWorkspaceRequiresCategory, + openPolicyDistanceRatesPage, }; diff --git a/src/pages/workspace/WorkspaceInitialPage.tsx b/src/pages/workspace/WorkspaceInitialPage.tsx index 571e4cafce74..0044215ee8cc 100644 --- a/src/pages/workspace/WorkspaceInitialPage.tsx +++ b/src/pages/workspace/WorkspaceInitialPage.tsx @@ -169,6 +169,12 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_CATEGORIES.getRoute(policyID)))), routeName: SCREENS.WORKSPACE.CATEGORIES, }, + { + translationKey: 'workspace.common.distanceRates', + icon: Expensicons.Car, + action: singleExecution(waitForNavigate(() => Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES.getRoute(policyID)))), + routeName: SCREENS.WORKSPACE.DISTANCE_RATES, + }, ]; const menuItems: WorkspaceMenuItem[] = [ @@ -179,7 +185,8 @@ function WorkspaceInitialPage({policyDraft, policy: policyProp, policyMembers, r brickRoadIndicator: hasGeneralSettingsError ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined, routeName: SCREENS.WORKSPACE.PROFILE, }, - ...(isPaidGroupPolicy && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []), + // TODO revert to isPaidGroupPolicy + ...(isFreeGroupPolicy && shouldShowProtectedItems ? protectedCollectPolicyMenuItems : []), ...(isFreeGroupPolicy && shouldShowProtectedItems ? protectedFreePolicyMenuItems : []), ]; diff --git a/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx new file mode 100644 index 000000000000..79047049add7 --- /dev/null +++ b/src/pages/workspace/distanceRates/PolicyDistanceRatesPage.tsx @@ -0,0 +1,195 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {withOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; +import * as Expensicons from '@components/Icon/Expensicons'; +import * as Illustrations from '@components/Icon/Illustrations'; +import ScreenWrapper from '@components/ScreenWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useTheme from '@hooks/useTheme'; +import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import type {CentralPaneNavigatorParamList} from '@navigation/types'; +import {openPolicyDistanceRatesPage} from '@userActions/Policy'; +import ButtonWithDropdownMenu from '@src/components/ButtonWithDropdownMenu'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type SCREENS from '@src/SCREENS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type Policy from '@src/types/onyx/Policy'; +import type {Rate} from '@src/types/onyx/Policy'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type RateForList = { + value: string; + text: string; + keyForList: string; + isSelected: boolean; + rightElement: React.ReactNode; +}; + +type PolicyDistanceRatesPageOnyxProps = { + /** Policy details */ + policy: OnyxEntry; + + /** Constant, list of available currencies */ + currencyList: OnyxEntry; +}; + +type PolicyDistanceRatesPageProps = PolicyDistanceRatesPageOnyxProps & StackScreenProps; + +const distanceRates: Record = { + RATE1: { + name: 'rate 1', + rate: 0.665, + currency: 'USD', + enabled: true, + customUnitRateID: 'RATE1', + }, + RATE2: { + name: 'rate 2', + rate: 0.122, + currency: 'USD', + enabled: false, + customUnitRateID: 'RATE2', + }, +}; + +function PolicyDistanceRatesPage({policy, currencyList, route}: PolicyDistanceRatesPageProps) { + const {isSmallScreenWidth} = useWindowDimensions(); + const styles = useThemeStyles(); + const theme = useTheme(); + const {translate} = useLocalize(); + const [selectedDistanceRates, setSelectedDistanceRates] = useState>({}); + + useEffect(() => { + openPolicyDistanceRatesPage(route.params.policyID); + }, [route.params.policyID]); + + const distanceRatesList = useMemo( + () => + // TODO replace distanceRates const with actual data from API + Object.values(distanceRates ?? {}).map((value) => ({ + value: value.customUnitRateID ?? '', + text: `${currencyList?.[value.currency ?? '']?.symbol ?? ''}${value.rate} / mile`, + keyForList: value.customUnitRateID ?? '', + isSelected: selectedDistanceRates[value.customUnitRateID ?? ''], + rightElement: ( + + {value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')} + + + + + ), + })), + [currencyList, selectedDistanceRates, styles.alignSelfCenter, styles.disabledText, styles.flexRow, styles.p1, styles.pl2, theme.icon, translate], + ); + + const addRate = () => { + // Navigation.navigate(ROUTES.WORKSPACE_CREATE_DISTANCE_RATE.getRoute(route.params.policyID)); + }; + + const openSettings = () => { + // Navigation.navigate(ROUTES.WORKSPACE_DISTANCE_RATES_SETTINGS.getRoute(route.params.policyID)); + }; + + const editRate = (rateID: string) => { + // Navigation.navigate(ROUTES.WORKSPACE_EDIT_DISTANCE_RATE.getRoute(route.params.policyID, rateID)); + }; + + const deleteRates = () => { + // run deleteWorkspaceDistanceRates for all selected rows + }; + + const toggleRate = (rate: RateForList) => { + setSelectedDistanceRates((prev) => ({ + ...prev, + [rate.value]: !prev[rate.value], + })); + }; + + const toggleAllRates = () => { + const isAllSelected = distanceRatesList.every((rate) => selectedDistanceRates[rate.value]); + setSelectedDistanceRates(isAllSelected ? {} : Object.fromEntries(distanceRatesList.map((item) => [item.value, true]))); + }; + + const getCustomListHeader = () => ( + + {translate('workspace.distanceRates.rate')} + {translate('statusPage.status')} + + ); + + const headerButtons = isEmptyObject(selectedDistanceRates) ? ( + +