diff --git a/src/components/ErrorMessageRow.tsx b/src/components/ErrorMessageRow.tsx new file mode 100644 index 0000000000000..2e6e414492748 --- /dev/null +++ b/src/components/ErrorMessageRow.tsx @@ -0,0 +1,43 @@ +import {mapValues} from 'lodash'; +import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import MessagesRow from './MessagesRow'; + +type ErrorMessageRowProps = { + /** The errors to display */ + errors?: OnyxCommon.Errors | ReceiptErrors | null; + + /** Additional style object for the error row */ + errorRowStyles?: StyleProp; + + /** A function to run when the X button next to the error is clicked */ + onClose?: () => void; + + /** Whether we can dismiss the error message */ + canDismissError?: boolean; +}; + +function ErrorMessageRow({errors, errorRowStyles, onClose, canDismissError = true}: ErrorMessageRowProps) { + // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. + const errorEntries = Object.entries(errors ?? {}); + const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError] => errorEntry[1] !== null); + const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => error); + const hasErrorMessages = !isEmptyObject(errorMessages); + + return hasErrorMessages ? ( + + ) : null; +} + +ErrorMessageRow.displayName = 'ErrorMessageRow'; + +export default ErrorMessageRow; diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index ac9eda4043e8e..1f4b56d87f173 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -1,4 +1,3 @@ -import {mapValues} from 'lodash'; import React, {useCallback} from 'react'; import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; @@ -10,11 +9,11 @@ import shouldRenderOffscreen from '@libs/shouldRenderOffscreen'; import type {AllStyles} from '@styles/utils/types'; import CONST from '@src/CONST'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; -import type {ReceiptError, ReceiptErrors} from '@src/types/onyx/Transaction'; +import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import CustomStylesForChildrenProvider from './CustomStylesForChildrenProvider'; -import MessagesRow from './MessagesRow'; +import ErrorMessageRow from './ErrorMessageRow'; /** * This component should be used when we are using the offline pattern B (offline with feedback). @@ -83,12 +82,6 @@ function OfflineWithFeedback({ const hasErrors = !isEmptyObject(errors ?? {}); - // Some errors have a null message. This is used to apply opacity only and to avoid showing redundant messages. - const errorEntries = Object.entries(errors ?? {}); - const filteredErrorEntries = errorEntries.filter((errorEntry): errorEntry is [string, string | ReceiptError] => errorEntry[1] !== null); - const errorMessages = mapValues(Object.fromEntries(filteredErrorEntries), (error) => error); - - const hasErrorMessages = !isEmptyObject(errorMessages); const isOfflinePendingAction = !!isOffline && !!pendingAction; const isUpdateOrDeleteError = hasErrors && (pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE); const isAddError = hasErrors && pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD; @@ -139,13 +132,12 @@ function OfflineWithFeedback({ {children} )} - {shouldShowErrorMessages && hasErrorMessages && ( - )} diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index eb2e66ad9a78a..6cc6b403dc7ab 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -67,6 +67,7 @@ function BaseSelectionList( showConfirmButton = false, shouldPreventDefaultFocusOnSelectRow = false, containerStyle, + sectionListStyle, disableKeyboardShortcuts = false, children, shouldStopPropagation = false, @@ -729,7 +730,7 @@ function BaseSelectionList( viewabilityConfig={{viewAreaCoveragePercentThreshold: 95}} testID="selection-list" onLayout={onSectionListLayout} - style={(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0} + style={[(!maxToRenderPerBatch || (shouldHideListOnInitialRender && isInitialSectionListRender)) && styles.opacity0, sectionListStyle]} ListHeaderComponent={listHeaderContent} ListFooterComponent={listFooterContent ?? ShowMoreButtonInstance} ListEmptyComponent={listEmptyContent} diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index bfab13e895ccc..723e48699b3c6 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -413,6 +413,9 @@ type BaseSelectionListProps = Partial & { /** Styles to apply to SelectionList container */ containerStyle?: StyleProp; + /** Styles to apply to SectionList component */ + sectionListStyle?: StyleProp; + /** Whether focus event should be delayed */ shouldDelayFocus?: boolean; diff --git a/src/components/SelectionScreen.tsx b/src/components/SelectionScreen.tsx index 8e1b0a88c875d..0e3f0d84e10e1 100644 --- a/src/components/SelectionScreen.tsx +++ b/src/components/SelectionScreen.tsx @@ -11,6 +11,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import type {ConnectionName, PolicyFeatureName} from '@src/types/onyx/Policy'; import type {ReceiptErrors} from '@src/types/onyx/Transaction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import ErrorMessageRow from './ErrorMessageRow'; import HeaderWithBackButton from './HeaderWithBackButton'; import OfflineWithFeedback from './OfflineWithFeedback'; import ScreenWrapper from './ScreenWrapper'; @@ -128,9 +129,6 @@ function SelectionScreen({ {headerContent} @@ -143,7 +141,14 @@ function SelectionScreen({ initiallyFocusedOptionKey={initiallyFocusedOptionKey} listEmptyContent={listEmptyContent} listFooterContent={listFooterContent} - /> + sectionListStyle={[styles.flexGrow0]} + > + + diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 8715ea4b96bb1..1de45a79a3626 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -530,14 +530,14 @@ function clearNetSuiteErrorField(policyID: string, fieldName: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {options: {config: {errorFields: {[fieldName]: null}}}}}}); } -function clearNetSuiteAutoSyncErrorField(policyID: string) { - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {config: {errorFields: {autoSync: null}}}}}); -} - function clearSageIntacctErrorField(policyID: string, fieldName: string) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {intacct: {config: {errorFields: {[fieldName]: null}}}}}); } +function clearNetSuiteAutoSyncErrorField(policyID: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {connections: {netsuite: {config: {errorFields: {autoSync: null}}}}}); +} + function setWorkspaceReimbursement(policyID: string, reimbursementChoice: ValueOf, reimburserEmail: string) { const policy = getPolicy(policyID); @@ -3127,6 +3127,7 @@ export { generateCustomUnitID, clearQBOErrorField, clearXeroErrorField, + clearSageIntacctErrorField, clearNetSuiteErrorField, clearNetSuiteAutoSyncErrorField, clearWorkspaceReimbursementErrors, @@ -3144,7 +3145,6 @@ export { openPolicyExpensifyCardsPage, requestExpensifyCardLimitIncrease, getAdminPoliciesConnectedToSageIntacct, - clearSageIntacctErrorField, }; export type {NewCustomUnit}; diff --git a/src/libs/actions/connections/SageIntacct.ts b/src/libs/actions/connections/SageIntacct.ts index 2b388b632d062..5ac8b54656419 100644 --- a/src/libs/actions/connections/SageIntacct.ts +++ b/src/libs/actions/connections/SageIntacct.ts @@ -11,6 +11,7 @@ import type { Connections, SageIntacctConnectionsConfig, SageIntacctDimension, + SageIntacctExportConfig, SageIntacctMappingName, SageIntacctMappingType, SageIntacctMappingValue, @@ -316,7 +317,7 @@ function removeSageIntacctUserDimensions(policyID: string, dimensionName: string ); } -function prepareOnyxDataForExportUpdate(policyID: string, settingName: keyof Connections['intacct']['config']['export'], settingValue: string | null) { +function prepareOnyxDataForExportUpdate(policyID: string, settingName: keyof SageIntacctExportConfig, settingValue: string | null) { const optimisticData: OnyxUpdate[] = [ { onyxMethod: Onyx.METHOD.MERGE, @@ -327,12 +328,12 @@ function prepareOnyxDataForExportUpdate(policyID: string, settingName: keyof Con config: { export: { [settingName]: settingValue, - pendingFields: { - [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, - }, - errorFields: { - [settingName]: null, - }, + }, + pendingFields: { + [settingName]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + }, + errorFields: { + [settingName]: null, }, }, }, @@ -349,14 +350,11 @@ function prepareOnyxDataForExportUpdate(policyID: string, settingName: keyof Con connections: { intacct: { config: { - export: { - [settingName]: settingValue, - pendingFields: { - [settingName]: null, - }, - errorFields: { - [settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), - }, + pendingFields: { + [settingName]: null, + }, + errorFields: { + [settingName]: ErrorUtils.getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, }, }, @@ -373,14 +371,11 @@ function prepareOnyxDataForExportUpdate(policyID: string, settingName: keyof Con connections: { intacct: { config: { - export: { - [settingName]: settingValue, - pendingFields: { - [settingName]: null, - }, - errorFields: { - [settingName]: null, - }, + pendingFields: { + [settingName]: null, + }, + errorFields: { + [settingName]: null, }, }, }, @@ -472,7 +467,7 @@ function updateSageIntacctNonreimbursableExpensesExportVendor(policyID: string, API.write(WRITE_COMMANDS.UPDATE_SAGE_INTACCT_NON_REIMBURSABLE_EXPENSES_EXPORT_VENDOR, parameters, {optimisticData, failureData, successData}); } -function updateSageIntacctDefaultVendor(policyID: string, settingName: keyof Connections['intacct']['config']['export'], vendor: string) { +function updateSageIntacctDefaultVendor(policyID: string, settingName: keyof SageIntacctExportConfig, vendor: string) { if (settingName === CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR) { updateSageIntacctReimbursableExpensesReportExportDefaultVendor(policyID, vendor); } else if (settingName === CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR) { diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx index 1f688404ab0a7..40ab18c7f9cd3 100644 --- a/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctAdvancedPage.tsx @@ -118,7 +118,7 @@ function SageIntacctAdvancedPage({policy}: WithPolicyProps) { switchAccessibilityLabel={section.label} isActive={section.isActive} onToggle={section.onToggle} - wrapperStyle={[styles.ph5, styles.pv5]} + wrapperStyle={[styles.ph5, styles.pb3]} pendingAction={section.pendingAction} errors={section.error} onCloseError={section.onCloseError} diff --git a/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx b/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx index dadd1dc0af2c0..3a9bd67b1e48e 100644 --- a/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx +++ b/src/pages/workspace/accounting/intacct/advanced/SageIntacctPaymentAccountPage.tsx @@ -65,7 +65,7 @@ function SageIntacctPaymentAccountPage({policy}: WithPolicyConnectionsProps) { connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} pendingAction={config?.pendingFields?.reimbursementAccountID} errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID)} - errorRowStyles={[styles.ph5, styles.mv2]} + errorRowStyles={[styles.ph5, styles.pv3]} onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSEMENT_ACCOUNT_ID)} /> ); diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx index 51d08423e530c..a0761a4e334be 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDatePage.tsx @@ -8,10 +8,12 @@ import type {SelectorType} from '@components/SelectionScreen'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import {updateSageIntacctExportDate} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -23,6 +25,7 @@ function SageIntacctDatePage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; const styles = useThemeStyles(); + const {config} = policy?.connections?.intacct ?? {}; const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; const data: MenuListItem[] = Object.values(CONST.SAGE_INTACCT_EXPORT_DATE).map((dateType) => ({ value: dateType, @@ -65,6 +68,10 @@ function SageIntacctDatePage({policy}: WithPolicyProps) { featureName={CONST.POLICY.MORE_FEATURES.ARE_CONNECTIONS_ENABLED} onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.exportDate} + errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.EXPORT_DATE)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.EXPORT_DATE)} /> ); } diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx index 4782cfba9e976..43f2e289409c5 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctDefaultVendorPage.tsx @@ -10,11 +10,13 @@ import SelectionScreen from '@components/SelectionScreen'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getSageIntacctNonReimbursableActiveDefaultVendor, getSageIntacctVendors} from '@libs/PolicyUtils'; import type {SettingsNavigatorParamList} from '@navigation/types'; import variables from '@styles/variables'; import {updateSageIntacctDefaultVendor} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -29,20 +31,22 @@ function SageIntacctDefaultVendorPage({route}: SageIntacctDefaultVendorPageProps const policyID = route.params.policyID ?? '-1'; const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); + const {config} = policy?.connections?.intacct ?? {}; + const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; const isReimbursable = route.params.reimbursable === CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE; let defaultVendor; let settingName: keyof Connections['intacct']['config']['export']; if (!isReimbursable) { - const {nonReimbursable} = policy?.connections?.intacct?.config.export ?? {}; + const {nonReimbursable} = exportConfig ?? {}; defaultVendor = getSageIntacctNonReimbursableActiveDefaultVendor(policy); settingName = nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE ? CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR : CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_VENDOR; } else { - const {reimbursableExpenseReportDefaultVendor} = policy?.connections?.intacct?.config.export ?? {}; + const {reimbursableExpenseReportDefaultVendor} = exportConfig ?? {}; defaultVendor = reimbursableExpenseReportDefaultVendor; settingName = CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR; } @@ -103,6 +107,10 @@ function SageIntacctDefaultVendorPage({route}: SageIntacctDefaultVendorPageProps listEmptyContent={listEmptyContent} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.[settingName]} + errors={ErrorUtils.getLatestErrorField(config, settingName)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, settingName)} /> ); } diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx index 508f39add2e0e..5c3c977a93a51 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctExportPage.tsx @@ -16,6 +16,7 @@ function SageIntacctExportPage({policy}: WithPolicyProps) { const styles = useThemeStyles(); const policyID = policy?.id ?? '-1'; + const {config} = policy?.connections?.intacct ?? {}; const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; const sections = useMemo( @@ -24,15 +25,15 @@ function SageIntacctExportPage({policy}: WithPolicyProps) { description: translate('workspace.sageIntacct.preferredExporter'), action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_PREFERRED_EXPORTER.getRoute(policyID)), title: exportConfig?.exporter ?? translate('workspace.sageIntacct.notConfigured'), - hasError: !!exportConfig?.errorFields?.exporter, - pendingAction: exportConfig?.pendingFields?.exporter, + hasError: !!config?.errorFields?.exporter, + pendingAction: config?.pendingFields?.exporter, }, { description: translate('workspace.sageIntacct.exportDate.label'), action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT_DATE.getRoute(policyID)), title: exportConfig?.exportDate ? translate(`workspace.sageIntacct.exportDate.values.${exportConfig.exportDate}.label`) : translate(`workspace.sageIntacct.notConfigured`), - hasError: !!exportConfig?.errorFields?.exportDate, - pendingAction: exportConfig?.pendingFields?.exportDate, + hasError: !!config?.errorFields?.exportDate, + pendingAction: config?.pendingFields?.exportDate, }, { description: translate('workspace.sageIntacct.reimbursableExpenses.label'), @@ -40,8 +41,8 @@ function SageIntacctExportPage({policy}: WithPolicyProps) { title: exportConfig?.reimbursable ? translate(`workspace.sageIntacct.reimbursableExpenses.values.${exportConfig.reimbursable}`) : translate('workspace.sageIntacct.notConfigured'), - hasError: !!exportConfig?.errorFields?.reimbursable, - pendingAction: exportConfig?.pendingFields?.reimbursable, + hasError: !!config?.errorFields?.reimbursable || !!config?.errorFields?.reimbursableExpenseReportDefaultVendor, + pendingAction: config?.pendingFields?.reimbursable ?? config?.pendingFields?.reimbursableExpenseReportDefaultVendor, }, { description: translate('workspace.sageIntacct.nonReimbursableExpenses.label'), @@ -49,11 +50,21 @@ function SageIntacctExportPage({policy}: WithPolicyProps) { title: exportConfig?.nonReimbursable ? translate(`workspace.sageIntacct.nonReimbursableExpenses.values.${exportConfig.nonReimbursable}`) : translate('workspace.sageIntacct.notConfigured'), - hasError: !!exportConfig?.errorFields?.nonReimbursable, - pendingAction: exportConfig?.pendingFields?.nonReimbursable, + hasError: + !!config?.errorFields?.nonReimbursable ?? + !!config?.errorFields?.nonReimbursableAccount ?? + config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ? !!config?.errorFields?.nonReimbursableVendor + : !!config?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, + pendingAction: + config?.pendingFields?.nonReimbursable ?? + config?.errorFields?.nonReimbursableAccount ?? + config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL + ? config?.pendingFields?.nonReimbursableVendor + : config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, }, ], - [exportConfig, policyID, translate], + [config, exportConfig, policyID, translate], ); return ( diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx index f22b653b26f9a..1b191cc536277 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableCreditCardAccountPage.tsx @@ -6,12 +6,14 @@ import type {SelectorType} from '@components/SelectionScreen'; import SelectionScreen from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getSageIntacctCreditCards} from '@libs/PolicyUtils'; import type {WithPolicyConnectionsProps} from '@pages/workspace/withPolicyConnections'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import variables from '@styles/variables'; import {updateSageIntacctNonreimbursableExpensesExportAccount} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -20,7 +22,7 @@ function SageIntacctNonReimbursableCreditCardAccountPage({policy}: WithPolicyCon const {translate} = useLocalize(); const policyID = policy?.id ?? '-1'; - + const {config} = policy?.connections?.intacct ?? {}; const {export: exportConfig} = policy?.connections?.intacct?.config ?? {}; const creditCardSelectorOptions = useMemo(() => getSageIntacctCreditCards(policy, exportConfig?.nonReimbursableAccount), [exportConfig?.nonReimbursableAccount, policy]); @@ -63,6 +65,10 @@ function SageIntacctNonReimbursableCreditCardAccountPage({policy}: WithPolicyCon listEmptyContent={listEmptyContent} accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN, CONST.POLICY.ACCESS_VARIANTS.PAID]} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.nonReimbursableAccount} + errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_ACCOUNT)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_ACCOUNT)} /> ); } diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx index ba977cbba2384..c87d33857513e 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctNonReimbursableExpensesPage.tsx @@ -10,12 +10,14 @@ import type {ListItem} from '@components/SelectionList/types'; import type {SelectorType} from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import {getSageIntacctNonReimbursableActiveDefaultVendor} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import {updateSageIntacctDefaultVendor, updateSageIntacctNonreimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {SageIntacctDataElementWithValue} from '@src/types/onyx/Policy'; @@ -61,12 +63,12 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyProps) { title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), hasError: config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL - ? !!config?.export?.errorFields?.nonReimbursableVendor - : !!config?.export?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, + ? !!config?.errorFields?.nonReimbursableVendor + : !!config?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, pendingAction: config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL - ? config?.export?.pendingFields?.nonReimbursableVendor - : config?.export?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, + ? config?.pendingFields?.nonReimbursableVendor + : config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, }; return ( @@ -84,11 +86,11 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyProps) { ); }, [ - config?.export?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, - config?.export?.errorFields?.nonReimbursableVendor, + config?.errorFields?.nonReimbursableCreditCardChargeDefaultVendor, + config?.errorFields?.nonReimbursableVendor, config?.export.nonReimbursable, - config?.export?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, - config?.export?.pendingFields?.nonReimbursableVendor, + config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor, + config?.pendingFields?.nonReimbursableVendor, intacctData?.vendors, policy, policyID, @@ -100,8 +102,8 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyProps) { description: translate('workspace.sageIntacct.creditCardAccount'), action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_NON_REIMBURSABLE_CREDIT_CARD_ACCOUNT.getRoute(policyID)), title: config?.export.nonReimbursableAccount ? config.export.nonReimbursableAccount : translate('workspace.sageIntacct.notConfigured'), - hasError: !!config?.export?.errorFields?.nonReimbursableAccount, - pendingAction: config?.export?.pendingFields?.nonReimbursableAccount, + hasError: !!config?.errorFields?.nonReimbursableAccount, + pendingAction: config?.pendingFields?.nonReimbursableAccount, }; return ( @@ -118,7 +120,7 @@ function SageIntacctNonReimbursableExpensesPage({policy}: WithPolicyProps) { /> ); - }, [config?.export?.errorFields?.nonReimbursableAccount, config?.export.nonReimbursableAccount, config?.export?.pendingFields?.nonReimbursableAccount, policyID, translate]); + }, [config?.errorFields?.nonReimbursableAccount, config?.export.nonReimbursableAccount, config?.pendingFields?.nonReimbursableAccount, policyID, translate]); return ( - selectNonReimbursableExpense(selection as MenuListItem)} - sections={[{data}]} - ListItem={RadioListItem} - showScrollIndicator - shouldShowTooltips={false} - listFooterContent={ + Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE)} + style={[styles.flexGrow1, styles.flexShrink1]} + contentContainerStyle={[styles.flexGrow1, styles.flexShrink1]} + > + selectNonReimbursableExpense(selection as MenuListItem)} + sections={[{data}]} + ListItem={RadioListItem} + showScrollIndicator + shouldShowTooltips={false} + containerStyle={[styles.flexReset, styles.flexGrow1, styles.flexShrink1, styles.pb0]} + /> + + + {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL && defaultVendor} + {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && ( - {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.VENDOR_BILL && defaultVendor} - {config?.export.nonReimbursable === CONST.SAGE_INTACCT_NON_REIMBURSABLE_EXPENSE_TYPE.CREDIT_CARD_CHARGE && ( - - {creditCardAccount} - { - const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; - updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor); - }} - wrapperStyle={[styles.ph5, styles.pv3]} - pendingAction={config?.export?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor} - /> - {!!config?.export.nonReimbursableCreditCardChargeDefaultVendor && defaultVendor} - - )} + {creditCardAccount} + { + const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; + updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR, vendor); + }} + wrapperStyle={[styles.ph5, styles.pv3]} + pendingAction={config?.pendingFields?.nonReimbursableCreditCardChargeDefaultVendor} + errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR)} + onCloseError={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.NON_REIMBURSABLE_CREDIT_CARD_VENDOR)} + /> + {!!config?.export.nonReimbursableCreditCardChargeDefaultVendor && defaultVendor} - } - /> + )} + ); } diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx index abbb87982a5e1..5d248aec382dd 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctPreferredExporterPage.tsx @@ -8,11 +8,13 @@ import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import {getAdminEmployees, isExpensifyTeam} from '@libs/PolicyUtils'; import Navigation from '@navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import {updateSageIntacctExporter} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; @@ -24,6 +26,7 @@ function SageIntacctPreferredExporterPage({policy}: WithPolicyProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policyOwner = policy?.owner ?? ''; + const {config} = policy?.connections?.intacct ?? {}; const {export: exportConfiguration} = policy?.connections?.intacct?.config ?? {}; const exporters = getAdminEmployees(policy); const {login: currentUserLogin} = useCurrentUserPersonalDetails(); @@ -95,6 +98,10 @@ function SageIntacctPreferredExporterPage({policy}: WithPolicyProps) { onBackButtonPress={() => Navigation.goBack(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_EXPORT.getRoute(policyID))} title="workspace.sageIntacct.preferredExporter" connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} + pendingAction={config?.pendingFields?.exporter} + errors={ErrorUtils.getLatestErrorField(config ?? {}, CONST.SAGE_INTACCT_CONFIG.EXPORTER)} + errorRowStyles={[styles.ph5, styles.pv3]} + onClose={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.EXPORTER)} /> ); } diff --git a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx index b3a4a04a55823..f030943936676 100644 --- a/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx +++ b/src/pages/workspace/accounting/intacct/export/SageIntacctReimbursableExpensesPage.tsx @@ -10,11 +10,13 @@ import type {ListItem} from '@components/SelectionList/types'; import type {SelectorType} from '@components/SelectionScreen'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ErrorUtils from '@libs/ErrorUtils'; import Navigation from '@navigation/Navigation'; import type {WithPolicyProps} from '@pages/workspace/withPolicy'; import withPolicyConnections from '@pages/workspace/withPolicyConnections'; import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; import {updateSageIntacctDefaultVendor, updateSageIntacctReimbursableExpensesExportDestination} from '@userActions/connections/SageIntacct'; +import * as Policy from '@userActions/Policy/Policy'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import type {SageIntacctDataElementWithValue} from '@src/types/onyx/Policy'; @@ -59,8 +61,8 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyProps) { description: translate('workspace.sageIntacct.defaultVendor'), action: () => Navigation.navigate(ROUTES.POLICY_ACCOUNTING_SAGE_INTACCT_DEFAULT_VENDOR.getRoute(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)), title: defaultVendorName && defaultVendorName !== '' ? defaultVendorName : translate('workspace.sageIntacct.notConfigured'), - hasError: !!config?.export?.errorFields?.reimbursableExpenseReportDefaultVendor, - pendingAction: config?.export?.pendingFields?.reimbursableExpenseReportDefaultVendor, + hasError: !!config?.errorFields?.reimbursableExpenseReportDefaultVendor, + pendingAction: config?.pendingFields?.reimbursableExpenseReportDefaultVendor, }; return ( @@ -78,8 +80,8 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyProps) { ); }, [ - config?.export?.errorFields?.reimbursableExpenseReportDefaultVendor, - config?.export?.pendingFields?.reimbursableExpenseReportDefaultVendor, + config?.errorFields?.reimbursableExpenseReportDefaultVendor, + config?.pendingFields?.reimbursableExpenseReportDefaultVendor, intacctData?.vendors, policyID, reimbursableExpenseReportDefaultVendor, @@ -97,36 +99,46 @@ function SageIntacctReimbursableExpensesPage({policy}: WithPolicyProps) { displayName={SageIntacctReimbursableExpensesPage.displayName} policyID={policyID} connectionName={CONST.POLICY.CONNECTIONS.NAME.SAGE_INTACCT} - contentContainerStyle={[styles.flex1]} shouldUseScrollView={false} + shouldIncludeSafeAreaPaddingBottom > - selectReimbursableDestination(selection as MenuListItem)} - sections={[{data}]} - ListItem={RadioListItem} - showScrollIndicator - shouldShowTooltips={false} - listFooterContent={ - reimbursable === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT ? ( - - { - const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; - updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor); - }} - wrapperStyle={[styles.ph5, styles.pv3]} - pendingAction={config?.export?.pendingFields?.reimbursableExpenseReportDefaultVendor} - /> - {!!reimbursableExpenseReportDefaultVendor && defaultVendor} - - ) : undefined - } - /> + Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE)} + style={[styles.flexGrow1, styles.flexShrink1]} + contentContainerStyle={[styles.flexGrow1, styles.flexShrink1]} + > + selectReimbursableDestination(selection as MenuListItem)} + sections={[{data}]} + ListItem={RadioListItem} + showScrollIndicator + shouldShowTooltips={false} + containerStyle={[styles.flexReset, styles.flexGrow1, styles.flexShrink1, styles.pb0]} + /> + + {reimbursable === CONST.SAGE_INTACCT_REIMBURSABLE_EXPENSE_TYPE.EXPENSE_REPORT && ( + + { + const vendor = enabled ? policy?.connections?.intacct?.data?.vendors?.[0].id ?? '' : ''; + updateSageIntacctDefaultVendor(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR, vendor); + }} + pendingAction={config?.pendingFields?.reimbursableExpenseReportDefaultVendor} + errors={ErrorUtils.getLatestErrorField(config, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR)} + wrapperStyle={[styles.ph5, styles.pv3]} + onCloseError={() => Policy.clearSageIntacctErrorField(policyID, CONST.SAGE_INTACCT_CONFIG.REIMBURSABLE_VENDOR)} + /> + {!!reimbursableExpenseReportDefaultVendor && defaultVendor} + + )} ); } diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index fe786208d17a4..93c7351e8209b 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1078,6 +1078,33 @@ type SageIntacctSyncConfig = { syncReimbursedReports: boolean | string; }; +/** Sage Intacct export configs */ +type SageIntacctExportConfig = { + /** Export date type */ + exportDate: ValueOf; + + /** The e-mail of the exporter */ + exporter: string; + + /** Defines how non-reimbursable expenses are exported */ + nonReimbursable: ValueOf; + + /** Account that receives the non-reimbursable expenses */ + nonReimbursableAccount: string; + + /** Default vendor used for credit card transactions of non-reimbursable bill */ + nonReimbursableCreditCardChargeDefaultVendor: string; + + /** Default vendor of non-reimbursable bill */ + nonReimbursableVendor: string; + + /** Defines how reimbursable expenses are exported */ + reimbursable: ValueOf; + + /** Default vendor of reimbursable bill */ + reimbursableExpenseReportDefaultVendor: string; +}; + /** * Connection config for Sage Intacct */ @@ -1113,34 +1140,7 @@ type SageIntacctConnectionsConfig = OnyxCommon.OnyxValueWithOfflineFeedback< }; /** Sage Intacct export configs */ - export: OnyxCommon.OnyxValueWithOfflineFeedback<{ - /** Export date type */ - exportDate: ValueOf; - - /** The e-mail of the exporter */ - exporter: string; - - /** Defines how non-reimbursable expenses are exported */ - nonReimbursable: ValueOf; - - /** Account that receives the non-reimbursable expenses */ - nonReimbursableAccount: string; - - /** Default vendor used for credit card transactions of non-reimbursable bill */ - nonReimbursableCreditCardChargeDefaultVendor: string; - - /** Default vendor of non-reimbursable bill */ - nonReimbursableVendor: string; - - /** Defines how reimbursable expenses are exported */ - reimbursable: ValueOf; - - /** Default vendor of reimbursable bill */ - reimbursableExpenseReportDefaultVendor: string; - - /** Collection of mapping field errors, which will be triggered when update action fails */ - errorFields?: OnyxCommon.ErrorFields; - }>; + export: SageIntacctExportConfig; /** Whether employees should be imported from Sage Intacct */ importEmployees: boolean; @@ -1163,7 +1163,7 @@ type SageIntacctConnectionsConfig = OnyxCommon.OnyxValueWithOfflineFeedback< /** Collection of form field errors */ errorFields?: OnyxCommon.ErrorFields; }, - SageIntacctOfflineStateKeys | keyof SageIntacctSyncConfig | keyof SageIntacctAutoSyncConfig + SageIntacctOfflineStateKeys | keyof SageIntacctSyncConfig | keyof SageIntacctAutoSyncConfig | keyof SageIntacctExportConfig >; /** State of integration connection */ @@ -1568,4 +1568,5 @@ export type { NetSuiteMappingValues, SageIntacctDataElement, SageIntacctConnectionsConfig, + SageIntacctExportConfig, };