From 393ae0a876ba4fa2412802534c0d20cf35949d64 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Fri, 31 Oct 2025 02:37:51 +0530 Subject: [PATCH 01/10] Implement invoice fields feature --- src/CONST/index.ts | 5 + src/ROUTES.ts | 10 +- src/languages/en.ts | 7 + src/languages/es.ts | 7 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 1 + src/libs/actions/Policy/ReportField.ts | 11 +- .../WorkspaceInvoiceFieldsSection.tsx | 231 ++++++++++++++++++ .../invoices/WorkspaceInvoicesPage.tsx | 2 + .../reports/CreateReportFieldsPage.tsx | 37 ++- .../reports/WorkspaceReportsPage.tsx | 4 +- 11 files changed, 302 insertions(+), 16 deletions(-) create mode 100644 src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 90f3d84db9d97..7d59b28cebdc7 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -7038,6 +7038,11 @@ const CONST = { LIST: 'dropdown', FORMULA: 'formula', }, + REPORT_FIELD_TARGETS: { + EXPENSE: 'expense', + INVOICE: 'invoice', + PAYCHECK: 'paycheck', + }, NAVIGATION_ACTIONS: { RESET: 'RESET', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index bad346a8f0d03..a2791b730df40 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1999,7 +1999,15 @@ const ROUTES = { }, WORKSPACE_CREATE_REPORT_FIELD: { route: 'workspaces/:policyID/reports/newReportField', - getRoute: (policyID: string) => `workspaces/${policyID}/reports/newReportField` as const, + getRoute: (policyID: string, target?: ValueOf) => { + const baseRoute = `workspaces/${policyID}/reports/newReportField` as const; + + if (!target) { + return baseRoute; + } + + return `${baseRoute}?target=${encodeURIComponent(target)}` as const; + }, }, WORKSPACE_REPORT_FIELDS_SETTINGS: { route: 'workspaces/:policyID/reports/:reportFieldID/edit', diff --git a/src/languages/en.ts b/src/languages/en.ts index e8f5ad9ba9b33..a5ab49c2fcf9c 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3536,6 +3536,7 @@ const translations = { customFieldHint: 'Add custom coding that applies to all spend from this member.', reports: 'Reports', reportFields: 'Report fields', + invoiceFields: 'Invoice fields', reportTitle: 'Report title', reportField: 'Report field', taxes: 'Taxes', @@ -4900,6 +4901,12 @@ const translations = { reportFieldInitialValueRequiredError: 'Please choose a report field initial value', genericFailureMessage: 'An error occurred while updating the report field. Please try again.', }, + invoiceFields: { + subtitle: "Invoice fields can be helpful when you'd like to include extra information.", + importedFromAccountingSoftware: 'The invoice fields below are imported from your', + disableInvoiceFields: 'Disable invoice fields', + disableInvoiceFieldsConfirmation: 'Are you sure? Invoice fields will be disabled on invoices.', + }, tags: { tagName: 'Tag name', requiresTag: 'Members must tag all expenses', diff --git a/src/languages/es.ts b/src/languages/es.ts index 7e3cbac17f669..b5d6f63d10d83 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3528,6 +3528,7 @@ ${amount} para ${merchant} - ${date}`, customFieldHint: 'Añade una codificación personalizada que se aplique a todos los gastos de este miembro.', reports: 'Informes', reportFields: 'Campos de informe', + invoiceFields: 'Campos de factura', reportTitle: 'El título del informe.', taxes: 'Impuestos', bills: 'Pagar facturas', @@ -4914,6 +4915,12 @@ ${amount} para ${merchant} - ${date}`, reportFieldInitialValueRequiredError: 'Elige un valor inicial de campo de informe', genericFailureMessage: 'Se ha producido un error al actualizar el campo de informe. Por favor, inténtalo de nuevo.', }, + invoiceFields: { + subtitle: 'Los campos de factura pueden ayudarte cuando quieras incluir información adicional.', + importedFromAccountingSoftware: 'Campos de factura importados desde', + disableInvoiceFields: 'Desactivar campos de factura', + disableInvoiceFieldsConfirmation: '¿Estás seguro? Los campos de factura se desactivarán en las facturas.', + }, tags: { tagName: 'Nombre de etiqueta', requiresTag: 'Los miembros deben etiquetar todos los gastos', diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 0efb1dacd98fa..6acdc4043a8dc 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -957,6 +957,9 @@ const config: LinkingOptions['config'] = { }, [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { path: ROUTES.WORKSPACE_CREATE_REPORT_FIELD.route, + parse: { + target: (value: string) => value, + }, }, [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { path: ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8e41ba3e4f303..38b606f7b2d1e 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -534,6 +534,7 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { policyID: string; + target?: ValueOf; }; [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { policyID: string; diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index 670f393850c79..c7769718a3341 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -51,7 +51,8 @@ type DeleteReportFieldsListValueParams = { type CreateReportFieldParams = Pick & { listValues: string[]; disabledListValues: boolean[]; - policyExpenseReportIDs: Array | undefined; + policyReportIDs: Array | undefined; + target?: PolicyReportField['target']; policy: OnyxEntry; }; @@ -167,7 +168,7 @@ function deleteReportFieldsListValue({valueIndexes, listValues, disabledListValu /** * Creates a new report field. */ -function createReportField({name, type, initialValue, listValues, disabledListValues, policyExpenseReportIDs, policy}: CreateReportFieldParams) { +function createReportField({name, type, initialValue, listValues, disabledListValues, policyReportIDs, target = CONST.REPORT_FIELD_TARGETS.EXPENSE, policy}: CreateReportFieldParams) { if (!policy) { Log.warn('Policy data is not present'); return; @@ -179,7 +180,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa const optimisticReportFieldDataForPolicy: Omit, 'value'> = { name, type, - target: 'expense', + target, defaultValue: initialValue, values: listValues, disabledOptions: disabledListValues, @@ -202,7 +203,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa errorFields: null, }, }, - ...(policyExpenseReportIDs ?? []).map((reportID) => ({ + ...(policyReportIDs ?? []).map((reportID) => ({ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, onyxMethod: Onyx.METHOD.MERGE, value: { @@ -226,7 +227,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa }, }, }, - ...(policyExpenseReportIDs ?? []).map((reportID) => ({ + ...(policyReportIDs ?? []).map((reportID) => ({ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, onyxMethod: Onyx.METHOD.MERGE, value: { diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx new file mode 100644 index 0000000000000..d3e5047d86685 --- /dev/null +++ b/src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx @@ -0,0 +1,231 @@ +import {FlashList} from '@shopify/flash-list'; +import type {ListRenderItemInfo} from '@shopify/flash-list'; +import {Str} from 'expensify-common'; +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import ConfirmModal from '@components/ConfirmModal'; +import ImportedFromAccountingSoftware from '@components/ImportedFromAccountingSoftware'; +import {Plus} from '@components/Icon/Expensicons'; +import MenuItem from '@components/MenuItem'; +import OfflineWithFeedback from '@components/OfflineWithFeedback'; +import Section from '@components/Section'; +import ToggleSettingOptionRow from '@pages/workspace/workflows/ToggleSettingsOptionRow'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; +import {enablePolicyReportFields} from '@libs/actions/Policy/Policy'; +import Navigation from '@libs/Navigation/Navigation'; +import {getConnectedIntegration, getCurrentConnectionName, hasAccountingConnections as hasAccountingConnectionsPolicyUtils, isControlPolicy, shouldShowSyncError} from '@libs/PolicyUtils'; +import {getReportFieldTypeTranslationKey} from '@libs/WorkspaceReportFieldUtils'; +import {openPolicyReportFieldsPage} from '@userActions/Policy/ReportField'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type {PolicyReportField} from '@src/types/onyx'; + +type InvoiceFieldListItem = { + fieldID: string; + isDisabled: boolean; + keyForList: string; + pendingAction?: PolicyReportField['pendingAction']; + rightLabel: string; + text: string; +}; + +type WorkspaceInvoiceFieldsSectionProps = { + policyID: string; +}; + +function keyExtractor(item: InvoiceFieldListItem) { + return item.keyForList; +} + +function WorkspaceInvoiceFieldsSection({policyID}: WorkspaceInvoiceFieldsSectionProps) { + const policy = usePolicy(policyID); + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + const {shouldUseNarrowLayout} = useResponsiveLayout(); + const [isReportFieldsWarningModalOpen, setIsReportFieldsWarningModalOpen] = useState(false); + const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false); + const [connectionSyncProgress] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_CONNECTION_SYNC_PROGRESS}${policyID}`, {canBeMissing: true}); + const isSyncInProgress = isConnectionInProgress(connectionSyncProgress, policy); + const hasSyncError = shouldShowSyncError(policy, isSyncInProgress); + const connectedIntegration = getConnectedIntegration(policy) ?? connectionSyncProgress?.connectionName; + const isConnectionVerified = connectedIntegration && !isConnectionUnverified(policy, connectedIntegration); + const currentConnectionName = getCurrentConnectionName(policy); + const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); + const fetchReportFields = useCallback(() => { + openPolicyReportFieldsPage(policyID); + }, [policyID]); + + const {isOffline} = useNetwork({onReconnect: fetchReportFields}); + + useEffect(() => { + fetchReportFields(); + }, [fetchReportFields]); + + const invoiceFields = useMemo(() => { + if (!policy?.fieldList) { + return []; + } + + return Object.values(policy.fieldList) + .filter((field) => field.target === CONST.REPORT_FIELD_TARGETS.INVOICE) + .sort((a, b) => localeCompare(a.name, b.name)) + .map((field) => ({ + text: field.name, + keyForList: String(field.fieldID), + fieldID: field.fieldID, + pendingAction: field.pendingAction, + isDisabled: field.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + rightLabel: Str.recapitalize(translate(getReportFieldTypeTranslationKey(field.type))), + })); + }, [localeCompare, policy?.fieldList, translate]); + + const navigateToReportFieldSettings = useCallback( + (item: InvoiceFieldListItem) => { + Navigation.navigate(ROUTES.WORKSPACE_REPORT_FIELDS_SETTINGS.getRoute(policyID, item.fieldID)); + }, + [policyID], + ); + + const renderItem = useCallback( + ({item}: ListRenderItemInfo) => ( + + navigateToReportFieldSettings(item)} + description={item.text} + disabled={item.isDisabled} + shouldShowRightIcon={!item.isDisabled} + interactive={!item.isDisabled} + rightLabel={item.rightLabel} + descriptionTextStyle={[styles.popoverMenuText, styles.textStrong]} + /> + + ), + [navigateToReportFieldSettings, shouldUseNarrowLayout, styles.ph5, styles.ph8, styles.popoverMenuText, styles.textStrong], + ); + + const getHeaderText = () => + !hasSyncError && isConnectionVerified && currentConnectionName ? ( + + + + ) : ( + + {translate('workspace.invoiceFields.subtitle')} + + ); + + const isLoading = !isOffline && policy === undefined; + + return ( + <> +
+ { + if (!isEnabled) { + setIsReportFieldsWarningModalOpen(true); + return; + } + + if (!isControlPolicy(policy)) { + Navigation.navigate( + ROUTES.WORKSPACE_UPGRADE.getRoute( + policyID, + CONST.UPGRADE_FEATURE_INTRO_MAPPING.reportFields.alias, + ROUTES.WORKSPACE_INVOICES.getRoute(policyID), + ), + ); + return; + } + + enablePolicyReportFields(policyID, isEnabled); + }} + disabled={hasAccountingConnections} + disabledAction={() => setIsOrganizeWarningModalOpen(true)} + subMenuItems={ + !!policy?.areReportFieldsEnabled && ( + <> + + {!isLoading && ( + + )} + + {!hasAccountingConnections && ( + + Navigation.navigate( + ROUTES.WORKSPACE_CREATE_REPORT_FIELD.getRoute(policyID, CONST.REPORT_FIELD_TARGETS.INVOICE), + ) + } + title={translate('workspace.reportFields.addField')} + icon={Plus} + style={[styles.sectionMenuItemTopDescription]} + /> + )} + + ) + } + /> +
+ + { + setIsReportFieldsWarningModalOpen(false); + enablePolicyReportFields(policyID, false); + }} + onCancel={() => setIsReportFieldsWarningModalOpen(false)} + prompt={translate('workspace.invoiceFields.disableInvoiceFieldsConfirmation')} + confirmText={translate('common.disable')} + cancelText={translate('common.cancel')} + danger + /> + + { + setIsOrganizeWarningModalOpen(false); + Navigation.navigate(ROUTES.POLICY_ACCOUNTING.getRoute(policyID)); + }} + onCancel={() => setIsOrganizeWarningModalOpen(false)} + isVisible={isOrganizeWarningModalOpen} + prompt={translate('workspace.moreFeatures.connectionsWarningModal.featureEnabledText')} + confirmText={translate('workspace.moreFeatures.connectionsWarningModal.manageSettings')} + cancelText={translate('common.cancel')} + /> + + + ); +} + +WorkspaceInvoiceFieldsSection.displayName = 'WorkspaceInvoiceFieldsSection'; + +export default WorkspaceInvoiceFieldsSection; diff --git a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx index 10ffb36ae91d7..48a35a682105c 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoicesPage.tsx @@ -11,6 +11,7 @@ import WorkspacePageWithSections from '@pages/workspace/WorkspacePageWithSection import CONST from '@src/CONST'; import type SCREENS from '@src/SCREENS'; import WorkspaceInvoiceBalanceSection from './WorkspaceInvoiceBalanceSection'; +import WorkspaceInvoiceFieldsSection from './WorkspaceInvoiceFieldsSection'; import WorkspaceInvoiceVBASection from './WorkspaceInvoiceVBASection'; import WorkspaceInvoicingDetailsSection from './WorkspaceInvoicingDetailsSection'; @@ -41,6 +42,7 @@ function WorkspaceInvoicesPage({route}: WorkspaceInvoicesPageProps) { {!!policyID && } {!!policyID && } {!!policyID && } + {!!policyID && } )} diff --git a/src/pages/workspace/reports/CreateReportFieldsPage.tsx b/src/pages/workspace/reports/CreateReportFieldsPage.tsx index 334ec1967d120..2a2bd7d85f9a3 100644 --- a/src/pages/workspace/reports/CreateReportFieldsPage.tsx +++ b/src/pages/workspace/reports/CreateReportFieldsPage.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect, useRef} from 'react'; +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -38,7 +38,7 @@ const defaultDate = DateUtils.extractDate(new Date().toString()); function WorkspaceCreateReportFieldsPage({ policy, route: { - params: {policyID}, + params: {policyID, target}, }, }: CreateReportFieldsPageProps) { const styles = useThemeStyles(); @@ -46,17 +46,35 @@ function WorkspaceCreateReportFieldsPage({ const formRef = useRef(null); const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); - const policyExpenseReportIDsSelector = useCallback( + const fieldTarget = useMemo(() => { + if (target && Object.values(CONST.REPORT_FIELD_TARGETS).includes(target)) { + return target; + } + + return CONST.REPORT_FIELD_TARGETS.EXPENSE; + }, [target]); + const reportTypeForTarget = useMemo(() => { + switch (fieldTarget) { + case CONST.REPORT_FIELD_TARGETS.INVOICE: + return CONST.REPORT.TYPE.INVOICE; + case CONST.REPORT_FIELD_TARGETS.PAYCHECK: + return CONST.REPORT.TYPE.PAYCHECK; + default: + return CONST.REPORT.TYPE.EXPENSE; + } + }, [fieldTarget]); + + const policyReportIDsSelector = useCallback( (reports: OnyxCollection) => Object.values(reports ?? {}) - .filter((report) => report?.policyID === policyID && report.type === CONST.REPORT.TYPE.EXPENSE) + .filter((report) => report?.policyID === policyID && report.type === reportTypeForTarget) .map((report) => report?.reportID), - [policyID], + [policyID, reportTypeForTarget], ); - const [policyExpenseReportIDs] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { + const [policyReportIDs] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { canBeMissing: true, - selector: policyExpenseReportIDsSelector, + selector: policyReportIDsSelector, }); const availableListValuesLength = (formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? []).filter((disabledListValue) => !disabledListValue).length; @@ -70,11 +88,12 @@ function WorkspaceCreateReportFieldsPage({ initialValue: !(values[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength === 0) ? values[INPUT_IDS.INITIAL_VALUE] : '', listValues: formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], disabledListValues: formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? [], - policyExpenseReportIDs, + policyReportIDs, + target: fieldTarget, }); Navigation.goBack(); }, - [availableListValuesLength, formDraft, policy, policyExpenseReportIDs], + [availableListValuesLength, formDraft, policy, policyReportIDs, fieldTarget], ); const validateForm = useCallback( diff --git a/src/pages/workspace/reports/WorkspaceReportsPage.tsx b/src/pages/workspace/reports/WorkspaceReportsPage.tsx index a5149646a9298..27348e99e8b7f 100644 --- a/src/pages/workspace/reports/WorkspaceReportsPage.tsx +++ b/src/pages/workspace/reports/WorkspaceReportsPage.tsx @@ -76,7 +76,9 @@ function WorkspaceReportFieldsPage({ return {}; } // eslint-disable-next-line @typescript-eslint/no-unused-vars - return Object.fromEntries(Object.entries(policy.fieldList).filter(([_, value]) => value.fieldID !== 'text_title')); + return Object.fromEntries( + Object.entries(policy.fieldList).filter(([, value]) => value.fieldID !== 'text_title' && value.target !== CONST.REPORT_FIELD_TARGETS.INVOICE), + ); }, [policy]); const [isOrganizeWarningModalOpen, setIsOrganizeWarningModalOpen] = useState(false); From ba1d1c2e2fb59a3dbec4efece29cf057ecb8f9e3 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sun, 7 Dec 2025 23:16:02 +0530 Subject: [PATCH 02/10] Add support for API --- src/CONST/index.ts | 1 + .../MoneyRequestViewReportFields.tsx | 7 +++- .../ReportActionItem/MoneyReportView.tsx | 7 +++- src/libs/API/parameters/index.ts | 1 + src/libs/API/types.ts | 6 ++++ src/libs/actions/Policy/Policy.ts | 35 ++++++++++++++----- src/libs/actions/Policy/ReportField.ts | 18 +++++++++- src/libs/actions/RequestConflictUtils.ts | 1 + src/pages/EditReportFieldPage.tsx | 6 +++- .../WorkspaceInvoiceFieldsSection.tsx | 18 +++++----- src/types/onyx/Policy.ts | 3 ++ 11 files changed, 83 insertions(+), 20 deletions(-) diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 92bb834684947..95345d344197e 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3096,6 +3096,7 @@ const CONST = { ARE_DISTANCE_RATES_ENABLED: 'areDistanceRatesEnabled', ARE_WORKFLOWS_ENABLED: 'areWorkflowsEnabled', ARE_REPORT_FIELDS_ENABLED: 'areReportFieldsEnabled', + ARE_INVOICE_FIELDS_ENABLED: 'areInvoiceFieldsEnabled', ARE_CONNECTIONS_ENABLED: 'areConnectionsEnabled', ARE_RECEIPT_PARTNERS_ENABLED: 'receiptPartners', ARE_COMPANY_CARDS_ENABLED: 'areCompanyCardsEnabled', diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 8cee588e0fdb9..1538b00109dc1 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -120,7 +120,12 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); - const shouldDisplayReportFields = (isPaidGroupPolicyExpenseReport || isInvoiceReport) && policy?.areReportFieldsEnabled && (!isOnlyTitleFieldEnabled || !isCombinedReport); + const isReportFieldsFeatureEnabled = + report?.type === CONST.REPORT.TYPE.INVOICE + ? policy?.areInvoiceFieldsEnabled ?? policy?.areReportFieldsEnabled + : policy?.areReportFieldsEnabled; + const shouldDisplayReportFields = + (isPaidGroupPolicyExpenseReport || isInvoiceReport) && isReportFieldsFeatureEnabled && (!isOnlyTitleFieldEnabled || !isCombinedReport); return ( shouldDisplayReportFields && diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 5d851e192c6b8..7745177333748 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -108,9 +108,14 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo return isReportFieldOfTypeTitle(reportField) || (!fieldValue && !hasEnableOption); }; + const isReportFieldsFeatureEnabled = + report?.type === CONST.REPORT.TYPE.INVOICE + ? policy?.areInvoiceFieldsEnabled ?? policy?.areReportFieldsEnabled + : policy?.areReportFieldsEnabled; const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isPaidGroupPolicyExpenseReport || isInvoiceReport) && + isReportFieldsFeatureEnabled && (!isCombinedReport || !isOnlyTitleFieldEnabled) && !sortedPolicyReportFields.every(shouldHideSingleReportField); @@ -137,7 +142,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo {!isClosedExpenseReportWithNoExpenses && ( <> {(isPaidGroupPolicyExpenseReport || isInvoiceReport) && - policy?.areReportFieldsEnabled && + isReportFieldsFeatureEnabled && (!isCombinedReport || !isOnlyTitleFieldEnabled) && sortedPolicyReportFields.map((reportField) => { if (shouldHideSingleReportField(reportField)) { diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 9ee2eff4f71ae..aae41c8c9c128 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -222,6 +222,7 @@ export type {default as EnablePolicyDistanceRatesParams} from './EnablePolicyDis export type {default as EnablePolicyTagsParams} from './EnablePolicyTagsParams'; export type {default as SetPolicyTagsEnabled} from './SetPolicyTagsEnabled'; export type {default as EnablePolicyWorkflowsParams} from './EnablePolicyWorkflowsParams'; +export type {default as EnablePolicyInvoiceFieldsParams} from './EnablePolicyInvoiceFieldsParams'; export type {default as EnablePolicyReportFieldsParams} from './EnablePolicyReportFieldsParams'; export type {default as EnablePolicyExpensifyCardsParams} from './EnablePolicyExpensifyCardsParams'; export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 6c2a626e111b0..47134f8a55460 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -246,6 +246,7 @@ const WRITE_COMMANDS = { ENABLE_POLICY_TAXES: 'EnablePolicyTaxes', ENABLE_POLICY_WORKFLOWS: 'EnablePolicyWorkflows', ENABLE_POLICY_REPORT_FIELDS: 'EnablePolicyReportFields', + ENABLE_POLICY_INVOICE_FIELDS: 'EnablePolicyInvoiceFields', ENABLE_POLICY_EXPENSIFY_CARDS: 'EnablePolicyExpensifyCards', TOGGLE_POLICY_PER_DIEM: 'TogglePolicyPerDiem', ENABLE_POLICY_COMPANY_CARDS: 'EnablePolicyCompanyCards', @@ -361,6 +362,7 @@ const WRITE_COMMANDS = { OPEN_SIDE_PANEL: 'OpenSidePanel', CLOSE_SIDE_PANEL: 'CloseSidePanel', UPDATE_NETSUITE_SUBSIDIARY: 'UpdateNetSuiteSubsidiary', + CREATE_WORKSPACE_INVOICE_FIELD: 'CreatePolicyInvoiceField', CREATE_WORKSPACE_REPORT_FIELD: 'CreatePolicyReportField', UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE: 'SetPolicyReportFieldDefault', ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'EnablePolicyReportFieldOption', @@ -772,6 +774,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.ENABLE_POLICY_TAXES]: Parameters.EnablePolicyTaxesParams; [WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS]: Parameters.EnablePolicyWorkflowsParams; [WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS]: Parameters.EnablePolicyReportFieldsParams; + [WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS]: Parameters.EnablePolicyInvoiceFieldsParams; [WRITE_COMMANDS.ENABLE_POLICY_EXPENSIFY_CARDS]: Parameters.EnablePolicyExpensifyCardsParams; [WRITE_COMMANDS.TOGGLE_POLICY_PER_DIEM]: Parameters.TogglePolicyPerDiemParams; [WRITE_COMMANDS.ENABLE_POLICY_COMPANY_CARDS]: Parameters.EnablePolicyCompanyCardsParams; @@ -937,6 +940,7 @@ type WriteCommandParameters = { // Workspace report field parameters [WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD]: Parameters.CreateWorkspaceReportFieldParams; + [WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD]: Parameters.CreateWorkspaceReportFieldParams; [WRITE_COMMANDS.UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE]: Parameters.UpdateWorkspaceReportFieldInitialValueParams; [WRITE_COMMANDS.ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.EnableWorkspaceReportFieldListValueParams; [WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.CreateWorkspaceReportFieldListValueParams; @@ -1116,6 +1120,7 @@ const READ_COMMANDS = { OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage', OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage', + OPEN_POLICY_INVOICE_FIELDS_PAGE: 'OpenPolicyInvoiceFieldsPage', OPEN_POLICY_RULES_PAGE: 'OpenPolicyRulesPage', OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage', OPEN_POLICY_COMPANY_CARDS_FEED: 'OpenPolicyCompanyCardsFeed', @@ -1198,6 +1203,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams; [READ_COMMANDS.OPEN_POLICY_TAXES_PAGE]: Parameters.OpenPolicyTaxesPageParams; [READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; + [READ_COMMANDS.OPEN_POLICY_INVOICE_FIELDS_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; [READ_COMMANDS.OPEN_POLICY_RULES_PAGE]: Parameters.OpenPolicyRulesPageParams; [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index 598dd5f0f3d86..fe332172a50f6 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -23,6 +23,7 @@ import type { EnablePolicyCompanyCardsParams, EnablePolicyConnectionsParams, EnablePolicyExpensifyCardsParams, + EnablePolicyInvoiceFieldsParams, EnablePolicyInvoicingParams, EnablePolicyReportFieldsParams, EnablePolicyTaxesParams, @@ -4159,16 +4160,25 @@ function enableCompanyCards(policyID: string, enabled: boolean, shouldGoBack = t } } -function enablePolicyReportFields(policyID: string, enabled: boolean) { +function enablePolicyFields( + policyID: string, + enabled: boolean, + command: typeof WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS | typeof WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS, +) { + const enableFieldKey = + command === WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS + ? CONST.POLICY.MORE_FEATURES.ARE_INVOICE_FIELDS_ENABLED + : CONST.POLICY.MORE_FEATURES.ARE_REPORT_FIELDS_ENABLED; + const onyxData: OnyxData = { optimisticData: [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - areReportFieldsEnabled: enabled, + [enableFieldKey]: enabled, pendingFields: { - areReportFieldsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + [enableFieldKey]: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, }, }, }, @@ -4179,7 +4189,7 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { pendingFields: { - areReportFieldsEnabled: null, + [enableFieldKey]: null, }, }, }, @@ -4189,18 +4199,26 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - areReportFieldsEnabled: !enabled, + [enableFieldKey]: !enabled, pendingFields: { - areReportFieldsEnabled: null, + [enableFieldKey]: null, }, }, }, ], }; - const parameters: EnablePolicyReportFieldsParams = {policyID, enabled}; + const parameters: EnablePolicyReportFieldsParams | EnablePolicyInvoiceFieldsParams = {policyID, enabled}; + + API.writeWithNoDuplicatesEnableFeatureConflicts(command, parameters, onyxData); +} + +function enablePolicyReportFields(policyID: string, enabled: boolean) { + enablePolicyFields(policyID, enabled, WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS); +} - API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, parameters, onyxData); +function enablePolicyInvoiceFields(policyID: string, enabled: boolean) { + enablePolicyFields(policyID, enabled, WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS); } function enablePolicyTaxes(policyID: string, enabled: boolean) { @@ -6526,6 +6544,7 @@ export { enablePolicyConnections, enablePolicyReceiptPartners, enablePolicyReportFields, + enablePolicyInvoiceFields, enablePolicyTaxes, enablePolicyWorkflows, changePolicyUberBillingAccount, diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index 94cfd266a9961..cb4f8c64f596f 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -99,6 +99,19 @@ function openPolicyReportFieldsPage(policyID: string) { API.read(READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE, params); } +function openPolicyInvoiceFieldsPage(policyID: string) { + if (!policyID) { + Log.warn('openPolicyInvoiceFieldsPage invalid params', {policyID}); + return; + } + + const params: OpenPolicyReportFieldsPageParams = { + policyID, + }; + + API.read(READ_COMMANDS.OPEN_POLICY_INVOICE_FIELDS_PAGE, params); +} + /** * Sets the initial form values for the workspace report fields form. */ @@ -263,7 +276,9 @@ function createReportField({name, type, initialValue, listValues, disabledListVa reportFields: JSON.stringify([optimisticReportFieldDataForPolicy]), }; - API.write(WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD, parameters, onyxData); + const createCommand = + target === CONST.REPORT_FIELD_TARGETS.INVOICE ? WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD : WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD; + API.write(createCommand, parameters, onyxData); } function deleteReportFields({policy, reportFieldsToUpdate}: DeleteReportFieldsParams) { @@ -550,6 +565,7 @@ export { updateReportFieldInitialValue, updateReportFieldListValueEnabled, openPolicyReportFieldsPage, + openPolicyInvoiceFieldsPage, addReportFieldListValue, removeReportFieldListValue, }; diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 7cf4c2021d55a..38bcb4c037150 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -31,6 +31,7 @@ const enablePolicyFeatureCommand = [ WRITE_COMMANDS.ENABLE_POLICY_TAGS, WRITE_COMMANDS.ENABLE_POLICY_TAXES, WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, + WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS, WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, WRITE_COMMANDS.ENABLE_POLICY_INVOICING, diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 3f674634c1502..6807cba59b451 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -53,7 +53,11 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const {translate} = useLocalize(); const isReportFieldTitle = isReportFieldOfTypeTitle(reportField); - const reportFieldsEnabled = ((isPaidGroupPolicyExpenseReport(report) || isInvoiceReport(report)) && !!policy?.areReportFieldsEnabled) || isReportFieldTitle; + const isReportFieldsFeatureEnabled = + report?.type === CONST.REPORT.TYPE.INVOICE + ? policy?.areInvoiceFieldsEnabled ?? policy?.areReportFieldsEnabled + : policy?.areReportFieldsEnabled; + const reportFieldsEnabled = ((isPaidGroupPolicyExpenseReport(report) || isInvoiceReport(report)) && !!isReportFieldsFeatureEnabled) || isReportFieldTitle; const hasOtherViolations = report?.fieldList && Object.entries(report.fieldList).some(([key, field]) => key !== fieldKey && field.value === '' && !isReportFieldDisabled(report, reportField, policy)); diff --git a/src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx b/src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx index d3e5047d86685..191489af1a711 100644 --- a/src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx +++ b/src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx @@ -18,11 +18,11 @@ import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {isConnectionInProgress, isConnectionUnverified} from '@libs/actions/connections'; -import {enablePolicyReportFields} from '@libs/actions/Policy/Policy'; +import {enablePolicyInvoiceFields} from '@libs/actions/Policy/Policy'; import Navigation from '@libs/Navigation/Navigation'; import {getConnectedIntegration, getCurrentConnectionName, hasAccountingConnections as hasAccountingConnectionsPolicyUtils, isControlPolicy, shouldShowSyncError} from '@libs/PolicyUtils'; import {getReportFieldTypeTranslationKey} from '@libs/WorkspaceReportFieldUtils'; -import {openPolicyReportFieldsPage} from '@userActions/Policy/ReportField'; +import {openPolicyInvoiceFieldsPage} from '@userActions/Policy/ReportField'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -60,8 +60,10 @@ function WorkspaceInvoiceFieldsSection({policyID}: WorkspaceInvoiceFieldsSection const currentConnectionName = getCurrentConnectionName(policy); const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); const fetchReportFields = useCallback(() => { - openPolicyReportFieldsPage(policyID); + openPolicyInvoiceFieldsPage(policyID); }, [policyID]); + const invoiceFieldsEnabled = policy?.areInvoiceFieldsEnabled ?? policy?.areReportFieldsEnabled; + const invoiceFieldsPendingAction = policy?.pendingFields?.areInvoiceFieldsEnabled ?? policy?.pendingFields?.areReportFieldsEnabled; const {isOffline} = useNetwork({onReconnect: fetchReportFields}); @@ -137,12 +139,12 @@ function WorkspaceInvoiceFieldsSection({policyID}: WorkspaceInvoiceFieldsSection containerStyles={shouldUseNarrowLayout ? styles.p5 : styles.p8} > { if (!isEnabled) { setIsReportFieldsWarningModalOpen(true); @@ -160,12 +162,12 @@ function WorkspaceInvoiceFieldsSection({policyID}: WorkspaceInvoiceFieldsSection return; } - enablePolicyReportFields(policyID, isEnabled); + enablePolicyInvoiceFields(policyID, isEnabled); }} disabled={hasAccountingConnections} disabledAction={() => setIsOrganizeWarningModalOpen(true)} subMenuItems={ - !!policy?.areReportFieldsEnabled && ( + !!invoiceFieldsEnabled && ( <> {!isLoading && ( @@ -200,7 +202,7 @@ function WorkspaceInvoiceFieldsSection({policyID}: WorkspaceInvoiceFieldsSection isVisible={isReportFieldsWarningModalOpen} onConfirm={() => { setIsReportFieldsWarningModalOpen(false); - enablePolicyReportFields(policyID, false); + enablePolicyInvoiceFields(policyID, false); }} onCancel={() => setIsReportFieldsWarningModalOpen(false)} prompt={translate('workspace.invoiceFields.disableInvoiceFieldsConfirmation')} diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 8abd836c2147d..158595efc6467 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -1921,6 +1921,9 @@ type Policy = OnyxCommon.OnyxValueWithOfflineFeedback< /** Whether the Report Fields feature is enabled */ areReportFieldsEnabled?: boolean; + /** Whether the Invoice Fields feature is enabled */ + areInvoiceFieldsEnabled?: boolean; + /** Whether the Connections feature is enabled */ areConnectionsEnabled?: boolean; From e3f4f7cabab4701fab196455ef3fe4a7f8531340 Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Sun, 25 Jan 2026 22:24:22 +0530 Subject: [PATCH 03/10] Implement invoice specific fields UI and logic --- src/ROUTES.ts | 37 +- src/SCREENS.ts | 7 + .../ReportActionItem/MoneyReportView.tsx | 1 - ...ateWorkspaceInvoiceFieldListValueParams.ts | 10 + .../CreateWorkspaceInvoiceFieldParams.ts | 10 + .../parameters/DeletePolicyInvoiceField.ts | 10 + ...bleWorkspaceInvoiceFieldListValueParams.ts | 10 + ...oveWorkspaceInvoiceFieldListValueParams.ts | 10 + ...WorkspaceInvoiceFieldInitialValueParams.ts | 10 + src/libs/API/parameters/index.ts | 6 + src/libs/API/types.ts | 16 +- .../ModalStackNavigators/index.tsx | 7 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 13 +- src/libs/Navigation/linkingConfig/config.ts | 24 +- src/libs/Navigation/types.ts | 29 +- src/libs/actions/Policy/ReportField.ts | 134 ++++-- .../iou/request/step/IOURequestStepReport.tsx | 24 +- .../workspace/fields/CreateFieldsPage.tsx | 262 +++++++++++ .../fields/FieldsAddListValuePage.tsx | 115 +++++ .../workspace/fields/FieldsEditValuePage.tsx | 105 +++++ .../fields/FieldsInitialValuePage.tsx | 157 +++++++ .../workspace/fields/FieldsListValuesPage.tsx | 439 ++++++++++++++++++ .../workspace/fields/FieldsSettingsPage.tsx | 136 ++++++ .../fields/FieldsValueSettingsPage.tsx | 167 +++++++ .../invoices/CreateInvoiceFieldsPage.tsx | 31 ++ .../InvoiceFieldsAddListValuePage.tsx | 30 ++ .../invoices/InvoiceFieldsEditValuePage.tsx | 30 ++ .../InvoiceFieldsInitialValuePage.tsx | 30 ++ .../invoices/InvoiceFieldsListValuesPage.tsx | 40 ++ .../invoices/InvoiceFieldsSettingsPage.tsx | 33 ++ .../InvoiceFieldsValueSettingsPage.tsx | 36 ++ .../WorkspaceInvoiceFieldsSection.tsx | 16 +- .../reports/CreateReportFieldsPage.tsx | 283 +---------- .../reports/ReportFieldsAddListValuePage.tsx | 104 +---- .../reports/ReportFieldsEditValuePage.tsx | 94 +--- .../reports/ReportFieldsInitialValuePage.tsx | 146 +----- .../reports/ReportFieldsListValuesPage.tsx | 423 +---------------- .../reports/ReportFieldsSettingsPage.tsx | 122 +---- .../reports/ReportFieldsValueSettingsPage.tsx | 147 +----- src/pages/workspace/withPolicy.tsx | 10 + 40 files changed, 2005 insertions(+), 1309 deletions(-) create mode 100644 src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts create mode 100644 src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts create mode 100644 src/libs/API/parameters/DeletePolicyInvoiceField.ts create mode 100644 src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts create mode 100644 src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts create mode 100644 src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts create mode 100644 src/pages/workspace/fields/CreateFieldsPage.tsx create mode 100644 src/pages/workspace/fields/FieldsAddListValuePage.tsx create mode 100644 src/pages/workspace/fields/FieldsEditValuePage.tsx create mode 100644 src/pages/workspace/fields/FieldsInitialValuePage.tsx create mode 100644 src/pages/workspace/fields/FieldsListValuesPage.tsx create mode 100644 src/pages/workspace/fields/FieldsSettingsPage.tsx create mode 100644 src/pages/workspace/fields/FieldsValueSettingsPage.tsx create mode 100644 src/pages/workspace/invoices/CreateInvoiceFieldsPage.tsx create mode 100644 src/pages/workspace/invoices/InvoiceFieldsAddListValuePage.tsx create mode 100644 src/pages/workspace/invoices/InvoiceFieldsEditValuePage.tsx create mode 100644 src/pages/workspace/invoices/InvoiceFieldsInitialValuePage.tsx create mode 100644 src/pages/workspace/invoices/InvoiceFieldsListValuesPage.tsx create mode 100644 src/pages/workspace/invoices/InvoiceFieldsSettingsPage.tsx create mode 100644 src/pages/workspace/invoices/InvoiceFieldsValueSettingsPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a388df6d5e707..41b09127c3593 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1888,6 +1888,35 @@ const ROUTES = { route: 'workspaces/:policyID/invoices/company-website', getRoute: (policyID: string) => `workspaces/${policyID}/invoices/company-website` as const, }, + WORKSPACE_INVOICE_FIELDS_CREATE: { + route: 'workspaces/:policyID/invoices/newInvoiceField', + getRoute: (policyID: string) => `workspaces/${policyID}/invoices/newInvoiceField` as const, + }, + WORKSPACE_INVOICE_FIELDS_SETTINGS: { + route: 'workspaces/:policyID/invoices/:reportFieldID/edit', + getRoute: (policyID: string, reportFieldID: string) => `workspaces/${policyID}/invoices/${encodeURIComponent(reportFieldID)}/edit` as const, + }, + WORKSPACE_INVOICE_FIELDS_LIST_VALUES: { + route: 'workspaces/:policyID/invoices/listValues/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `workspaces/${policyID}/invoices/listValues/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, + }, + WORKSPACE_INVOICE_FIELDS_ADD_VALUE: { + route: 'workspaces/:policyID/invoices/addValue/:reportFieldID?', + getRoute: (policyID: string, reportFieldID?: string) => `workspaces/${policyID}/invoices/addValue/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, + }, + WORKSPACE_INVOICE_FIELDS_VALUE_SETTINGS: { + route: 'workspaces/:policyID/invoices/:valueIndex/:reportFieldID?', + getRoute: (policyID: string, valueIndex: number, reportFieldID?: string) => + `workspaces/${policyID}/invoices/${valueIndex}/${reportFieldID ? encodeURIComponent(reportFieldID) : ''}` as const, + }, + WORKSPACE_INVOICE_FIELDS_EDIT_VALUE: { + route: 'workspaces/:policyID/invoices/newInvoiceField/:valueIndex/edit', + getRoute: (policyID: string, valueIndex: number) => `workspaces/${policyID}/invoices/newInvoiceField/${valueIndex}/edit` as const, + }, + WORKSPACE_INVOICE_FIELDS_EDIT_INITIAL_VALUE: { + route: 'workspaces/:policyID/invoices/:reportFieldID/edit/initialValue', + getRoute: (policyID: string, reportFieldID: string) => `workspaces/${policyID}/invoices/${encodeURIComponent(reportFieldID)}/edit/initialValue` as const, + }, WORKSPACE_MEMBERS: { route: 'workspaces/:policyID/members', getRoute: (policyID: string | undefined) => { @@ -2239,14 +2268,10 @@ const ROUTES = { }, WORKSPACE_CREATE_REPORT_FIELD: { route: 'workspaces/:policyID/reports/newReportField', - getRoute: (policyID: string, target?: ValueOf) => { + getRoute: (policyID: string) => { const baseRoute = `workspaces/${policyID}/reports/newReportField` as const; - if (!target) { - return baseRoute; - } - - return `${baseRoute}?target=${encodeURIComponent(target)}` as const; + return baseRoute; }, }, WORKSPACE_REPORT_FIELDS_SETTINGS: { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 617ab3c4d9a64..d60869cde786e 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -646,6 +646,13 @@ const SCREENS = { INVOICES_VERIFY_ACCOUNT: 'Workspace_Invoices_Verify_Account', INVOICES_COMPANY_NAME: 'Workspace_Invoices_Company_Name', INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website', + INVOICE_FIELDS_CREATE: 'Workspace_InvoiceFields_Create', + INVOICE_FIELDS_SETTINGS: 'Workspace_InvoiceFields_Settings', + INVOICE_FIELDS_LIST_VALUES: 'Workspace_InvoiceFields_ListValues', + INVOICE_FIELDS_ADD_VALUE: 'Workspace_InvoiceFields_AddValue', + INVOICE_FIELDS_VALUE_SETTINGS: 'Workspace_InvoiceFields_ValueSettings', + INVOICE_FIELDS_EDIT_VALUE: 'Workspace_InvoiceFields_EditValue', + INVOICE_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_InvoiceFields_EditInitialValue', MEMBERS: 'Workspace_Members', MEMBERS_IMPORT: 'Members_Import', MEMBERS_IMPORTED: 'Members_Imported', diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4db75ed2df9aa..4eb72a625709a 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -135,7 +135,6 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo {!isClosedExpenseReportWithNoExpenses && ( <> {(isPaidGroupPolicyExpenseReport || isInvoiceReport) && - isReportFieldsFeatureEnabled && (!isCombinedReport || !isOnlyTitleFieldEnabled) && sortedPolicyReportFields.map((reportField) => { if (shouldHideSingleReportField(reportField)) { diff --git a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts new file mode 100644 index 0000000000000..fa22805f4e71c --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts @@ -0,0 +1,10 @@ +type CreateWorkspaceInvoiceFieldListValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default CreateWorkspaceInvoiceFieldListValueParams; diff --git a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts new file mode 100644 index 0000000000000..5cce12e49273e --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts @@ -0,0 +1,10 @@ +type CreateWorkspaceInvoiceFieldParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default CreateWorkspaceInvoiceFieldParams; diff --git a/src/libs/API/parameters/DeletePolicyInvoiceField.ts b/src/libs/API/parameters/DeletePolicyInvoiceField.ts new file mode 100644 index 0000000000000..5b3c37fb1e07c --- /dev/null +++ b/src/libs/API/parameters/DeletePolicyInvoiceField.ts @@ -0,0 +1,10 @@ +type DeletePolicyInvoiceField = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default DeletePolicyInvoiceField; diff --git a/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts new file mode 100644 index 0000000000000..a52405efad545 --- /dev/null +++ b/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts @@ -0,0 +1,10 @@ +type EnableWorkspaceInvoiceFieldListValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default EnableWorkspaceInvoiceFieldListValueParams; diff --git a/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts new file mode 100644 index 0000000000000..c793b4bdf10a2 --- /dev/null +++ b/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts @@ -0,0 +1,10 @@ +type RemoveWorkspaceInvoiceFieldListValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default RemoveWorkspaceInvoiceFieldListValueParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts b/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts new file mode 100644 index 0000000000000..eeb3cd5106aa7 --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts @@ -0,0 +1,10 @@ +type UpdateWorkspaceInvoiceFieldInitialValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default UpdateWorkspaceInvoiceFieldInitialValueParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index fea08310b9fe2..800ce06813da1 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -300,14 +300,20 @@ export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyR export type {default as PayMoneyRequestOnSearchParams} from './PayMoneyRequestOnSearchParams'; export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; +export type {default as DeletePolicyInvoiceField} from './DeletePolicyInvoiceField'; export type {default as DeletePolicyReportField} from './DeletePolicyReportField'; export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams'; export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams'; +export type {default as CreateWorkspaceInvoiceFieldParams} from './CreateWorkspaceInvoiceFieldParams'; export type {default as UpdateWorkspaceReportFieldInitialValueParams} from './UpdateWorkspaceReportFieldInitialValueParams'; +export type {default as UpdateWorkspaceInvoiceFieldInitialValueParams} from './UpdateWorkspaceInvoiceFieldInitialValueParams'; export type {default as EnableWorkspaceReportFieldListValueParams} from './EnableWorkspaceReportFieldListValueParams'; +export type {default as EnableWorkspaceInvoiceFieldListValueParams} from './EnableWorkspaceInvoiceFieldListValueParams'; export type {default as EnablePolicyInvoicingParams} from './EnablePolicyInvoicingParams'; export type {default as CreateWorkspaceReportFieldListValueParams} from './CreateWorkspaceReportFieldListValueParams'; +export type {default as CreateWorkspaceInvoiceFieldListValueParams} from './CreateWorkspaceInvoiceFieldListValueParams'; export type {default as RemoveWorkspaceReportFieldListValueParams} from './RemoveWorkspaceReportFieldListValueParams'; +export type {default as RemoveWorkspaceInvoiceFieldListValueParams} from './RemoveWorkspaceInvoiceFieldListValueParams'; export type {default as OpenPolicyExpensifyCardsPageParams} from './OpenPolicyExpensifyCardsPageParams'; export type {default as OpenPolicyTravelPageParams} from './OpenPolicyTravelPageParams'; export type {default as OpenPolicyEditCardLimitTypePageParams} from './OpenPolicyEditCardLimitTypePageParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 8a82ef15db79e..7727e651cbb57 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -178,6 +178,7 @@ const WRITE_COMMANDS = { UPDATE_POLICY_CATEGORY_GL_CODE: 'UpdatePolicyCategoryGLCode', DELETE_WORKSPACE_CATEGORIES: 'DeleteWorkspaceCategories', DELETE_POLICY_REPORT_FIELD: 'DeletePolicyReportField', + DELETE_POLICY_INVOICE_FIELD: 'DeletePolicyInvoiceField', SET_POLICY_TAGS_REQUIRED: 'SetPolicyTagsRequired', SET_POLICY_TAG_LISTS_REQUIRED: 'SetPolicyTagListsRequired', SET_POLICY_REQUIRES_TAG: 'SetPolicyRequiresTag', @@ -367,9 +368,13 @@ const WRITE_COMMANDS = { CLOSE_SIDE_PANEL: 'CloseSidePanel', UPDATE_NETSUITE_SUBSIDIARY: 'UpdateNetSuiteSubsidiary', CREATE_WORKSPACE_INVOICE_FIELD: 'CreatePolicyInvoiceField', + CREATE_WORKSPACE_INVOICE_FIELD_LIST_VALUE: 'CreatePolicyInvoiceFieldOption', CREATE_WORKSPACE_REPORT_FIELD: 'CreatePolicyReportField', + UPDATE_WORKSPACE_INVOICE_FIELD_INITIAL_VALUE: 'SetPolicyInvoiceFieldDefault', UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE: 'SetPolicyReportFieldDefault', + ENABLE_WORKSPACE_INVOICE_FIELD_LIST_VALUE: 'EnablePolicyInvoiceFieldOption', ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'EnablePolicyReportFieldOption', + REMOVE_WORKSPACE_INVOICE_FIELD_LIST_VALUE: 'RemovePolicyInvoiceFieldOption', CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'CreatePolicyReportFieldOption', REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE: 'RemovePolicyReportFieldOption', UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION: 'UpdateNetSuiteSyncTaxConfiguration', @@ -702,6 +707,7 @@ type WriteCommandParameters = { [WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_PAYROLL_CODE]: Parameters.UpdatePolicyCategoryPayrollCodeParams; [WRITE_COMMANDS.UPDATE_POLICY_CATEGORY_GL_CODE]: Parameters.UpdatePolicyCategoryGLCodeParams; [WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD]: Parameters.DeletePolicyReportField; + [WRITE_COMMANDS.DELETE_POLICY_INVOICE_FIELD]: Parameters.DeletePolicyInvoiceField; [WRITE_COMMANDS.SET_POLICY_REQUIRES_TAG]: Parameters.SetPolicyRequiresTag; [WRITE_COMMANDS.SET_POLICY_TAGS_REQUIRED]: Parameters.SetPolicyTagsRequired; [WRITE_COMMANDS.SET_POLICY_TAG_LISTS_REQUIRED]: Parameters.SetPolicyTagListsRequired; @@ -959,11 +965,15 @@ type WriteCommandParameters = { // Workspace report field parameters [WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD]: Parameters.CreateWorkspaceReportFieldParams; - [WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD]: Parameters.CreateWorkspaceReportFieldParams; + [WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD]: Parameters.CreateWorkspaceInvoiceFieldParams; [WRITE_COMMANDS.UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE]: Parameters.UpdateWorkspaceReportFieldInitialValueParams; + [WRITE_COMMANDS.UPDATE_WORKSPACE_INVOICE_FIELD_INITIAL_VALUE]: Parameters.UpdateWorkspaceInvoiceFieldInitialValueParams; [WRITE_COMMANDS.ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.EnableWorkspaceReportFieldListValueParams; + [WRITE_COMMANDS.ENABLE_WORKSPACE_INVOICE_FIELD_LIST_VALUE]: Parameters.EnableWorkspaceInvoiceFieldListValueParams; [WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.CreateWorkspaceReportFieldListValueParams; + [WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD_LIST_VALUE]: Parameters.CreateWorkspaceInvoiceFieldListValueParams; [WRITE_COMMANDS.REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE]: Parameters.RemoveWorkspaceReportFieldListValueParams; + [WRITE_COMMANDS.REMOVE_WORKSPACE_INVOICE_FIELD_LIST_VALUE]: Parameters.RemoveWorkspaceInvoiceFieldListValueParams; [WRITE_COMMANDS.UPDATE_NETSUITE_SYNC_TAX_CONFIGURATION]: Parameters.UpdateNetSuiteGenericTypeParams<'enabled', boolean>; [WRITE_COMMANDS.UPDATE_SAGE_INTACCT_TAX_SOLUTION_ID]: Parameters.UpdateNetSuiteGenericTypeParams<'taxSolutionID', string>; @@ -1147,7 +1157,7 @@ const READ_COMMANDS = { OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage', OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage', - OPEN_POLICY_INVOICE_FIELDS_PAGE: 'OpenPolicyInvoiceFieldsPage', + OPEN_POLICY_INVOICES_PAGE: 'OpenPolicyInvoicesPage', OPEN_POLICY_RULES_PAGE: 'OpenPolicyRulesPage', OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage', OPEN_POLICY_TRAVEL_PAGE: 'OpenPolicyTravelPage', @@ -1233,7 +1243,7 @@ type ReadCommandParameters = { [READ_COMMANDS.OPEN_POLICY_TAGS_PAGE]: Parameters.OpenPolicyTagsPageParams; [READ_COMMANDS.OPEN_POLICY_TAXES_PAGE]: Parameters.OpenPolicyTaxesPageParams; [READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; - [READ_COMMANDS.OPEN_POLICY_INVOICE_FIELDS_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; + [READ_COMMANDS.OPEN_POLICY_INVOICES_PAGE]: Parameters.OpenPolicyReportFieldsPageParams; [READ_COMMANDS.OPEN_POLICY_RULES_PAGE]: Parameters.OpenPolicyRulesPageParams; [READ_COMMANDS.OPEN_WORKSPACE_INVITE_PAGE]: Parameters.OpenWorkspaceInvitePageParams; [READ_COMMANDS.OPEN_DRAFT_WORKSPACE_REQUEST]: Parameters.OpenDraftWorkspaceRequestParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index f8a60900b6aff..fe347ee4b4f03 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -778,6 +778,13 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/reports/ReportFieldsValueSettingsPage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reports/ReportFieldsInitialValuePage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reports/ReportFieldsEditValuePage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_CREATE]: () => require('../../../../pages/workspace/invoices/CreateInvoiceFieldsPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_SETTINGS]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsSettingsPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsListValuesPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsAddListValuePage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsValueSettingsPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsInitialValuePage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsEditValuePage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctImportPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 0beb9e8ba14f1..37936e4a26170 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -235,7 +235,18 @@ const WORKSPACE_TO_RHP: Partial['config'] = { path: ROUTES.WORKSPACE_INVOICES_VERIFY_ACCOUNT.route, exact: true, }, + [SCREENS.WORKSPACE.INVOICE_FIELDS_CREATE]: { + path: ROUTES.WORKSPACE_INVOICE_FIELDS_CREATE.route, + }, + [SCREENS.WORKSPACE.INVOICE_FIELDS_LIST_VALUES]: { + path: ROUTES.WORKSPACE_INVOICE_FIELDS_LIST_VALUES.route, + }, + [SCREENS.WORKSPACE.INVOICE_FIELDS_ADD_VALUE]: { + path: ROUTES.WORKSPACE_INVOICE_FIELDS_ADD_VALUE.route, + }, + [SCREENS.WORKSPACE.INVOICE_FIELDS_VALUE_SETTINGS]: { + path: ROUTES.WORKSPACE_INVOICE_FIELDS_VALUE_SETTINGS.route, + }, + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_VALUE]: { + path: ROUTES.WORKSPACE_INVOICE_FIELDS_EDIT_VALUE.route, + }, + [SCREENS.WORKSPACE.INVOICE_FIELDS_SETTINGS]: { + path: ROUTES.WORKSPACE_INVOICE_FIELDS_SETTINGS.route, + }, + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_INITIAL_VALUE]: { + path: ROUTES.WORKSPACE_INVOICE_FIELDS_EDIT_INITIAL_VALUE.route, + }, [SCREENS.WORKSPACE.COMPANY_CARDS_SELECT_FEED]: { path: ROUTES.WORKSPACE_COMPANY_CARDS_SELECT_FEED.route, }, @@ -1049,9 +1070,6 @@ const config: LinkingOptions['config'] = { }, [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { path: ROUTES.WORKSPACE_CREATE_REPORT_FIELD.route, - parse: { - target: (value: string) => value, - }, }, [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { path: ROUTES.WORKSPACE_REPORT_FIELDS_LIST_VALUES.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 370e057941f01..b45941fcbf09d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -610,33 +610,60 @@ type SettingsNavigatorParamList = { }; [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { policyID: string; - target?: ValueOf; + }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_CREATE]: { + policyID: string; }; [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { policyID: string; reportFieldID?: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_LIST_VALUES]: { + policyID: string; + reportFieldID?: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: { policyID: string; reportFieldID?: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_ADD_VALUE]: { + policyID: string; + reportFieldID?: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: { policyID: string; valueIndex: number; reportFieldID?: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_VALUE_SETTINGS]: { + policyID: string; + valueIndex: number; + reportFieldID?: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: { policyID: string; valueIndex: number; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_VALUE]: { + policyID: string; + valueIndex: number; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: { policyID: string; reportFieldID: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_SETTINGS]: { + policyID: string; + reportFieldID: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: { policyID: string; reportFieldID: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_INITIAL_VALUE]: { + policyID: string; + reportFieldID: string; + }; [SCREENS.WORKSPACE.MEMBER_DETAILS]: { policyID: string; accountID: string; diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index 8e580dca9877e..9dff0641691c6 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -5,11 +5,17 @@ import * as API from '@libs/API'; import type { CreateWorkspaceReportFieldListValueParams, CreateWorkspaceReportFieldParams, + CreateWorkspaceInvoiceFieldListValueParams, + CreateWorkspaceInvoiceFieldParams, DeletePolicyReportField, + DeletePolicyInvoiceField, EnableWorkspaceReportFieldListValueParams, + EnableWorkspaceInvoiceFieldListValueParams, OpenPolicyReportFieldsPageParams, RemoveWorkspaceReportFieldListValueParams, + RemoveWorkspaceInvoiceFieldListValueParams, UpdateWorkspaceReportFieldInitialValueParams, + UpdateWorkspaceInvoiceFieldInitialValueParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; import * as ErrorUtils from '@libs/ErrorUtils'; @@ -52,7 +58,7 @@ type CreateReportFieldParams = Pick | undefined; - target?: PolicyReportField['target']; + isInvoiceField?: boolean; policy: OnyxEntry; }; @@ -99,9 +105,9 @@ function openPolicyReportFieldsPage(policyID: string) { API.read(READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE, params); } -function openPolicyInvoiceFieldsPage(policyID: string) { +function openPolicyInvoicesPage(policyID: string) { if (!policyID) { - Log.warn('openPolicyInvoiceFieldsPage invalid params', {policyID}); + Log.warn('openPolicyInvoicesPage invalid params', {policyID}); return; } @@ -109,7 +115,7 @@ function openPolicyInvoiceFieldsPage(policyID: string) { policyID, }; - API.read(READ_COMMANDS.OPEN_POLICY_INVOICE_FIELDS_PAGE, params); + API.read(READ_COMMANDS.OPEN_POLICY_INVOICES_PAGE, params); } /** @@ -179,7 +185,7 @@ function deleteReportFieldsListValue({valueIndexes, listValues, disabledListValu /** * Creates a new report field. */ -function createReportField({name, type, initialValue, listValues, disabledListValues, policyReportIDs, target = CONST.REPORT_FIELD_TARGETS.EXPENSE, policy}: CreateReportFieldParams) { +function createReportField({name, type, initialValue, listValues, disabledListValues, policyReportIDs, isInvoiceField = false, policy}: CreateReportFieldParams) { if (!policy) { Log.warn('Policy data is not present'); return; @@ -196,7 +202,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa const optimisticReportFieldDataForPolicy: Omit, 'value'> = { name, type: optimisticType, - target: 'expense', + target: isInvoiceField ? CONST.REPORT_FIELD_TARGETS.INVOICE : CONST.REPORT_FIELD_TARGETS.EXPENSE, defaultValue: initialValue, values: listValues, disabledOptions: disabledListValues, @@ -275,13 +281,17 @@ function createReportField({name, type, initialValue, listValues, disabledListVa failureData, }; - const parameters: CreateWorkspaceReportFieldParams = { - policyID: policy?.id, - reportFields: JSON.stringify([optimisticReportFieldDataForPolicy]), - }; - - const createCommand = - target === CONST.REPORT_FIELD_TARGETS.INVOICE ? WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD : WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD; + const parameters: CreateWorkspaceReportFieldParams | CreateWorkspaceInvoiceFieldParams = isInvoiceField + ? { + policyID: policy?.id, + invoiceFields: JSON.stringify([optimisticReportFieldDataForPolicy]), + } + : { + policyID: policy?.id, + reportFields: JSON.stringify([optimisticReportFieldDataForPolicy]), + }; + + const createCommand = isInvoiceField ? WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD : WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD; API.write(createCommand, parameters, onyxData); } @@ -343,12 +353,22 @@ function deleteReportFields({policy, reportFieldsToUpdate}: DeleteReportFieldsPa ], }; - const parameters: DeletePolicyReportField = { - policyID: policy?.id, - reportFields: JSON.stringify(Object.values(updatedReportFields)), - }; - - API.write(WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD, parameters, onyxData); + const fieldsToDelete = reportFieldsToUpdate + .map((reportFieldKey) => allReportFields[reportFieldKey]) + .filter((reportField): reportField is PolicyReportField => !!reportField); + const isInvoiceField = fieldsToDelete.length > 0 && fieldsToDelete.every((reportField) => reportField.target === CONST.REPORT_FIELD_TARGETS.INVOICE); + const parameters: DeletePolicyReportField | DeletePolicyInvoiceField = isInvoiceField + ? { + policyID: policy?.id, + invoiceFields: JSON.stringify(Object.values(updatedReportFields)), + } + : { + policyID: policy?.id, + reportFields: JSON.stringify(Object.values(updatedReportFields)), + }; + const deleteCommand = isInvoiceField ? WRITE_COMMANDS.DELETE_POLICY_INVOICE_FIELD : WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD; + + API.write(deleteCommand, parameters, onyxData); } /** @@ -416,12 +436,19 @@ function updateReportFieldInitialValue({policy, reportFieldID, newInitialValue}: }, ], }; - const parameters: UpdateWorkspaceReportFieldInitialValueParams = { - policyID: policy?.id, - reportFields: JSON.stringify([updatedReportField]), - }; - - API.write(WRITE_COMMANDS.UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE, parameters, onyxData); + const isInvoiceField = updatedReportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + const parameters: UpdateWorkspaceReportFieldInitialValueParams | UpdateWorkspaceInvoiceFieldInitialValueParams = isInvoiceField + ? { + policyID: policy?.id, + invoiceFields: JSON.stringify([updatedReportField]), + } + : { + policyID: policy?.id, + reportFields: JSON.stringify([updatedReportField]), + }; + const updateCommand = isInvoiceField ? WRITE_COMMANDS.UPDATE_WORKSPACE_INVOICE_FIELD_INITIAL_VALUE : WRITE_COMMANDS.UPDATE_WORKSPACE_REPORT_FIELD_INITIAL_VALUE; + + API.write(updateCommand, parameters, onyxData); } function updateReportFieldListValueEnabled({policy, reportFieldID, valueIndexes, enabled}: UpdateReportFieldListValueEnabledParams) { @@ -460,12 +487,19 @@ function updateReportFieldListValueEnabled({policy, reportFieldID, valueIndexes, ], }; - const parameters: EnableWorkspaceReportFieldListValueParams = { - policyID: policy?.id, - reportFields: JSON.stringify([updatedReportField]), - }; - - API.write(WRITE_COMMANDS.ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE, parameters, onyxData); + const isInvoiceField = reportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + const parameters: EnableWorkspaceReportFieldListValueParams | EnableWorkspaceInvoiceFieldListValueParams = isInvoiceField + ? { + policyID: policy?.id, + invoiceFields: JSON.stringify([updatedReportField]), + } + : { + policyID: policy?.id, + reportFields: JSON.stringify([updatedReportField]), + }; + const enableCommand = isInvoiceField ? WRITE_COMMANDS.ENABLE_WORKSPACE_INVOICE_FIELD_LIST_VALUE : WRITE_COMMANDS.ENABLE_WORKSPACE_REPORT_FIELD_LIST_VALUE; + + API.write(enableCommand, parameters, onyxData); } /** @@ -500,12 +534,19 @@ function addReportFieldListValue({policy, reportFieldID, valueName}: AddReportFi ], }; - const parameters: CreateWorkspaceReportFieldListValueParams = { - policyID: policy?.id, - reportFields: JSON.stringify([updatedReportField]), - }; + const isInvoiceField = reportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + const parameters: CreateWorkspaceReportFieldListValueParams | CreateWorkspaceInvoiceFieldListValueParams = isInvoiceField + ? { + policyID: policy?.id, + invoiceFields: JSON.stringify([updatedReportField]), + } + : { + policyID: policy?.id, + reportFields: JSON.stringify([updatedReportField]), + }; + const createCommand = isInvoiceField ? WRITE_COMMANDS.CREATE_WORKSPACE_INVOICE_FIELD_LIST_VALUE : WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE; - API.write(WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE, parameters, onyxData); + API.write(createCommand, parameters, onyxData); } /** @@ -548,12 +589,19 @@ function removeReportFieldListValue({policy, reportFieldID, valueIndexes}: Remov ], }; - const parameters: RemoveWorkspaceReportFieldListValueParams = { - policyID: policy?.id, - reportFields: JSON.stringify([updatedReportField]), - }; - - API.write(WRITE_COMMANDS.REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE, parameters, onyxData); + const isInvoiceField = reportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + const parameters: RemoveWorkspaceReportFieldListValueParams | RemoveWorkspaceInvoiceFieldListValueParams = isInvoiceField + ? { + policyID: policy?.id, + invoiceFields: JSON.stringify([updatedReportField]), + } + : { + policyID: policy?.id, + reportFields: JSON.stringify([updatedReportField]), + }; + const removeCommand = isInvoiceField ? WRITE_COMMANDS.REMOVE_WORKSPACE_INVOICE_FIELD_LIST_VALUE : WRITE_COMMANDS.REMOVE_WORKSPACE_REPORT_FIELD_LIST_VALUE; + + API.write(removeCommand, parameters, onyxData); } export type {CreateReportFieldParams}; @@ -569,7 +617,7 @@ export { updateReportFieldInitialValue, updateReportFieldListValueEnabled, openPolicyReportFieldsPage, - openPolicyInvoiceFieldsPage, + openPolicyInvoicesPage, addReportFieldListValue, removeReportFieldListValue, }; diff --git a/src/pages/iou/request/step/IOURequestStepReport.tsx b/src/pages/iou/request/step/IOURequestStepReport.tsx index 7cfe90d4f96ac..bc426071ffee9 100644 --- a/src/pages/iou/request/step/IOURequestStepReport.tsx +++ b/src/pages/iou/request/step/IOURequestStepReport.tsx @@ -13,12 +13,13 @@ import useShowNotFoundPageInIOUStep from '@hooks/useShowNotFoundPageInIOUStep'; import {clearSubrates, setCustomUnitID, setCustomUnitRateID} from '@libs/actions/IOU'; import {createNewReport} from '@libs/actions/Report'; import {changeTransactionsReport, setTransactionReport} from '@libs/actions/Transaction'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getPerDiemCustomUnit, getPolicyByCustomUnitID} from '@libs/PolicyUtils'; import {getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils'; import {getPersonalDetailsForAccountID, getReportOrDraftReport, hasViolations as hasViolationsReportUtils, isPolicyExpenseChat, isReportOutstanding} from '@libs/ReportUtils'; import {shouldRestrictUserBillableActions} from '@libs/SubscriptionUtils'; -import {isPerDiemRequest} from '@libs/TransactionUtils'; +import {isDistanceRequest, isPerDiemRequest} from '@libs/TransactionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -75,6 +76,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const {policyForMovingExpensesID, shouldSelectPolicy} = usePolicyForMovingExpenses(isPerDiemRequest(transaction)); const [transactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); const [allTransactions] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, {canBeMissing: true}); + const [lastSelectedDistanceRates] = useOnyx(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {canBeMissing: true}); const hasViolations = hasViolationsReportUtils(undefined, transactionViolations, session?.accountID ?? CONST.DEFAULT_NUMBER_ID, session?.email ?? ''); const policyForMovingExpenses = policyForMovingExpensesID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyForMovingExpensesID}`] : undefined; useRestartOnReceiptFailure(transaction, reportIDFromRoute, iouType, action); @@ -89,6 +91,21 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { } }; + const updateDistanceRateForReport = (nextReportID: string | undefined, nextPolicyID: string | undefined) => { + if (!transaction || !isDistanceRequest(transaction)) { + return; + } + + const nextPolicy = nextPolicyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${nextPolicyID}`] : undefined; + const nextRateID = DistanceRequestUtils.getCustomUnitRateID({ + reportID: nextReportID, + isPolicyExpenseChat: !!nextPolicy, + policy: nextPolicy, + lastSelectedDistanceRates, + }); + setCustomUnitRateID(transaction.transactionID, nextRateID); + }; + const handleGlobalCreateReport = (item: TransactionGroupListItem) => { if (!transaction) { return; @@ -108,6 +125,8 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { const newPolicyID = reportOrDraftReportFromValue?.policyID; const policyChanged = currentPolicyID && newPolicyID && currentPolicyID !== newPolicyID; + updateDistanceRateForReport(item.value, newPolicyID ?? item.policyID); + const newPolicy = newPolicyID ? allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${newPolicyID}`] : undefined; const newPerDiemCustomUnit = getPerDiemCustomUnit(newPolicy); const newCustomUnitID = newPerDiemCustomUnit?.customUnitID; @@ -153,6 +172,8 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { !isEditing, ); + updateDistanceRateForReport(item.value, item.policyID); + if (isEditing) { changeTransactionsReport({ transactionIDs: [transaction.transactionID], @@ -200,6 +221,7 @@ function IOURequestStepReport({route, transaction}: IOURequestStepReportProps) { Navigation.dismissToSuperWideRHP(); // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { + updateDistanceRateForReport(CONST.REPORT.UNREPORTED_REPORT_ID, undefined); changeTransactionsReport({ transactionIDs: [transaction.transactionID], isASAPSubmitBetaEnabled, diff --git a/src/pages/workspace/fields/CreateFieldsPage.tsx b/src/pages/workspace/fields/CreateFieldsPage.tsx new file mode 100644 index 0000000000000..3e8da5a25cff6 --- /dev/null +++ b/src/pages/workspace/fields/CreateFieldsPage.tsx @@ -0,0 +1,262 @@ +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import type {ValueOf} from 'type-fest'; +import {View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues, FormRef} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextPicker from '@components/TextPicker'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import DateUtils from '@libs/DateUtils'; +import {addErrorMessage} from '@libs/ErrorUtils'; +import {hasCircularReferences} from '@libs/Formula'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import {isRequiredFulfilled} from '@libs/ValidationUtils'; +import {hasFormulaPartsInInitialValue, isReportFieldNameExisting} from '@libs/WorkspaceReportFieldUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import InitialListValueSelector from '@pages/workspace/reports/InitialListValueSelector'; +import TypeSelector from '@pages/workspace/reports/TypeSelector'; +import {createReportField, setInitialCreateReportFieldsForm} from '@userActions/Policy/ReportField'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; +import type {Policy, Report} from '@src/types/onyx'; + +type CreateFieldsPageProps = { + policy: OnyxEntry; + policyID: string; + isInvoiceField: boolean; + listValuesRoute: string; + featureName: ValueOf; + testID: string; +}; + +const defaultDate = DateUtils.extractDate(new Date().toString()); + +function CreateFieldsPage({policy, policyID, isInvoiceField, listValuesRoute, featureName, testID}: CreateFieldsPageProps) { + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + const formRef = useRef(null); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); + + const reportTypeForTarget = useMemo(() => (isInvoiceField ? CONST.REPORT.TYPE.INVOICE : CONST.REPORT.TYPE.EXPENSE), [isInvoiceField]); + + const policyReportIDsSelector = useCallback( + (reports: OnyxCollection) => + Object.values(reports ?? {}) + .filter((report) => report?.policyID === policyID && report.type === reportTypeForTarget) + .map((report) => report?.reportID), + [policyID, reportTypeForTarget], + ); + + const [policyReportIDs] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { + canBeMissing: true, + selector: policyReportIDsSelector, + }); + + const availableListValuesLength = (formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? []).filter((disabledListValue) => !disabledListValue).length; + + const submitForm = useCallback( + (values: FormOnyxValues) => { + createReportField({ + policy, + name: values[INPUT_IDS.NAME], + type: values[INPUT_IDS.TYPE], + initialValue: !(values[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength === 0) ? values[INPUT_IDS.INITIAL_VALUE] : '', + listValues: formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], + disabledListValues: formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? [], + policyReportIDs, + isInvoiceField, + }); + Navigation.goBack(); + }, + [availableListValuesLength, formDraft, policy, policyReportIDs, isInvoiceField], + ); + + const validateForm = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const {name, type, initialValue: formInitialValue} = values; + const errors: FormInputErrors = {}; + + if (!isRequiredFulfilled(name)) { + errors[INPUT_IDS.NAME] = translate('workspace.reportFields.reportFieldNameRequiredError'); + } else if (isReportFieldNameExisting(policy?.fieldList, name)) { + errors[INPUT_IDS.NAME] = translate('workspace.reportFields.existingReportFieldNameError'); + } else if ([...name].length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units. + addErrorMessage(errors, INPUT_IDS.NAME, translate('common.error.characterLimitExceedCounter', [...name].length, CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH)); + } + + if (!isRequiredFulfilled(type)) { + errors[INPUT_IDS.TYPE] = translate('workspace.reportFields.reportFieldTypeRequiredError'); + } + + // formInitialValue can be undefined because the InitialValue component is rendered conditionally. + // If it's not been rendered when the validation is executed, formInitialValue will be undefined. + if (type === CONST.REPORT_FIELD_TYPES.TEXT && !!formInitialValue && formInitialValue.length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('common.error.characterLimitExceedCounter', formInitialValue.length, CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH); + } + + if ((type === CONST.REPORT_FIELD_TYPES.TEXT || type === CONST.REPORT_FIELD_TYPES.FORMULA) && hasCircularReferences(formInitialValue, name, policy?.fieldList)) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.circularReferenceError'); + } + + if (type === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength > 0 && !isRequiredFulfilled(formInitialValue)) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.reportFieldInitialValueRequiredError'); + } + + return errors; + }, + [availableListValuesLength, policy?.fieldList, translate], + ); + + const validateName = useCallback( + (values: Record) => { + const errors: Record = {}; + const name = values[INPUT_IDS.NAME]; + if (isReportFieldNameExisting(policy?.fieldList, name)) { + errors[INPUT_IDS.NAME] = translate('workspace.reportFields.existingReportFieldNameError'); + } + return errors; + }, + [policy?.fieldList, translate], + ); + + const handleOnValueCommitted = useCallback( + (inputValues: FormOnyxValues) => (initialValue: string) => { + // Mirror optimisticType logic from createReportField: if user enters a formula + // while type is Text, automatically switch the type to Formula in the form, otherwise back to Text. + const isFormula = hasFormulaPartsInInitialValue(initialValue); + if (isFormula) { + formRef.current?.resetForm({ + ...inputValues, + [INPUT_IDS.TYPE]: CONST.REPORT_FIELD_TYPES.FORMULA, + [INPUT_IDS.INITIAL_VALUE]: initialValue, + }); + } else { + formRef.current?.resetForm({ + ...inputValues, + [INPUT_IDS.TYPE]: CONST.REPORT_FIELD_TYPES.TEXT, + [INPUT_IDS.INITIAL_VALUE]: initialValue, + }); + } + }, + [], + ); + + useEffect(() => { + setInitialCreateReportFieldsForm(); + }, []); + + const listValues = [...(formDraft?.[INPUT_IDS.LIST_VALUES] ?? [])].sort(localeCompare).join(', '); + + return ( + + + + + {({inputValues}) => ( + + + { + let initialValue; + if (type === CONST.REPORT_FIELD_TYPES.DATE) { + initialValue = defaultDate; + } else if (type === CONST.REPORT_FIELD_TYPES.FORMULA) { + initialValue = '{report:id}'; + } else { + initialValue = ''; + } + + formRef.current?.resetForm({ + ...inputValues, + type, + initialValue, + }); + }} + /> + + {inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && ( + Navigation.navigate(listValuesRoute)} + title={listValues} + numberOfLinesTitle={5} + /> + )} + + {(inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.TEXT || inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.FORMULA) && ( + + )} + {inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && ( + + )} + + )} + + + + ); +} + +export default CreateFieldsPage; diff --git a/src/pages/workspace/fields/FieldsAddListValuePage.tsx b/src/pages/workspace/fields/FieldsAddListValuePage.tsx new file mode 100644 index 0000000000000..0e7e660ef743a --- /dev/null +++ b/src/pages/workspace/fields/FieldsAddListValuePage.tsx @@ -0,0 +1,115 @@ +import React, {useCallback, useMemo} from 'react'; +import type {ValueOf} from 'type-fest'; +import {Keyboard} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import {getReportFieldKey} from '@libs/ReportUtils'; +import {validateReportFieldListValueName} from '@libs/WorkspaceReportFieldUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {addReportFieldListValue, createReportFieldsListValue} from '@userActions/Policy/ReportField'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; +import type {Policy} from '@src/types/onyx'; +import type {OnyxEntry} from 'react-native-onyx'; + +type FieldsAddListValuePageProps = { + policy: OnyxEntry; + policyID: string; + reportFieldID?: string; + featureName: ValueOf; + testID: string; +}; + +function FieldsAddListValuePage({policy, policyID, reportFieldID, featureName, testID}: FieldsAddListValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); + + const listValues = useMemo(() => { + let reportFieldListValues: string[]; + if (reportFieldID) { + const reportFieldKey = getReportFieldKey(reportFieldID); + reportFieldListValues = Object.values(policy?.fieldList?.[reportFieldKey]?.values ?? {}); + } else { + reportFieldListValues = formDraft?.[INPUT_IDS.LIST_VALUES] ?? []; + } + return reportFieldListValues; + }, [formDraft, policy?.fieldList, reportFieldID]); + + const validate = useCallback( + (values: FormOnyxValues) => + validateReportFieldListValueName(values[INPUT_IDS.VALUE_NAME].trim(), '', listValues, INPUT_IDS.VALUE_NAME, translate), + [listValues, translate], + ); + + const createValue = useCallback( + (values: FormOnyxValues) => { + if (reportFieldID) { + addReportFieldListValue({policy, reportFieldID, valueName: values[INPUT_IDS.VALUE_NAME]}); + } else { + createReportFieldsListValue({ + valueName: values[INPUT_IDS.VALUE_NAME], + listValues: formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], + disabledListValues: formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? [], + }); + } + Keyboard.dismiss(); + Navigation.goBack(); + }, + [formDraft, policy, reportFieldID], + ); + + return ( + + + + + + + + + ); +} + +export default FieldsAddListValuePage; diff --git a/src/pages/workspace/fields/FieldsEditValuePage.tsx b/src/pages/workspace/fields/FieldsEditValuePage.tsx new file mode 100644 index 0000000000000..93c0ff317fbdc --- /dev/null +++ b/src/pages/workspace/fields/FieldsEditValuePage.tsx @@ -0,0 +1,105 @@ +import React, {useCallback} from 'react'; +import type {ValueOf} from 'type-fest'; +import {Keyboard} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import {validateReportFieldListValueName} from '@libs/WorkspaceReportFieldUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {renameReportFieldsListValue} from '@userActions/Policy/ReportField'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; +import type {Policy} from '@src/types/onyx'; +import type {OnyxEntry} from 'react-native-onyx'; + +type FieldsEditValuePageProps = { + policy: OnyxEntry; + policyID: string; + valueIndex: number; + featureName: ValueOf; + testID: string; +}; + +function FieldsEditValuePage({policy, policyID, valueIndex, featureName, testID}: FieldsEditValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); + + const currentValueName = formDraft?.listValues?.[valueIndex] ?? ''; + + const validate = useCallback( + (values: FormOnyxValues) => + validateReportFieldListValueName(values[INPUT_IDS.NEW_VALUE_NAME].trim(), currentValueName, formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], INPUT_IDS.NEW_VALUE_NAME, translate), + [currentValueName, formDraft, translate], + ); + + const editValue = useCallback( + (values: FormOnyxValues) => { + const valueName = values[INPUT_IDS.NEW_VALUE_NAME]?.trim(); + if (currentValueName !== valueName) { + renameReportFieldsListValue({ + valueIndex, + newValueName: valueName, + listValues: formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], + }); + } + Keyboard.dismiss(); + Navigation.goBack(); + }, + [currentValueName, formDraft, valueIndex], + ); + + return ( + + + + + + + + + ); +} + +export default FieldsEditValuePage; diff --git a/src/pages/workspace/fields/FieldsInitialValuePage.tsx b/src/pages/workspace/fields/FieldsInitialValuePage.tsx new file mode 100644 index 0000000000000..e43def7fdcca0 --- /dev/null +++ b/src/pages/workspace/fields/FieldsInitialValuePage.tsx @@ -0,0 +1,157 @@ +import React, {useCallback, useState} from 'react'; +import type {ValueOf} from 'type-fest'; +import {View} from 'react-native'; +import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; +import type {FormInputErrors, FormOnyxValues} from '@components/Form/types'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import TextInput from '@components/TextInput'; +import useAutoFocusInput from '@hooks/useAutoFocusInput'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {hasCircularReferences} from '@libs/Formula'; +import Navigation from '@libs/Navigation/Navigation'; +import {getReportFieldKey} from '@libs/ReportUtils'; +import {isRequiredFulfilled} from '@libs/ValidationUtils'; +import {getReportFieldInitialValue} from '@libs/WorkspaceReportFieldUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {updateReportFieldInitialValue} from '@userActions/Policy/ReportField'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; +import type {Policy} from '@src/types/onyx'; +import type {OnyxEntry} from 'react-native-onyx'; +import ReportFieldsInitialListValuePicker from '@pages/workspace/reports/InitialListValueSelector/ReportFieldsInitialListValuePicker'; + +type FieldsInitialValuePageProps = { + policy: OnyxEntry; + policyID: string; + reportFieldID: string; + featureName: ValueOf; + testID: string; +}; + +function FieldsInitialValuePage({policy, policyID, reportFieldID, featureName, testID}: FieldsInitialValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + + const reportField = policy?.fieldList?.[getReportFieldKey(reportFieldID)] ?? null; + const availableListValuesLength = (reportField?.disabledOptions ?? []).filter((disabledListValue) => !disabledListValue).length; + const currentInitialValue = getReportFieldInitialValue(reportField, translate); + const [initialValue, setInitialValue] = useState(currentInitialValue); + + const submitForm = useCallback( + (values: FormOnyxValues) => { + if (currentInitialValue !== values.initialValue) { + updateReportFieldInitialValue({policy, reportFieldID, newInitialValue: values.initialValue}); + } + Navigation.goBack(); + }, + [currentInitialValue, policy, reportFieldID], + ); + + const submitListValueUpdate = (value: string) => { + updateReportFieldInitialValue({policy, reportFieldID, newInitialValue: currentInitialValue === value ? '' : value}); + Navigation.goBack(); + }; + + const validateForm = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const {initialValue: formInitialValue} = values; + const errors: FormInputErrors = {}; + + if (formInitialValue.length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('common.error.characterLimitExceedCounter', formInitialValue.length, CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH); + } + + if ( + (reportField?.type === CONST.REPORT_FIELD_TYPES.TEXT || reportField?.type === CONST.REPORT_FIELD_TYPES.FORMULA) && + hasCircularReferences(formInitialValue, reportField?.name, policy?.fieldList) + ) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.circularReferenceError'); + } + + if (reportField?.type === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength > 0 && !isRequiredFulfilled(formInitialValue)) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.reportFieldInitialValueRequiredError'); + } + + return errors; + }, + [availableListValuesLength, reportField?.name, reportField?.type, policy?.fieldList, translate], + ); + + if (!reportField) { + return ; + } + + const isTextFieldType = reportField.type === CONST.REPORT_FIELD_TYPES.TEXT; + const isFormulaFieldType = reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA; + const isListFieldType = reportField.type === CONST.REPORT_FIELD_TYPES.LIST; + + return ( + + + + {isListFieldType && ( + + {translate('workspace.reportFields.listValuesInputSubtitle')} + + )} + + {(isTextFieldType || isFormulaFieldType) && ( + + + + )} + {isListFieldType && ( + + )} + + + ); +} + +export default FieldsInitialValuePage; diff --git a/src/pages/workspace/fields/FieldsListValuesPage.tsx b/src/pages/workspace/fields/FieldsListValuesPage.tsx new file mode 100644 index 0000000000000..b64f9875f0fe0 --- /dev/null +++ b/src/pages/workspace/fields/FieldsListValuesPage.tsx @@ -0,0 +1,439 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import type {ValueOf} from 'type-fest'; +import {InteractionManager, View} from 'react-native'; +import Button from '@components/Button'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import ConfirmModal from '@components/ConfirmModal'; +import EmptyStateComponent from '@components/EmptyStateComponent'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +// eslint-disable-next-line no-restricted-imports +import * as Expensicons from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import SearchBar from '@components/SearchBar'; +import TableListItem from '@components/SelectionList/ListItem/TableListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionListWithModal from '@components/SelectionListWithModal'; +import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; +import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; +import Switch from '@components/Switch'; +import Text from '@components/Text'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import { + deleteReportFieldsListValue, + removeReportFieldListValue, + setReportFieldsListValueEnabled, + updateReportFieldListValueEnabled as updateReportFieldListValueEnabledReportField, +} from '@libs/actions/Policy/ReportField'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections as hasAccountingConnectionsPolicyUtils} from '@libs/PolicyUtils'; +import {getReportFieldKey} from '@libs/ReportUtils'; +import StringUtils from '@libs/StringUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import type {OnyxEntry} from 'react-native-onyx'; + +type ValueListItem = ListItem & { + /** The value */ + value: string; + + /** Whether the value is enabled */ + enabled: boolean; + + /** The value order weight in the list */ + orderWeight?: number; +}; + +type FieldsListValuesPageProps = { + policy: OnyxEntry; + policyID: string; + reportFieldID?: string; + isInvoicePage: boolean; + featureName: ValueOf; + getValueSettingsRoute: (isInvoiceRoute: boolean, policyID: string, valueIndex: number, reportFieldID?: string) => string; + getAddValueRoute: (isInvoiceRoute: boolean, policyID: string, reportFieldID?: string) => string; + testID: string; +}; + +function FieldsListValuesPage({ + policy, + policyID, + reportFieldID, + isInvoicePage, + featureName, + getValueSettingsRoute, + getAddValueRoute, + testID, +}: FieldsListValuesPageProps) { + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here to use the mobile selection mode on small screens only + // See https://github.com/Expensify/App/issues/48724 for more details + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); + const isMobileSelectionModeEnabled = useMobileSelectionMode(); + const illustrations = useMemoizedLazyIllustrations(['FolderWithPapers']); + + const [selectedValues, setSelectedValues] = useState>({}); + const [deleteValuesConfirmModalVisible, setDeleteValuesConfirmModalVisible] = useState(false); + const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); + const reportField = reportFieldID ? policy?.fieldList?.[getReportFieldKey(reportFieldID)] : undefined; + const shouldUseInvoiceRoutes = isInvoicePage || reportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + + const canSelectMultiple = isSmallScreenWidth ? isMobileSelectionModeEnabled : true; + + const [listValues, disabledListValues] = useMemo(() => { + let reportFieldValues: string[]; + let reportFieldDisabledValues: boolean[]; + + if (reportFieldID) { + const reportFieldKey = getReportFieldKey(reportFieldID); + + reportFieldValues = Object.values(policy?.fieldList?.[reportFieldKey]?.values ?? {}); + reportFieldDisabledValues = Object.values(policy?.fieldList?.[reportFieldKey]?.disabledOptions ?? {}); + } else { + reportFieldValues = formDraft?.listValues ?? []; + reportFieldDisabledValues = formDraft?.disabledListValues ?? []; + } + + return [reportFieldValues, reportFieldDisabledValues]; + }, [formDraft?.disabledListValues, formDraft?.listValues, policy?.fieldList, reportFieldID]); + + const updateReportFieldListValueEnabled = useCallback( + (value: boolean, valueIndex: number) => { + if (reportFieldID) { + updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: [Number(valueIndex)], enabled: value}); + return; + } + + setReportFieldsListValueEnabled({ + valueIndexes: [valueIndex], + enabled: value, + disabledListValues, + }); + }, + [disabledListValues, policy, reportFieldID], + ); + + useSearchBackPress({ + onClearSelection: () => { + setSelectedValues({}); + }, + onNavigationCallBack: () => Navigation.goBack(), + }); + + const data = useMemo( + () => + listValues.map((value, index) => ({ + value, + index, + text: value, + keyForList: value, + isSelected: selectedValues[value] && canSelectMultiple, + enabled: !disabledListValues.at(index), + rightElement: ( + updateReportFieldListValueEnabled(newValue, index)} + /> + ), + })), + [canSelectMultiple, disabledListValues, listValues, selectedValues, translate, updateReportFieldListValueEnabled], + ); + + const filterListValue = useCallback((item: ValueListItem, searchInput: string) => { + const itemText = StringUtils.normalize(item.text?.toLowerCase() ?? ''); + const normalizedSearchInput = StringUtils.normalize(searchInput.toLowerCase()); + return itemText.includes(normalizedSearchInput); + }, []); + const sortListValues = useCallback((values: ValueListItem[]) => values.sort((a, b) => localeCompare(a.value, b.value)), [localeCompare]); + const [inputValue, setInputValue, filteredListValues] = useSearchResults(data, filterListValue, sortListValues); + + const filteredListValuesArray = filteredListValues.map((item) => item.value); + + const shouldShowEmptyState = Object.values(listValues ?? {}).length <= 0; + const selectedValuesArray = Object.keys(selectedValues).filter((key) => selectedValues[key] && listValues.includes(key)); + + const toggleValue = (valueItem: ValueListItem) => { + setSelectedValues((prev) => ({ + ...prev, + [valueItem.value]: !prev[valueItem.value], + })); + }; + + const toggleAllValues = () => { + setSelectedValues(selectedValuesArray.length > 0 ? {} : Object.fromEntries(filteredListValuesArray.map((value) => [value, true]))); + }; + + const handleDeleteValues = () => { + const valuesToDelete = selectedValuesArray.reduce((acc, valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + + if (index !== -1) { + acc.push(index); + } + + return acc; + }, []); + + if (reportFieldID) { + removeReportFieldListValue({policy, reportFieldID, valueIndexes: valuesToDelete}); + } else { + deleteReportFieldsListValue({ + valueIndexes: valuesToDelete, + listValues, + disabledListValues, + }); + } + + setDeleteValuesConfirmModalVisible(false); + + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + setSelectedValues({}); + }); + }; + + const openListValuePage = (valueItem: ValueListItem) => { + if (valueItem.index === undefined) { + return; + } + + Navigation.navigate(getValueSettingsRoute(shouldUseInvoiceRoutes, policyID, valueItem.index, reportFieldID)); + }; + + const getCustomListHeader = () => { + if (filteredListValues.length === 0) { + return null; + } + return ( + + ); + }; + + const getHeaderButtons = () => { + const options: Array>> = []; + if (isSmallScreenWidth ? isMobileSelectionModeEnabled : selectedValuesArray.length > 0) { + if (selectedValuesArray.length > 0 && !hasAccountingConnections) { + options.push({ + icon: Expensicons.Trashcan, + text: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), + value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, + onSelected: () => setDeleteValuesConfirmModalVisible(true), + }); + } + const enabledValues = selectedValuesArray.filter((valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + return !disabledListValues?.at(index); + }); + + if (enabledValues.length > 0) { + const valuesToDisable = selectedValuesArray.reduce((acc, valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + if (!disabledListValues?.at(index) && index !== -1) { + acc.push(index); + } + + return acc; + }, []); + + options.push({ + icon: Expensicons.Close, + text: translate(enabledValues.length === 1 ? 'workspace.reportFields.disableValue' : 'workspace.reportFields.disableValues'), + value: CONST.POLICY.BULK_ACTION_TYPES.DISABLE, + onSelected: () => { + setSelectedValues({}); + + if (reportFieldID) { + updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: valuesToDisable, enabled: false}); + return; + } + + setReportFieldsListValueEnabled({ + valueIndexes: valuesToDisable, + enabled: false, + disabledListValues, + }); + }, + }); + } + + const disabledValues = selectedValuesArray.filter((valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + return disabledListValues?.at(index); + }); + + if (disabledValues.length > 0) { + const valuesToEnable = selectedValuesArray.reduce((acc, valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + if (disabledListValues?.at(index) && index !== -1) { + acc.push(index); + } + + return acc; + }, []); + + options.push({ + icon: Expensicons.Checkmark, + text: translate(disabledValues.length === 1 ? 'workspace.reportFields.enableValue' : 'workspace.reportFields.enableValues'), + value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, + onSelected: () => { + setSelectedValues({}); + + if (reportFieldID) { + updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: valuesToEnable, enabled: true}); + return; + } + + setReportFieldsListValueEnabled({ + valueIndexes: valuesToEnable, + enabled: true, + disabledListValues, + }); + }, + }); + } + + return ( + null} + shouldAlwaysShowDropdownMenu + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} + customText={translate('workspace.common.selected', {count: selectedValuesArray.length})} + options={options} + isSplitButton={false} + style={[isSmallScreenWidth && styles.flexGrow1, isSmallScreenWidth && styles.mb3]} + isDisabled={!selectedValuesArray.length} + /> + ); + } + + if (!hasAccountingConnections) { + return ( +