diff --git a/assets/images/product-illustrations/broken-magnifying-glass.svg b/assets/images/product-illustrations/broken-magnifying-glass.svg new file mode 100644 index 000000000000..0b85744c1869 --- /dev/null +++ b/assets/images/product-illustrations/broken-magnifying-glass.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/CONST.ts b/src/CONST.ts index 5f3fe783f1ac..be7e7fd2088c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -2291,6 +2291,17 @@ const CONST = { VISA: 'vcf', AMEX: 'gl1025', }, + STEP_NAMES: ['1', '2', '3', '4'], + STEP: { + ASSIGNEE: 'Assignee', + CARD: 'Card', + TRANSACTION_START_DATE: 'TransactionStartDate', + CONFIRMATION: 'Confirmation', + }, + TRANSACTION_START_DATE_OPTIONS: { + FROM_BEGINNING: 'fromBeginning', + CUSTOM: 'custom', + }, }, EXPENSIFY_CARD: { BANK: 'Expensify Card', diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a276f93403bd..d2a0372fd9c7 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -392,6 +392,9 @@ const ONYXKEYS = { /** Stores the information about the state of issuing a new card */ ISSUE_NEW_EXPENSIFY_CARD: 'issueNewExpensifyCard', + /** Stores the information about the state of assigning a company card */ + ASSIGN_CARD: 'assignCard', + /** Stores the information if mobile selection mode is active */ MOBILE_SELECTION_MODE: 'mobileSelectionMode', @@ -615,6 +618,8 @@ const ONYXKEYS = { SUBSCRIPTION_SIZE_FORM_DRAFT: 'subscriptionSizeFormDraft', ISSUE_NEW_EXPENSIFY_CARD_FORM: 'issueNewExpensifyCard', ISSUE_NEW_EXPENSIFY_CARD_FORM_DRAFT: 'issueNewExpensifyCardDraft', + ASSIGN_CARD_FORM: 'assignCard', + ASSIGN_CARD_FORM_DRAFT: 'assignCardDraft', EDIT_EXPENSIFY_CARD_NAME_FORM: 'editExpensifyCardName', EDIT_EXPENSIFY_CARD_NAME_DRAFT_FORM: 'editExpensifyCardNameDraft', EDIT_EXPENSIFY_CARD_LIMIT_FORM: 'editExpensifyCardLimit', @@ -712,6 +717,7 @@ type OnyxFormValuesMapping = { [ONYXKEYS.FORMS.NEW_CHAT_NAME_FORM]: FormTypes.NewChatNameForm; [ONYXKEYS.FORMS.SUBSCRIPTION_SIZE_FORM]: FormTypes.SubscriptionSizeForm; [ONYXKEYS.FORMS.ISSUE_NEW_EXPENSIFY_CARD_FORM]: FormTypes.IssueNewExpensifyCardForm; + [ONYXKEYS.FORMS.ASSIGN_CARD_FORM]: FormTypes.AssignCardForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_NAME_FORM]: FormTypes.EditExpensifyCardNameForm; [ONYXKEYS.FORMS.EDIT_EXPENSIFY_CARD_LIMIT_FORM]: FormTypes.EditExpensifyCardLimitForm; [ONYXKEYS.FORMS.SAGE_INTACCT_CREDENTIALS_FORM]: FormTypes.SageIntactCredentialsForm; @@ -908,6 +914,7 @@ type OnyxValuesMapping = { [ONYXKEYS.NVP_TRAVEL_SETTINGS]: OnyxTypes.TravelSettings; [ONYXKEYS.REVIEW_DUPLICATES]: OnyxTypes.ReviewDuplicates; [ONYXKEYS.ISSUE_NEW_EXPENSIFY_CARD]: OnyxTypes.IssueNewCard; + [ONYXKEYS.ASSIGN_CARD]: OnyxTypes.AssignCard; [ONYXKEYS.MOBILE_SELECTION_MODE]: OnyxTypes.MobileSelectionMode; [ONYXKEYS.NVP_FIRST_DAY_FREE_TRIAL]: string; [ONYXKEYS.NVP_LAST_DAY_FREE_TRIAL]: string; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 32181bca2bd8..89023063ad8f 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -910,6 +910,10 @@ const ROUTES = { route: 'settings/workspaces/:policyID/company-cards', getRoute: (policyID: string) => `settings/workspaces/${policyID}/company-cards` as const, }, + WORKSPACE_COMPANY_CARDS_ASSIGN_CARD: { + route: 'settings/workspaces/:policyID/company-cards/:feed/assign-card', + getRoute: (policyID: string, feed: string) => `settings/workspaces/${policyID}/company-cards/${feed}/assign-card` as const, + }, WORKSPACE_EXPENSIFY_CARD_DETAILS: { route: 'settings/workspaces/:policyID/expensify-card/:cardID', getRoute: (policyID: string, cardID: string, backTo?: string) => getUrlWithBackToParam(`settings/workspaces/${policyID}/expensify-card/${cardID}`, backTo), diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 829da0575d0a..db790dd389c3 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -368,6 +368,7 @@ const SCREENS = { RATE_AND_UNIT_RATE: 'Workspace_RateAndUnit_Rate', RATE_AND_UNIT_UNIT: 'Workspace_RateAndUnit_Unit', COMPANY_CARDS: 'Workspace_CompanyCards', + COMPANY_CARDS_ASSIGN_CARD: 'Workspace_CompanyCards_AssignCard', COMPANY_CARDS_SELECT_FEED: 'Workspace_CompanyCards_Select_Feed', COMPANY_CARDS_SETTINGS: 'Workspace_CompanyCards_Settings', COMPANY_CARDS_SETTINGS_FEED_NAME: 'Workspace_CompanyCards_Settings_Feed_Name', diff --git a/src/components/Icon/Illustrations.ts b/src/components/Icon/Illustrations.ts index 70cd0168aba8..0616794a8e3a 100644 --- a/src/components/Icon/Illustrations.ts +++ b/src/components/Icon/Illustrations.ts @@ -9,6 +9,7 @@ import Abracadabra from '@assets/images/product-illustrations/abracadabra.svg'; import BankArrowPink from '@assets/images/product-illustrations/bank-arrow--pink.svg'; import BankMouseGreen from '@assets/images/product-illustrations/bank-mouse--green.svg'; import BankUserGreen from '@assets/images/product-illustrations/bank-user--green.svg'; +import BrokenMagnifyingGlass from '@assets/images/product-illustrations/broken-magnifying-glass.svg'; import ConciergeBlue from '@assets/images/product-illustrations/concierge--blue.svg'; import ConciergeExclamation from '@assets/images/product-illustrations/concierge--exclamation.svg'; import CreditCardsBlue from '@assets/images/product-illustrations/credit-cards--blue.svg'; @@ -121,6 +122,7 @@ export { BankMouseGreen, BankUserGreen, BigRocket, + BrokenMagnifyingGlass, ChatBubbles, CoffeeMug, ConciergeBlue, diff --git a/src/components/InteractiveStepWrapper.tsx b/src/components/InteractiveStepWrapper.tsx new file mode 100644 index 000000000000..6ffe00b9bd5d --- /dev/null +++ b/src/components/InteractiveStepWrapper.tsx @@ -0,0 +1,58 @@ +import React from 'react'; +import {View} from 'react-native'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import HeaderWithBackButton from './HeaderWithBackButton'; +import InteractiveStepSubHeader from './InteractiveStepSubHeader'; +import ScreenWrapper from './ScreenWrapper'; + +type InteractiveStepWrapperProps = { + // Step content + children: React.ReactNode; + + // ID of the wrapper + wrapperID: string; + + // Function to handle back button press + handleBackButtonPress: () => void; + + // Title of the back button header + headerTitle: string; + + // Index of the highlighted step + startStepIndex?: number; + + // Array of step names + stepNames?: readonly string[]; +}; + +function InteractiveStepWrapper({children, wrapperID, handleBackButtonPress, headerTitle, startStepIndex, stepNames}: InteractiveStepWrapperProps) { + const styles = useThemeStyles(); + + return ( + + + {stepNames && ( + + + + )} + {children} + + ); +} + +InteractiveStepWrapper.displayName = 'InteractiveStepWrapper'; + +export default InteractiveStepWrapper; diff --git a/src/languages/en.ts b/src/languages/en.ts index 66d1e7c73c90..ca89b782a78e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -10,6 +10,7 @@ import type { AlreadySignedInParams, ApprovalWorkflowErrorParams, ApprovedAmountParams, + AssignCardParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, @@ -2792,6 +2793,23 @@ export default { assignCard: 'Assign card', cardNumber: 'Card number', customFeed: 'Custom feed', + whoNeedsCardAssigned: 'Who needs a card assigned?', + chooseCard: 'Choose a card', + chooseCardFor: ({assignee, feed}: AssignCardParams) => `Choose a card for ${assignee} from the ${feed} cards feed.`, + noActiveCards: 'No active cards on this feed', + somethingMightBeBroken: 'Or something might be broken. Either way, if you have any questions, just', + contactConcierge: 'contact Concierge', + chooseTransactionStartDate: 'Choose a transaction start date', + startDateDescription: 'We will import all transaction from this date onwards. If no date is specified, we’ll go as far back as your bank allows.', + fromTheBeginning: 'From the beginning', + customStartDate: 'Custom start date', + letsDoubleCheck: 'Let’s double check that everything looks right.', + confirmationDescription: 'We’ll begin importing transactions immediately.', + cardholder: 'Cardholder', + card: 'Card', + startTransactionDate: 'Start transaction date', + cardName: 'Card name', + assignedYouCard: (assigner: string) => `${assigner} assigned you a company card! Imported transactions will appear in this chat.`, }, expensifyCard: { issueAndManageCards: 'Issue and manage your Expensify Cards', diff --git a/src/languages/es.ts b/src/languages/es.ts index 54402fd1d6d5..e2e2ba7c2b83 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -8,6 +8,7 @@ import type { AlreadySignedInParams, ApprovalWorkflowErrorParams, ApprovedAmountParams, + AssignCardParams, BeginningOfChatHistoryAdminRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartOneParams, BeginningOfChatHistoryAnnounceRoomPartTwo, @@ -2839,6 +2840,23 @@ export default { assignCard: 'Asignar tarjeta', cardNumber: 'Número de la tarjeta', customFeed: 'Fuente personalizada', + whoNeedsCardAssigned: '¿Quién necesita una tarjeta?', + chooseCard: 'Elige una tarjeta', + chooseCardFor: ({assignee, feed}: AssignCardParams) => `Elige una tarjeta para ${assignee} del feed de tarjetas ${feed}.`, + noActiveCards: 'No hay tarjetas activas en este feed', + somethingMightBeBroken: 'O algo podría estar roto. De cualquier manera, si tienes alguna pregunta,', + contactConcierge: 'contacta a Concierge', + chooseTransactionStartDate: 'Elige una fecha de inicio de transacciones', + startDateDescription: 'Importaremos todas las transacciones desde esta fecha en adelante. Si no se especifica una fecha, iremos tan atrás como lo permita tu banco.', + fromTheBeginning: 'Desde el principio', + customStartDate: 'Fecha de inicio personalizada', + letsDoubleCheck: 'Verifiquemos que todo esté bien.', + confirmationDescription: 'Comenzaremos a importar transacciones inmediatamente.', + cardholder: 'Titular de la tarjeta', + card: 'Tarjeta', + startTransactionDate: 'Fecha de inicio de transacciones', + cardName: 'Nombre de la tarjeta', + assignedYouCard: (assigner: string) => `¡${assigner} te ha asignado una tarjeta de empresa! Las transacciones importadas aparecerán en este chat.`, }, expensifyCard: { issueAndManageCards: 'Emitir y gestionar Tarjetas Expensify', diff --git a/src/languages/types.ts b/src/languages/types.ts index fb396a3f64ea..f953cb17255b 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -361,6 +361,11 @@ type ApprovalWorkflowErrorParams = { name2: string; }; +type AssignCardParams = { + assignee: string; + feed: string; +}; + export type { AddressLineParams, AdminCanceledRequestParams, @@ -485,4 +490,5 @@ export type { RemoveMembersWarningPrompt, DeleteExpenseTranslationParams, ApprovalWorkflowErrorParams, + AssignCardParams, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 1bd32b1cd8a7..c2a30f20ed56 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -424,6 +424,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/taxes/WorkspaceTaxCodePage').default, [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsName').default, [SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE]: () => require('../../../../pages/workspace/invoices/WorkspaceInvoicingDetailsWebsite').default, + [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: () => require('../../../../pages/workspace/companyCards/assignCard/AssignCardFeedPage').default, [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: () => require('../../../../pages/workspace/companyCards/WorkspaceCompanyCardFeedSelectorPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW]: () => require('../../../../pages/workspace/expensifyCard/issueNew/IssueNewCardPage').default, [SCREENS.WORKSPACE.EXPENSIFY_CARD_SETTINGS]: () => require('../../../../pages/workspace/expensifyCard/WorkspaceCardSettingsPage').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 79d90453b676..22db5deaebfb 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -162,7 +162,12 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE, ], [SCREENS.WORKSPACE.INVOICES]: [SCREENS.WORKSPACE.INVOICES_COMPANY_NAME, SCREENS.WORKSPACE.INVOICES_COMPANY_WEBSITE], - [SCREENS.WORKSPACE.COMPANY_CARDS]: [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME], + [SCREENS.WORKSPACE.COMPANY_CARDS]: [ + SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED, + SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS, + SCREENS.WORKSPACE.COMPANY_CARDS_SETTINGS_FEED_NAME, + SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD, + ], [SCREENS.WORKSPACE.EXPENSIFY_CARD]: [ SCREENS.WORKSPACE.EXPENSIFY_CARD_ISSUE_NEW, SCREENS.WORKSPACE.EXPENSIFY_CARD_BANK_ACCOUNT, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 86919a114096..cf45126bfc04 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -517,6 +517,9 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.EXPENSIFY_CARD_DETAILS]: { path: ROUTES.WORKSPACE_EXPENSIFY_CARD_DETAILS.route, }, + [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: { + path: ROUTES.WORKSPACE_COMPANY_CARDS_ASSIGN_CARD.route, + }, [SCREENS.WORKSPACE.RATE_AND_UNIT]: { path: ROUTES.WORKSPACE_RATE_AND_UNIT.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 6c0a1f3e4ee6..c8c2c0f0e41d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1143,6 +1143,10 @@ type FullScreenNavigatorParamList = { [SCREENS.WORKSPACE.COMPANY_CARDS]: { policyID: string; }; + [SCREENS.WORKSPACE.COMPANY_CARDS_ASSIGN_CARD]: { + policyID: string; + feed: string; + }; [SCREENS.WORKSPACE.WORKFLOWS]: { policyID: string; }; diff --git a/src/libs/actions/CompanyCards.ts b/src/libs/actions/CompanyCards.ts new file mode 100644 index 000000000000..1e58ea3b6306 --- /dev/null +++ b/src/libs/actions/CompanyCards.ts @@ -0,0 +1,13 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {AssignCard} from '@src/types/onyx/AssignCard'; + +function setAssignCardStepAndData({data, isEditing, currentStep}: Partial) { + Onyx.merge(ONYXKEYS.ASSIGN_CARD, {data, isEditing, currentStep}); +} + +function clearAssignCardStepAndData() { + Onyx.set(ONYXKEYS.ASSIGN_CARD, {}); +} + +export {setAssignCardStepAndData, clearAssignCardStepAndData}; diff --git a/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx new file mode 100644 index 000000000000..66ceaaf914c4 --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/AssignCardFeedPage.tsx @@ -0,0 +1,47 @@ +import React, {useEffect} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import type {WithPolicyAndFullscreenLoadingProps} from '@pages/workspace/withPolicyAndFullscreenLoading'; +import withPolicyAndFullscreenLoading from '@pages/workspace/withPolicyAndFullscreenLoading'; +import * as CompanyCards from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import AssigneeStep from './AssigneeStep'; +import CardSelectionStep from './CardSelectionStep'; +import ConfirmationStep from './ConfirmationStep'; +import TransactionStartDateStep from './TransactionStartDateStep'; + +type AssignCardFeedPageProps = { + route: { + params: { + feed: string; + }; + }; +} & WithPolicyAndFullscreenLoadingProps; + +function AssignCardFeedPage({route, policy}: AssignCardFeedPageProps) { + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); + const currentStep = assignCard?.currentStep; + + const feed = route.params?.feed; + + useEffect(() => { + CompanyCards.setAssignCardStepAndData({data: {feed}}); + }, [feed]); + + switch (currentStep) { + case CONST.COMPANY_CARD.STEP.ASSIGNEE: + return ; + case CONST.COMPANY_CARD.STEP.CARD: + return ; + case CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE: + return ; + case CONST.COMPANY_CARD.STEP.CONFIRMATION: + return ; + default: + return ; + } + + return ; +} + +export default withPolicyAndFullscreenLoading(AssignCardFeedPage); diff --git a/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx new file mode 100644 index 000000000000..5be837840390 --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/AssigneeStep.tsx @@ -0,0 +1,174 @@ +import React, {useMemo, useState} from 'react'; +import {Keyboard} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import * as Expensicons from '@components/Icon/Expensicons'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import SelectionList from '@components/SelectionList'; +import type {ListItem} from '@components/SelectionList/types'; +import UserListItem from '@components/SelectionList/UserListItem'; +import Text from '@components/Text'; +import useDebouncedState from '@hooks/useDebouncedState'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {formatPhoneNumber} from '@libs/LocalePhoneNumber'; +import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import Navigation from '@navigation/Navigation'; +import * as CompanyCards from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; + +const MINIMUM_MEMBER_TO_SHOW_SEARCH = 8; + +type AssigneeStepProps = { + // The policy that the card will be issued under + policy: OnyxEntry; +}; + +function AssigneeStep({policy}: AssigneeStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); + + const isEditing = assignCard?.isEditing; + + const [selectedMember, setSelectedMember] = useState(assignCard?.data?.email ?? ''); + const [searchTerm, debouncedSearchTerm, setSearchTerm] = useDebouncedState(''); + const [shouldShowError, setShouldShowError] = useState(false); + + const selectMember = (assignee: ListItem) => { + Keyboard.dismiss(); + setSelectedMember(assignee.login ?? ''); + setShouldShowError(false); + }; + + const submit = () => { + if (!selectedMember) { + setShouldShowError(true); + return; + } + CompanyCards.setAssignCardStepAndData({ + currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.CARD, + data: { + email: selectedMember, + }, + isEditing: false, + }); + }; + + const handleBackButtonPress = () => { + if (isEditing) { + CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, isEditing: false}); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_COMPANY_CARDS.getRoute(policy?.id ?? '-1')); + }; + + const shouldShowSearchInput = policy?.employeeList && Object.keys(policy.employeeList).length >= MINIMUM_MEMBER_TO_SHOW_SEARCH; + const textInputLabel = shouldShowSearchInput ? translate('workspace.card.issueNewCard.findMember') : undefined; + + const membersDetails = useMemo(() => { + let membersList: ListItem[] = []; + if (!policy?.employeeList) { + return membersList; + } + + Object.entries(policy.employeeList ?? {}).forEach(([email, policyEmployee]) => { + if (PolicyUtils.isDeletedPolicyEmployee(policyEmployee, isOffline)) { + return; + } + + const personalDetail = PersonalDetailsUtils.getPersonalDetailByEmail(email); + membersList.push({ + keyForList: email, + text: personalDetail?.displayName, + alternateText: email, + login: email, + accountID: personalDetail?.accountID, + isSelected: selectedMember === email, + icons: [ + { + source: personalDetail?.avatar ?? Expensicons.FallbackAvatar, + name: formatPhoneNumber(email), + type: CONST.ICON_TYPE_AVATAR, + id: personalDetail?.accountID, + }, + ], + }); + }); + + membersList = OptionsListUtils.sortAlphabetically(membersList, 'text'); + + return membersList; + }, [isOffline, policy?.employeeList, selectedMember]); + + const sections = useMemo(() => { + if (!debouncedSearchTerm) { + return [ + { + data: membersDetails, + shouldShow: true, + }, + ]; + } + + const searchValue = OptionsListUtils.getSearchValueForPhoneOrEmail(debouncedSearchTerm).toLowerCase(); + const filteredOptions = membersDetails.filter((option) => !!option.text?.toLowerCase().includes(searchValue) || !!option.alternateText?.toLowerCase().includes(searchValue)); + + return [ + { + title: undefined, + data: filteredOptions, + shouldShow: true, + }, + ]; + }, [membersDetails, debouncedSearchTerm]); + + const headerMessage = useMemo(() => { + const searchValue = debouncedSearchTerm.trim().toLowerCase(); + + return OptionsListUtils.getHeaderMessage(sections[0].data.length !== 0, false, searchValue); + }, [debouncedSearchTerm, sections]); + + return ( + + {translate('workspace.companyCards.whoNeedsCardAssigned')} + + + + ); +} + +AssigneeStep.displayName = 'AssigneeStep'; + +export default AssigneeStep; diff --git a/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx new file mode 100644 index 000000000000..b31780d54fa2 --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/CardSelectionStep.tsx @@ -0,0 +1,173 @@ +import React, {useState} from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import FormAlertWithSubmitButton from '@components/FormAlertWithSubmitButton'; +import Icon from '@components/Icon'; +import * as Illustrations from '@components/Icon/Illustrations'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import Text from '@components/Text'; +import TextLink from '@components/TextLink'; +import useEnvironment from '@hooks/useEnvironment'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as CardUtils from '@libs/CardUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import variables from '@styles/variables'; +import * as CompanyCards from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; + +type MockedCard = { + key: string; + cardNumber: string; +}; + +const mockedCardList = [ + { + key: '1', + cardNumber: '123412XXXXXX1234', + }, + { + key: '2', + cardNumber: '123412XXXXXX1235', + }, + { + key: '3', + cardNumber: '123412XXXXXX1236', + }, +]; + +const mockedCardListEmpty: MockedCard[] = []; + +const feedNamesMapping = { + [CONST.COMPANY_CARD.FEED_BANK_NAME.VISA]: 'Visa', + [CONST.COMPANY_CARD.FEED_BANK_NAME.MASTER_CARD]: 'MasterCard', + [CONST.COMPANY_CARD.FEED_BANK_NAME.AMEX]: 'American Express', +}; + +type CardSelectionStepProps = { + feed: string; +}; + +function CardSelectionStep({feed}: CardSelectionStepProps) { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {environmentURL} = useEnvironment(); + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); + + const isEditing = assignCard?.isEditing; + const assignee = assignCard?.data?.email ?? ''; + + const [cardSelected, setCardSelected] = useState(assignCard?.data?.cardName ?? ''); + const [shouldShowError, setShouldShowError] = useState(false); + + const handleBackButtonPress = () => { + if (isEditing) { + CompanyCards.setAssignCardStepAndData({ + currentStep: CONST.COMPANY_CARD.STEP.CONFIRMATION, + isEditing: false, + }); + return; + } + CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.ASSIGNEE}); + }; + + const handleSelectCard = (cardNumber: string) => { + setCardSelected(cardNumber); + setShouldShowError(false); + }; + + const submit = () => { + if (!cardSelected) { + setShouldShowError(true); + return; + } + CompanyCards.setAssignCardStepAndData({ + currentStep: isEditing ? CONST.COMPANY_CARD.STEP.CONFIRMATION : CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE, + data: {cardName: cardSelected}, + isEditing: false, + }); + }; + + // TODO: for now mocking cards + const mockedCards = !Object.values(CONST.COMPANY_CARD.FEED_BANK_NAME).some((value) => value === feed) ? mockedCardListEmpty : mockedCardList; + + const cardListOptions = mockedCards.map((item) => ({ + keyForList: item?.cardNumber, + value: item?.cardNumber, + text: item?.cardNumber, + isSelected: cardSelected === item?.cardNumber, + leftElement: ( + + ), + })); + + return ( + + {!cardListOptions.length ? ( + + + {translate('workspace.companyCards.noActiveCards')} + + {translate('workspace.companyCards.somethingMightBeBroken')}{' '} + + {translate('workspace.companyCards.contactConcierge')} + + . + + + ) : ( + <> + {translate('workspace.companyCards.chooseCard')} + + {translate('workspace.companyCards.chooseCardFor', { + assignee: PersonalDetailsUtils.getPersonalDetailByEmail(assignee ?? '')?.displayName ?? '', + feed: feedNamesMapping[feed as ValueOf] ?? 'visa', + })} + + handleSelectCard(value)} + initiallyFocusedOptionKey={cardSelected} + shouldUpdateFocusedIndex + /> + + + )} + + ); +} + +CardSelectionStep.displayName = 'CardSelectionStep'; + +export default CardSelectionStep; diff --git a/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx new file mode 100644 index 000000000000..5eb39719c022 --- /dev/null +++ b/src/pages/workspace/companyCards/assignCard/ConfirmationStep.tsx @@ -0,0 +1,92 @@ +import React from 'react'; +import {View} from 'react-native'; +import {useOnyx} from 'react-native-onyx'; +import Button from '@components/Button'; +import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScrollView from '@components/ScrollView'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useSafePaddingBottomStyle from '@hooks/useSafePaddingBottomStyle'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import Navigation from '@navigation/Navigation'; +import * as CompanyCards from '@userActions/CompanyCards'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {AssignCardStep} from '@src/types/onyx/AssignCard'; + +function ConfirmationStep() { + const {translate} = useLocalize(); + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + const [assignCard] = useOnyx(ONYXKEYS.ASSIGN_CARD); + const safePaddingBottomStyle = useSafePaddingBottomStyle(); + + const data = assignCard?.data; + + const submit = () => { + Navigation.goBack(); + CompanyCards.clearAssignCardStepAndData(); + }; + + const editStep = (step: AssignCardStep) => { + CompanyCards.setAssignCardStepAndData({currentStep: step, isEditing: true}); + }; + + const handleBackButtonPress = () => { + CompanyCards.setAssignCardStepAndData({currentStep: CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE}); + }; + + return ( + + + {translate('workspace.companyCards.letsDoubleCheck')} + {translate('workspace.companyCards.confirmationDescription')} + editStep(CONST.COMPANY_CARD.STEP.ASSIGNEE)} + /> + editStep(CONST.COMPANY_CARD.STEP.CARD)} + /> + editStep(CONST.COMPANY_CARD.STEP.TRANSACTION_START_DATE)} + /> + +