Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Invoice collection account selector page #41511

Merged
4 changes: 4 additions & 0 deletions src/ROUTES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -792,6 +792,10 @@ const ROUTES = {
route: 'settings/workspaces/:policyID/accounting/xero/advanced',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced` as const,
},
POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR: {
route: 'settings/workspaces/:policyID/accounting/xero/advanced/invoice-account-selector',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/xero/advanced/invoice-account-selector` as const,
},
POLICY_ACCOUNTING_QUICKBOOKS_ONLINE_IMPORT: {
route: 'settings/workspaces/:policyID/accounting/quickbooks-online/import',
getRoute: (policyID: string) => `settings/workspaces/${policyID}/accounting/quickbooks-online/import` as const,
Expand Down
1 change: 1 addition & 0 deletions src/SCREENS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ const SCREENS = {
XERO_CUSTOMER: 'Policy_Acounting_Xero_Import_Customer',
XERO_TAXES: 'Policy_Accounting_Xero_Taxes',
XERO_ADVANCED: 'Policy_Accounting_Xero_Advanced',
XERO_INVOICE_ACCOUNT_SELECTOR: 'Policy_Accounting_Xero_Invoice_Account_Selector',
},
INITIAL: 'Workspace_Initial',
PROFILE: 'Workspace_Profile',
Expand Down
104 changes: 104 additions & 0 deletions src/components/SelectionScreen.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import React from 'react';
import useLocalize from '@hooks/useLocalize';
import type {PolicyAccessVariant} from '@pages/workspace/AccessOrNotFoundWrapper';
import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper';
import type {TranslationPaths} from '@src/languages/types';
import type {PolicyFeatureName} from '@src/types/onyx/Policy';
import HeaderWithBackButton from './HeaderWithBackButton';
import ScreenWrapper from './ScreenWrapper';
import SelectionList from './SelectionList';
import type RadioListItem from './SelectionList/RadioListItem';
import type TableListItem from './SelectionList/TableListItem';
import type {ListItem, SectionListDataType} from './SelectionList/types';
import type UserListItem from './SelectionList/UserListItem';

type SelectorType = ListItem & {
value: string;
};

type SelectionScreenProps = {
/** Used to set the testID for tests */
displayName: string;

/** Title of the selection component */
title: TranslationPaths;

/** Custom content to display in the header */
headerContent?: React.ReactNode;

/** Sections for the section list */
sections: Array<SectionListDataType<SelectorType>>;

/** Default renderer for every item in the list */
listItem: typeof RadioListItem | typeof UserListItem | typeof TableListItem;

/** Item `keyForList` to focus initially */
initiallyFocusedOptionKey?: string | null | undefined;

/** Callback to fire when a row is pressed */
onSelectRow: (selection: SelectorType) => void;

/** Callback to fire when back button is pressed */
onBackButtonPress: () => void;

/** The current policyID */
policyID: string;

/** Defines which types of access should be verified */
accessVariants?: PolicyAccessVariant[];

/** The current feature name that the user tries to get access to */
featureName?: PolicyFeatureName;

/** Whether or not to block user from accessing the page */
shouldBeBlocked?: boolean;
};

function SelectionScreen({
displayName,
title,
headerContent,
sections,
listItem,
initiallyFocusedOptionKey,
onSelectRow,
onBackButtonPress,
policyID,
accessVariants,
featureName,
shouldBeBlocked,
}: SelectionScreenProps) {
const {translate} = useLocalize();
return (
<AccessOrNotFoundWrapper
policyID={policyID}
accessVariants={accessVariants}
featureName={featureName}
shouldBeBlocked={shouldBeBlocked}
>
<ScreenWrapper
includeSafeAreaPaddingBottom={false}
testID={displayName}
>
<HeaderWithBackButton
title={translate(title)}
onBackButtonPress={onBackButtonPress}
/>
<SelectionList
onSelectRow={onSelectRow}
headerContent={headerContent}
sections={sections}
ListItem={listItem}
showScrollIndicator
shouldShowTooltips={false}
initiallyFocusedOptionKey={initiallyFocusedOptionKey}
/>
</ScreenWrapper>
</AccessOrNotFoundWrapper>
);
}

export type {SelectorType};

SelectionScreen.displayName = 'SelectionScreen';
export default SelectionScreen;
3 changes: 2 additions & 1 deletion src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1989,7 +1989,7 @@ export default {
qboInvoiceCollectionAccount: 'QuickBooks invoice collections account',
accountSelectDescription:
"As you've enabled sync reimbursed reports, you will need select the bank account your reimbursements are coming out of, and we'll create the payment in QuickBooks.",
invoiceAccountSelectDescription:
invoiceAccountSelectorDescription:
'If you are exporting invoices from Expensify to Quickbooks Online, this is the account the invoice will appear against once marked as paid.',
},
accounts: {
Expand Down Expand Up @@ -2033,6 +2033,7 @@ export default {
reimbursedReportsDescription: 'Any time a report is paid using Expensify ACH, the corresponding bill payment will be created in the Xero account below.',
xeroBillPaymentAccount: 'Xero Bill Payment Account',
xeroInvoiceCollectionAccount: 'Xero Invoice Collections Account',
invoiceAccountSelectorDescription: "As you've enabled exporting invoices from Expensify to Xero, this is the account the invoice will appear against once marked as paid.",
},
},
type: {
Expand Down
4 changes: 3 additions & 1 deletion src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2016,7 +2016,7 @@ export default {
qboInvoiceCollectionAccount: 'Cuenta de cobro de las facturas QuickBooks',
accountSelectDescription:
'Como has activado la sincronización de los informes de reembolso, tendrás que seleccionar la cuenta bancaria de la que saldrán tus reembolsos y crearemos el pago en QuickBooks.',
invoiceAccountSelectDescription:
invoiceAccountSelectorDescription:
'Si está exportando facturas de Expensify a Quickbooks Online, ésta es la cuenta en la que aparecerá la factura una vez marcada como pagada.',
},
accounts: {
Expand Down Expand Up @@ -2067,6 +2067,8 @@ export default {
'Cada vez que se pague un informe utilizando Expensify ACH, se creará el correspondiente pago de la factura en la cuenta de Xero indicadas a continuación.',
xeroBillPaymentAccount: 'Cuenta de pago de las facturas de Xero',
xeroInvoiceCollectionAccount: 'Cuenta de cobro de las facturas Xero',
invoiceAccountSelectorDescription:
'Como ha activado la exportación de facturas de Expensify a Xero, esta es la cuenta en la que aparecerá la factura una vez marcada como pagada.',
},
},
type: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ const SettingsModalStackNavigator = createModalStackNavigator<SettingsNavigatorP
[SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: () => require('../../../../pages/workspace/accounting/xero/import/XeroCustomerConfigurationPage').default as React.ComponentType,
[SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: () => require('../../../../pages/workspace/accounting/xero/XeroTaxesConfigurationPage').default as React.ComponentType,
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: () => require('../../../../pages/workspace/accounting/xero/advanced/XeroAdvancedPage').default as React.ComponentType,
[SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR]: () =>
require('../../../../pages/workspace/accounting/xero/advanced/XeroInvoiceAccountSelectorPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_FREQUENCY]: () => require('../../../../pages/workspace/workflows/WorkspaceAutoReportingFrequencyPage').default as React.ComponentType,
[SCREENS.WORKSPACE.WORKFLOWS_AUTO_REPORTING_MONTHLY_OFFSET]: () =>
require('../../../../pages/workspace/workflows/WorkspaceAutoReportingMonthlyOffsetPage').default as React.ComponentType,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial<Record<FullScreenName, string[]>> = {
SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER,
SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES,
SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED,
SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR,
],
[SCREENS.WORKSPACE.TAXES]: [
SCREENS.WORKSPACE.TAXES_SETTINGS,
Expand Down
1 change: 1 addition & 0 deletions src/libs/Navigation/linkingConfig/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ const config: LinkingOptions<RootStackParamList>['config'] = {
[SCREENS.WORKSPACE.ACCOUNTING.XERO_CUSTOMER]: {path: ROUTES.POLICY_ACCOUNTING_XERO_CUSTOMER.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_TAXES]: {path: ROUTES.POLICY_ACCOUNTING_XERO_TAXES.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: {path: ROUTES.POLICY_ACCOUNTING_XERO_ADVANCED.route},
[SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR]: {path: ROUTES.POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR.route},
[SCREENS.WORKSPACE.DESCRIPTION]: {
path: ROUTES.WORKSPACE_PROFILE_DESCRIPTION.route,
},
Expand Down
3 changes: 3 additions & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,9 @@ type SettingsNavigatorParamList = {
[SCREENS.WORKSPACE.ACCOUNTING.XERO_ADVANCED]: {
policyID: string;
};
[SCREENS.WORKSPACE.ACCOUNTING.XERO_INVOICE_ACCOUNT_SELECTOR]: {
policyID: string;
};
[SCREENS.GET_ASSISTANCE]: {
backTo: Routes;
};
Expand Down
8 changes: 6 additions & 2 deletions src/pages/workspace/AccessOrNotFoundWrapper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ type AccessOrNotFoundWrapperProps = AccessOrNotFoundWrapperOnyxProps & {

/** Props for customizing fallback pages */
fullPageNotFoundViewProps?: FullPageNotFoundViewProps;

/** Whether or not to block user from accessing the page */
shouldBeBlocked?: boolean;
} & Pick<FullPageNotFoundViewProps, 'subtitleKey' | 'onLinkPress'>;

type PageNotFoundFallbackProps = Pick<AccessOrNotFoundWrapperProps, 'policyID' | 'fullPageNotFoundViewProps'> & {shouldShowFullScreenFallback: boolean};
Expand All @@ -68,7 +71,7 @@ function PageNotFoundFallback({policyID, shouldShowFullScreenFallback, fullPageN
);
}

function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps, ...props}: AccessOrNotFoundWrapperProps) {
function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps, shouldBeBlocked, ...props}: AccessOrNotFoundWrapperProps) {
const {policy, policyID, featureName, isLoadingReportData} = props;

const isPolicyIDInRoute = !!policyID?.length;
Expand All @@ -92,7 +95,8 @@ function AccessOrNotFoundWrapper({accessVariants = [], fullPageNotFoundViewProps
return acc && accessFunction(policy);
}, true);

const shouldShowNotFoundPage = isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || !isPageAccessible || !isFeatureEnabled;
const shouldShowNotFoundPage =
isEmptyObject(policy) || (Object.keys(policy).length === 1 && !isEmptyObject(policy.errors)) || !policy?.id || !isPageAccessible || !isFeatureEnabled || shouldBeBlocked;

if (shouldShowFullScreenLoadingIndicator) {
return <FullscreenLoadingIndicator />;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ function QuickbooksAccountSelectPage({policy}: WithPolicyConnectionsProps) {
const listHeaderComponent = useMemo(
() => (
<View style={[styles.pb2, styles.ph5]}>
<Text style={[styles.pb5, styles.textNormal]}>{translate('workspace.qbo.advancedConfig.invoiceAccountSelectDescription')}</Text>
<Text style={[styles.pb5, styles.textNormal]}>{translate('workspace.qbo.advancedConfig.invoiceAccountSelectorDescription')}</Text>
</View>
),
[translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ function QuickbooksInvoiceAccountSelectPage({policy}: WithPolicyConnectionsProps
const listHeaderComponent = useMemo(
() => (
<View style={[styles.pb2, styles.ph5]}>
<Text style={[styles.pb5, styles.textNormal]}>{translate('workspace.qbo.advancedConfig.invoiceAccountSelectDescription')}</Text>
<Text style={[styles.pb5, styles.textNormal]}>{translate('workspace.qbo.advancedConfig.invoiceAccountSelectorDescription')}</Text>
</View>
),
[translate, styles.pb2, styles.ph5, styles.pb5, styles.textNormal],
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import React from 'react';
import React, {useMemo} from 'react';
import ConnectionLayout from '@components/ConnectionLayout';
import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription';
import OfflineWithFeedback from '@components/OfflineWithFeedback';
import useLocalize from '@hooks/useLocalize';
import useThemeStyles from '@hooks/useThemeStyles';
import * as Connections from '@libs/actions/connections';
import * as ErrorUtils from '@libs/ErrorUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections';
import withPolicyConnections from '@pages/workspace/withPolicyConnections';
import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow';
import * as Policy from '@userActions/Policy';
import CONST from '@src/CONST';
import ROUTES from '@src/ROUTES';

function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) {
const styles = useThemeStyles();
Expand All @@ -19,7 +21,14 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) {
const policyID = policy?.id ?? '';
const xeroConfig = policy?.connections?.xero?.config;
const {autoSync, pendingFields, sync} = xeroConfig ?? {};
const xeroData = policy?.connections?.xero?.data;
const {bankAccounts} = policy?.connections?.xero?.data ?? {};
const {invoiceCollectionsAccountID} = sync ?? {};

const selectedBankAccountName = useMemo(() => {
const selectedAccount = (bankAccounts ?? []).find((bank) => bank.id === invoiceCollectionsAccountID);

return selectedAccount?.name ?? '';
}, [bankAccounts, invoiceCollectionsAccountID]);

return (
<ConnectionLayout
Expand Down Expand Up @@ -48,7 +57,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) {
<OfflineWithFeedback pendingAction={pendingFields?.export}>
<MenuItemWithTopDescription
shouldShowRightIcon
title={xeroConfig?.export.billStatus.purchase}
title={xeroConfig?.export?.billStatus?.purchase}
description={translate('workspace.xero.advancedConfig.purchaseBillStatusTitle')}
key={translate('workspace.xero.advancedConfig.purchaseBillStatusTitle')}
wrapperStyle={[styles.sectionMenuItemTopDescription]}
Expand Down Expand Up @@ -76,7 +85,7 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) {
<OfflineWithFeedback pendingAction={pendingFields?.sync}>
<MenuItemWithTopDescription
shouldShowRightIcon
title={String(xeroData?.bankAccounts)}
title={String(bankAccounts)}
description={translate('workspace.xero.advancedConfig.xeroBillPaymentAccount')}
key={translate('workspace.xero.advancedConfig.xeroBillPaymentAccount')}
wrapperStyle={[styles.sectionMenuItemTopDescription]}
Expand All @@ -86,11 +95,13 @@ function XeroAdvancedPage({policy}: WithPolicyConnectionsProps) {
<OfflineWithFeedback pendingAction={pendingFields?.sync}>
<MenuItemWithTopDescription
shouldShowRightIcon
title={String(xeroData?.bankAccounts)}
title={String(selectedBankAccountName)}
description={translate('workspace.xero.advancedConfig.xeroInvoiceCollectionAccount')}
key={translate('workspace.xero.advancedConfig.xeroInvoiceCollectionAccount')}
wrapperStyle={[styles.sectionMenuItemTopDescription]}
onPress={() => {}}
onPress={() => {
Navigation.navigate(ROUTES.POLICY_ACCOUNTING_XERO_INVOICE_SELECTOR.getRoute(policyID));
}}
/>
</OfflineWithFeedback>
</>
Expand Down
Loading
Loading