From 02d9d2a73b9e2005b36ee62cffad4b0babd2e18c Mon Sep 17 00:00:00 2001 From: ShridharGoel <35566748+ShridharGoel@users.noreply.github.com> Date: Thu, 11 Jun 2026 23:16:38 +0530 Subject: [PATCH 1/5] Implement invoice fields --- src/CONST/index.ts | 13 + src/ROUTES.ts | 29 ++ src/SCREENS.ts | 7 + .../MoneyRequestViewReportFields.tsx | 9 +- .../ReportActionItem/MoneyReportView.tsx | 11 +- src/languages/de.ts | 30 ++ src/languages/en.ts | 30 ++ src/languages/es.ts | 30 ++ src/languages/fr.ts | 30 ++ src/languages/it.ts | 30 ++ src/languages/ja.ts | 30 ++ src/languages/nl.ts | 30 ++ src/languages/pl.ts | 30 ++ src/languages/pt-BR.ts | 30 ++ src/languages/zh-hans.ts | 30 ++ ...ateWorkspaceInvoiceFieldListValueParams.ts | 10 + .../CreateWorkspaceInvoiceFieldParams.ts | 10 + .../parameters/DeletePolicyInvoiceField.ts | 10 + .../EnablePolicyInvoiceFieldsParams.ts | 6 + ...bleWorkspaceInvoiceFieldListValueParams.ts | 10 + ...oveWorkspaceInvoiceFieldListValueParams.ts | 10 + ...WorkspaceInvoiceFieldInitialValueParams.ts | 10 + src/libs/API/parameters/index.ts | 7 + src/libs/API/types.ts | 16 + .../ModalStackNavigators/index.tsx | 7 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 13 +- src/libs/Navigation/linkingConfig/config.ts | 21 + src/libs/Navigation/types.ts | 31 ++ src/libs/ReportActionsUtils.ts | 1 + src/libs/ReportUtils.ts | 44 +- src/libs/WorkspaceReportFieldUtils.ts | 36 +- src/libs/actions/Policy/CopyPolicySettings.ts | 18 +- src/libs/actions/Policy/Policy.ts | 43 +- src/libs/actions/Policy/ReportField.ts | 169 +++++-- src/libs/actions/Report/index.ts | 61 ++- src/libs/actions/RequestConflictUtils.ts | 1 + src/pages/DynamicEditReportFieldPage.tsx | 11 +- .../WorkspaceDuplicateSelectFeaturesForm.tsx | 5 +- .../workspace/fields/CreateFieldsPage.tsx | 284 ++++++++++++ .../fields/FieldsAddListValuePage.tsx | 122 +++++ .../workspace/fields/FieldsEditValuePage.tsx | 112 +++++ .../fields/FieldsInitialValuePage.tsx | 171 +++++++ .../workspace/fields/FieldsListValuesPage.tsx | 438 ++++++++++++++++++ .../workspace/fields/FieldsSettingsPage.tsx | 164 +++++++ .../fields/FieldsValueSettingsPage.tsx | 178 +++++++ .../fields/WorkspaceFieldsSection.tsx | 278 +++++++++++ .../invoices/CreateInvoiceFieldsPage.tsx | 31 ++ .../InvoiceFieldsAddListValuePage.tsx | 30 ++ .../invoices/InvoiceFieldsEditValuePage.tsx | 30 ++ .../InvoiceFieldsInitialValuePage.tsx | 30 ++ .../invoices/InvoiceFieldsListValuesPage.tsx | 40 ++ .../invoices/InvoiceFieldsSettingsPage.tsx | 36 ++ .../InvoiceFieldsValueSettingsPage.tsx | 36 ++ .../WorkspaceInvoiceFieldsSection.tsx | 42 ++ .../invoices/WorkspaceInvoicesPage.tsx | 2 + .../reports/CreateReportFieldsPage.tsx | 271 +---------- .../reports/ReportFieldsAddListValuePage.tsx | 102 +--- .../reports/ReportFieldsEditValuePage.tsx | 95 +--- .../reports/ReportFieldsInitialValuePage.tsx | 155 +------ .../reports/ReportFieldsListValuesPage.tsx | 423 +---------------- .../reports/ReportFieldsSettingsPage.tsx | 126 +---- .../reports/ReportFieldsValueSettingsPage.tsx | 156 +------ .../reports/WorkspaceReportsPage.tsx | 212 ++------- .../upgrade/WorkspaceUpgradePage.tsx | 6 + src/pages/workspace/withPolicy.tsx | 10 + src/types/onyx/Policy.ts | 3 + 66 files changed, 2976 insertions(+), 1526 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/EnablePolicyInvoiceFieldsParams.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/fields/WorkspaceFieldsSection.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 create mode 100644 src/pages/workspace/invoices/WorkspaceInvoiceFieldsSection.tsx diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 1216203604ba..6732e4490a2a 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3984,6 +3984,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', @@ -7250,6 +7251,14 @@ const CONST = { description: 'workspace.upgrade.reportFields.description' as const, icon: 'Pencil', }, + invoiceFields: { + id: 'invoiceFields' as const, + alias: 'invoice-fields', + name: 'Invoice Fields', + title: 'workspace.upgrade.invoiceFields.title' as const, + description: 'workspace.upgrade.invoiceFields.description' as const, + icon: 'Pencil', + }, policyPreventMemberChangingTitle: { id: 'policyPreventMemberChangingTitle' as const, alias: 'policy-prevent-member-changing-title', @@ -7511,6 +7520,10 @@ const CONST = { LIST: 'dropdown', FORMULA: 'formula', }, + REPORT_FIELD_TARGETS: { + EXPENSE: 'expense', + INVOICE: 'invoice', + }, NAVIGATION_ACTIONS: { RESET: 'RESET', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a569263733c1..228932825fa1 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -2382,6 +2382,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) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index ecd94afdbc75..0fd266a29d12 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -767,6 +767,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', ROOMS: 'Workspace_Rooms', ROOM_CREATE: 'Workspace_Room_Create', diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 73d105909a67..efb92e020090 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -50,6 +50,10 @@ type EnrichedPolicyReportField = { violationTranslation: string; } & PolicyReportField; +function isReportFieldTargetMatchingReport(report: OnyxEntry, field: PolicyReportField) { + return field.target === report?.type || (report?.type === CONST.REPORT.TYPE.EXPENSE && !field.target); +} + function ReportFieldView(reportField: EnrichedPolicyReportField, report: OnyxEntry, styles: ThemeStyles, pendingAction?: PendingAction) { return ( field.target === report?.type) + .filter((field) => isReportFieldTargetMatchingReport(report, field)) .filter((reportField) => !shouldHideSingleReportField(reportField)) .sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight) .map((field): EnrichedPolicyReportField => { @@ -121,8 +125,9 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); + const areFieldsEnabledForReport = isInvoiceReport ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled; - const shouldDisplayReportFields = (isGroupPolicyExpenseReport || isInvoiceReport) && !!policy?.areReportFieldsEnabled && (!isOnlyTitleFieldEnabled || !isCombinedReport); + const shouldDisplayReportFields = (isGroupPolicyExpenseReport || isInvoiceReport) && !!areFieldsEnabledForReport && (!isOnlyTitleFieldEnabled || !isCombinedReport); if (!shouldDisplayReportFields || !sortedPolicyReportFields.length) { return null; diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 1e8b277803c6..dca2500b4fb6 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -49,7 +49,7 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import {clearReportFieldKeyErrors} from '@src/libs/actions/Report'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; -import type {Policy, Report} from '@src/types/onyx'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; type MoneyReportViewProps = { @@ -82,6 +82,10 @@ type MoneyReportViewProps = { isTotalPending?: boolean; }; +function isReportFieldTargetMatchingReport(report: OnyxEntry, field: PolicyReportField) { + return field.target === report?.type || (report?.type === CONST.REPORT.TYPE.EXPENSE && !field.target); +} + function MoneyReportView({ report, policy, @@ -139,7 +143,7 @@ function MoneyReportView({ const {sortedPolicyReportFields, fieldValues, fieldsByName} = useMemo(() => { const {fieldValues: values, fieldsByName: byName} = getReportFieldMaps(report, policy?.fieldList ?? {}); const sorted = Object.values(byName) - .filter((field) => field.target === report?.type) + .filter((field) => isReportFieldTargetMatchingReport(report, field)) .sort(({orderWeight: a}, {orderWeight: b}) => a - b); return {sortedPolicyReportFields: sorted, fieldValues: values, fieldsByName: byName}; }, [policy?.fieldList, report]); @@ -152,6 +156,7 @@ function MoneyReportView({ const isGroupPolicyExpenseReport = isGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); + const areFieldsEnabledForReport = isInvoiceReport ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled; const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isGroupPolicyExpenseReport || isInvoiceReport) && @@ -184,7 +189,7 @@ function MoneyReportView({ {!isClosedExpenseReportWithNoExpenses && ( <> {(isGroupPolicyExpenseReport || isInvoiceReport) && - !!policy?.areReportFieldsEnabled && + areFieldsEnabledForReport && (!isCombinedReport || !isOnlyTitleFieldEnabled) && sortedPolicyReportFields.map((reportField) => { if (shouldHideSingleReportField(reportField)) { diff --git a/src/languages/de.ts b/src/languages/de.ts index 904cf07f8785..35175efae713 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -4342,6 +4342,7 @@ ${amount} für ${merchant} – ${date}`, customFieldHint: 'Füge benutzerdefinierte Codierung hinzu, die für alle Ausgaben dieses Mitglieds gilt.', reports: 'Berichte', reportFields: 'Berichtsfelder', + invoiceFields: 'Rechnungsfelder', reportTitle: 'Berichtstitel', reportField: 'Berichts­feld', taxes: 'Steuern', @@ -6026,6 +6027,29 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU reportFieldInitialValueRequiredError: 'Bitte wähle einen Anfangswert für ein Berichtsfeld aus', genericFailureMessage: 'Beim Aktualisieren des Berichtfelds ist ein Fehler aufgetreten. Bitte versuche es erneut.', }, + invoiceFields: { + subtitle: 'Rechnungsfelder können hilfreich sein, wenn du zusätzliche Informationen einfügen möchtest.', + importedFromAccountingSoftware: 'Die folgenden Rechnungsfelder werden importiert aus Ihrem', + disableInvoiceFields: 'Rechnungsfelder deaktivieren', + disableInvoiceFieldsConfirmation: 'Sind Sie sicher? Rechnungsfelder werden auf Rechnungen deaktiviert.', + delete: 'Rechnungsfeld löschen', + deleteConfirmation: 'Sind Sie sicher, dass Sie dieses Rechnungsfeld löschen möchten?', + findInvoiceField: 'Rechnungsfeld suchen', + nameInputSubtitle: 'Wähle einen Namen für das Rechnungsfeld.', + typeInputSubtitle: 'Wähle aus, welcher Rechnungsfeldtyp verwendet werden soll.', + initialValueInputSubtitle: 'Gib einen Startwert ein, der im Rechnungsfeld angezeigt werden soll.', + listValuesInputSubtitle: 'Diese Werte werden im Dropdown-Menü des Rechnungsfelds angezeigt. Aktivierte Werte können von Mitgliedern ausgewählt werden.', + listInputSubtitle: 'Diese Werte werden in der Rechnungsfeldliste angezeigt. Aktivierte Werte können von Mitgliedern ausgewählt werden.', + emptyInvoiceFieldsValues: { + title: 'Noch keine Listenwerte', + subtitle: 'Füge benutzerdefinierte Werte hinzu, die auf Rechnungen angezeigt werden.', + }, + existingInvoiceFieldNameError: 'Ein Rechnungsfeld mit diesem Namen existiert bereits', + invoiceFieldNameRequiredError: 'Bitte gib einen Namen für das Rechnungsfeld ein', + invoiceFieldTypeRequiredError: 'Bitte wähle einen Rechnungsfeldtyp aus', + invoiceFieldInitialValueRequiredError: 'Bitte wähle einen Anfangswert für ein Rechnungsfeld aus', + addField: 'Feld hinzufügen', + }, tags: { tagName: 'Tag-Name', requiresTag: 'Mitglieder müssen alle Ausgaben taggen', @@ -6830,6 +6854,12 @@ Möchten Sie sie wirklich noch einmal exportieren?`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Berichtsfelder sind nur im Control-Tarif verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, }, + invoiceFields: { + title: 'Rechnungsfelder', + description: `Mit Rechnungsfeldern kannst du zusätzliche Details auf Rechnungsebene in Rechnungen aufnehmen.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Rechnungsfelder sind nur im Control-Tarif verfügbar, ab ${formattedPrice} ${hasTeam2025Pricing ? `pro Mitglied und Monat.` : `pro aktivem Mitglied und Monat.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Profitiere von automatischer Synchronisierung und reduziere manuelle Eingaben mit der Expensify + NetSuite-Integration. Gewinne detaillierte Finanzanalysen in Echtzeit mit nativer und benutzerdefinierter Segmentunterstützung, einschließlich Projekt- und Kundenzuordnung.`, diff --git a/src/languages/en.ts b/src/languages/en.ts index 4a8d69409d11..e806f974ca36 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -4431,6 +4431,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', @@ -6073,6 +6074,29 @@ 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.', + delete: 'Delete invoice field', + deleteConfirmation: 'Are you sure you want to delete this invoice field?', + findInvoiceField: 'Find invoice field', + nameInputSubtitle: 'Choose a name for the invoice field.', + typeInputSubtitle: 'Choose what type of invoice field to use.', + initialValueInputSubtitle: 'Enter a starting value to show in the invoice field.', + listValuesInputSubtitle: 'These values will appear in your invoice field dropdown. Enabled values can be selected by members.', + listInputSubtitle: 'These values will appear in your invoice field list. Enabled values can be selected by members.', + emptyInvoiceFieldsValues: { + title: 'No list values yet', + subtitle: 'Add custom values to appear on invoices.', + }, + existingInvoiceFieldNameError: 'An invoice field with this name already exists', + invoiceFieldNameRequiredError: 'Please enter an invoice field name', + invoiceFieldTypeRequiredError: 'Please choose an invoice field type', + invoiceFieldInitialValueRequiredError: 'Please choose an invoice field initial value', + addField: 'Add field', + }, tags: { tagName: 'Tag name', requiresTag: 'Members must tag all expenses', @@ -6953,6 +6977,12 @@ const translations = { onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Report fields are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, }, + invoiceFields: { + title: 'Invoice fields', + description: `Invoice fields let you include extra invoice-level details on invoices.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Invoice fields are only available on the Control plan, starting at ${formattedPrice} ${hasTeam2025Pricing ? `per member per month.` : `per active member per month.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Enjoy automated syncing and reduce manual entries with the Expensify + NetSuite integration. Gain in-depth, realtime financial insights with native and custom segment support, including project and customer mapping.`, diff --git a/src/languages/es.ts b/src/languages/es.ts index acb17f61c243..ced878030187 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -4223,6 +4223,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', @@ -5865,6 +5866,29 @@ ${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.', + delete: 'Eliminar campo de factura', + deleteConfirmation: '¿Seguro que deseas eliminar este campo de factura?', + findInvoiceField: 'Buscar campo de factura', + nameInputSubtitle: 'Elige un nombre para el campo de factura.', + typeInputSubtitle: 'Elige qué tipo de campo de factura usar.', + initialValueInputSubtitle: 'Ingresa un valor inicial para mostrar en el campo de factura.', + listValuesInputSubtitle: 'Estos valores aparecerán en el menú desplegable del campo de factura. Los miembros pueden seleccionar los valores activados.', + listInputSubtitle: 'Estos valores aparecerán en la lista del campo de factura. Los miembros pueden seleccionar los valores activados.', + emptyInvoiceFieldsValues: { + title: 'Aún no hay valores de lista', + subtitle: 'Agrega valores personalizados para que aparezcan en las facturas.', + }, + existingInvoiceFieldNameError: 'Ya existe un campo de factura con este nombre', + invoiceFieldNameRequiredError: 'Ingresa un nombre para el campo de factura', + invoiceFieldTypeRequiredError: 'Elige un tipo de campo de factura', + invoiceFieldInitialValueRequiredError: 'Elige un valor inicial para el campo de factura', + addField: 'Añadir campo', + }, tags: { tagName: 'Nombre de etiqueta', requiresTag: 'Los miembros deben etiquetar todos los gastos', @@ -6761,6 +6785,12 @@ ${amount} para ${merchant} - ${date}`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => `Los campos de informe sólo están disponibles en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, }, + invoiceFields: { + title: 'Campos de factura', + description: `Los campos de factura te permiten incluir detalles adicionales a nivel de factura.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}) => + `Los campos de factura sólo están disponibles en el plan Controlar, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por miembro al mes.` : `por miembro activo al mes.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Disfruta de la sincronización automática y reduce las entradas manuales con la integración Expensify + NetSuite. Obtén información financiera en profundidad y en tiempo real con la compatibilidad nativa y personalizada con segmentos, incluida la asignación de proyectos y clientes.`, diff --git a/src/languages/fr.ts b/src/languages/fr.ts index adf3e191fc75..5127c3e4f90c 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -4354,6 +4354,7 @@ ${amount} pour ${merchant} - ${date}`, customFieldHint: 'Ajoutez un codage personnalisé qui s’applique à toutes les dépenses de ce membre.', reports: 'Notes de frais', reportFields: 'Champs de note de frais', + invoiceFields: 'Champs de facture', reportTitle: 'Titre de la note de frais', reportField: 'Champ de note de frais', taxes: 'Taxes', @@ -6052,6 +6053,29 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. reportFieldInitialValueRequiredError: 'Veuillez choisir une valeur initiale pour le champ de note de frais', genericFailureMessage: 'Une erreur s’est produite lors de la mise à jour du champ de note de frais. Veuillez réessayer.', }, + invoiceFields: { + subtitle: 'Les champs de facture peuvent être utiles lorsque vous souhaitez inclure des informations supplémentaires.', + importedFromAccountingSoftware: 'Les champs de facture ci-dessous sont importés depuis votre', + disableInvoiceFields: 'Désactiver les champs de facture', + disableInvoiceFieldsConfirmation: 'Êtes-vous sûr ? Les champs de facture seront désactivés sur les factures.', + delete: 'Supprimer le champ de facture', + deleteConfirmation: 'Voulez-vous vraiment supprimer ce champ de facture ?', + findInvoiceField: 'Trouver un champ de facture', + nameInputSubtitle: 'Choisissez un nom pour le champ de facture.', + typeInputSubtitle: 'Choisissez le type de champ de facture à utiliser.', + initialValueInputSubtitle: 'Saisissez une valeur de départ à afficher dans le champ de facture.', + listValuesInputSubtitle: 'Ces valeurs apparaîtront dans la liste déroulante du champ de facture. Les valeurs activées peuvent être sélectionnées par les membres.', + listInputSubtitle: 'Ces valeurs apparaîtront dans la liste du champ de facture. Les valeurs activées peuvent être sélectionnées par les membres.', + emptyInvoiceFieldsValues: { + title: 'Aucune valeur de liste pour le moment', + subtitle: 'Ajoutez des valeurs personnalisées à afficher sur les factures.', + }, + existingInvoiceFieldNameError: 'Un champ de facture portant ce nom existe déjà', + invoiceFieldNameRequiredError: 'Veuillez saisir un nom de champ de facture', + invoiceFieldTypeRequiredError: 'Veuillez choisir un type de champ de facture', + invoiceFieldInitialValueRequiredError: 'Veuillez choisir une valeur initiale de champ de facture', + addField: 'Ajouter un champ', + }, tags: { tagName: 'Nom du tag', requiresTag: 'Les membres doivent taguer toutes les dépenses', @@ -6857,6 +6881,12 @@ Voulez-vous vraiment les exporter à nouveau ?`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Les champs de note de frais sont uniquement disponibles avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, }, + invoiceFields: { + title: 'Champs de facture', + description: `Les champs de facture vous permettent d'inclure des détails supplémentaires au niveau de la facture.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Les champs de facture sont uniquement disponibles avec l’offre Control, à partir de ${formattedPrice} ${hasTeam2025Pricing ? `par membre et par mois.` : `par membre actif et par mois.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Profitez de la synchronisation automatisée et réduisez les saisies manuelles grâce à l’intégration Expensify + NetSuite. Obtenez des informations financières détaillées et en temps réel avec la prise en charge des segments natifs et personnalisés, y compris la correspondance des projets et des clients.`, diff --git a/src/languages/it.ts b/src/languages/it.ts index 4250b4e06139..9ca1e4b5b405 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -4328,6 +4328,7 @@ ${amount} per ${merchant} - ${date}`, customFieldHint: 'Aggiungi una codifica personalizzata che si applichi a tutte le spese di questo membro.', reports: 'Report', reportFields: 'Campi del report', + invoiceFields: 'Campi fattura', reportTitle: 'Titolo del report', reportField: 'Campo report', taxes: 'Tasse', @@ -6016,6 +6017,29 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. reportFieldInitialValueRequiredError: 'Scegli un valore iniziale per il campo del resoconto', genericFailureMessage: 'Si è verificato un errore durante l’aggiornamento del campo del report. Riprova.', }, + invoiceFields: { + subtitle: 'I campi della fattura possono essere utili quando vuoi includere informazioni aggiuntive.', + importedFromAccountingSoftware: 'I campi della fattura riportati di seguito sono importati dal tuo', + disableInvoiceFields: 'Disattiva i campi della fattura', + disableInvoiceFieldsConfirmation: 'Sei sicuro? I campi della fattura verranno disattivati nelle fatture.', + delete: 'Elimina campo della fattura', + deleteConfirmation: 'Sei sicuro di voler eliminare questo campo della fattura?', + findInvoiceField: 'Trova campo della fattura', + nameInputSubtitle: 'Scegli un nome per il campo della fattura.', + typeInputSubtitle: 'Scegli il tipo di campo della fattura da utilizzare.', + initialValueInputSubtitle: 'Inserisci un valore iniziale da mostrare nel campo della fattura.', + listValuesInputSubtitle: 'Questi valori appariranno nel menu a discesa del campo della fattura. I valori abilitati possono essere selezionati dai membri.', + listInputSubtitle: 'Questi valori appariranno nell’elenco del campo della fattura. I valori abilitati possono essere selezionati dai membri.', + emptyInvoiceFieldsValues: { + title: 'Nessun valore elenco ancora', + subtitle: 'Aggiungi valori personalizzati da mostrare sulle fatture.', + }, + existingInvoiceFieldNameError: 'Esiste già un campo della fattura con questo nome', + invoiceFieldNameRequiredError: 'Inserisci un nome per il campo della fattura', + invoiceFieldTypeRequiredError: 'Scegli un tipo di campo della fattura', + invoiceFieldInitialValueRequiredError: 'Scegli un valore iniziale per il campo della fattura', + addField: 'Aggiungi campo', + }, tags: { tagName: 'Nome tag', requiresTag: 'I membri devono etichettare tutte le spese', @@ -6814,6 +6838,12 @@ Vuoi davvero esportarli di nuovo?`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `I campi del report sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`}`, }, + invoiceFields: { + title: 'Campi fattura', + description: `I campi fattura ti consentono di includere dettagli aggiuntivi a livello di fattura.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `I campi fattura sono disponibili solo con il piano Control, a partire da ${formattedPrice} ${hasTeam2025Pricing ? `per utente al mese.` : `per membro attivo al mese.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Approfitta della sincronizzazione automatica e riduci le registrazioni manuali con l’integrazione Expensify + NetSuite. Ottieni approfondimenti finanziari dettagliati e in tempo reale grazie al supporto di segmenti nativi e personalizzati, inclusa la mappatura di progetti e clienti.`, diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 04dad47c7647..5f9f37bf0ca5 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -4299,6 +4299,7 @@ ${integrationName === CONST.ONBOARDING_ACCOUNTING_MAPPING.other ? 'あなたの' customFieldHint: 'このメンバーのすべての支出に適用されるカスタムコードを追加します。', reports: 'レポート', reportFields: 'レポート項目', + invoiceFields: '請求書項目', reportTitle: 'レポートタイトル', reportField: 'レポート項目', taxes: '税金', @@ -5947,6 +5948,29 @@ _詳しい手順については、[ヘルプサイトをご覧ください](${CO reportFieldInitialValueRequiredError: 'レポート項目の初期値を選択してください', genericFailureMessage: 'レポートフィールドの更新中にエラーが発生しました。もう一度お試しください。', }, + invoiceFields: { + subtitle: '追加情報を含めたい場合、請求書フィールドが役立ちます。', + importedFromAccountingSoftware: '以下の請求書フィールドは、次からインポートされます', + disableInvoiceFields: '請求書フィールドを無効にする', + disableInvoiceFieldsConfirmation: 'よろしいですか?請求書フィールドは請求書で無効になります。', + delete: '請求書フィールドを削除', + deleteConfirmation: 'この請求書フィールドを削除してもよろしいですか?', + findInvoiceField: '請求書フィールドを検索', + nameInputSubtitle: '請求書フィールドの名前を選択してください。', + typeInputSubtitle: '使用する請求書フィールドの種類を選択してください。', + initialValueInputSubtitle: '請求書フィールドに表示する開始値を入力してください。', + listValuesInputSubtitle: 'これらの値は請求書フィールドのドロップダウンに表示されます。有効な値はメンバーが選択できます。', + listInputSubtitle: 'これらの値は請求書フィールドのリストに表示されます。有効な値はメンバーが選択できます。', + emptyInvoiceFieldsValues: { + title: 'リスト値はまだありません', + subtitle: '請求書に表示するカスタム値を追加します。', + }, + existingInvoiceFieldNameError: 'この名前の請求書フィールドは既に存在します', + invoiceFieldNameRequiredError: '請求書フィールド名を入力してください', + invoiceFieldTypeRequiredError: '請求書フィールドの種類を選択してください', + invoiceFieldInitialValueRequiredError: '請求書フィールドの初期値を選択してください', + addField: 'フィールドを追加', + }, tags: { tagName: 'タグ名', requiresTag: 'メンバーはすべての経費にタグを付ける必要があります', @@ -6742,6 +6766,12 @@ ${reportName} onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `レポートフィールドは、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からのControlプランでのみ利用できます`, }, + invoiceFields: { + title: '請求書項目', + description: `請求書フィールドを使うと、請求書レベルの追加情報を請求書に含めることができます。`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `請求書フィールドは、${formattedPrice} ${hasTeam2025Pricing ? `メンバー1人あたり月額` : `アクティブメンバー1人あたり月額`}からのControlプランでのみ利用できます`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Expensify と NetSuite の連携により、自動同期を活用して手入力を減らしましょう。プロジェクトや顧客のマッピングを含むネイティブおよびカスタムセグメントのサポートで、詳細かつリアルタイムな財務インサイトを得られます。`, diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 55aec52a4f20..19fb3e4f7e95 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -4323,6 +4323,7 @@ ${amount} voor ${merchant} - ${date}`, customFieldHint: 'Voeg aangepaste codering toe die van toepassing is op alle uitgaven van dit lid.', reports: 'Rapporten', reportFields: 'Rapportvelden', + invoiceFields: 'Factuurvelden', reportTitle: 'Rapporttitel', reportField: 'Rapportveld', taxes: 'Belastingen', @@ -5994,6 +5995,29 @@ _Voor meer gedetailleerde instructies, [bezoek onze help-site](${CONST.NETSUITE_ reportFieldInitialValueRequiredError: 'Kies een beginwaarde voor een rapportveld', genericFailureMessage: 'Er is een fout opgetreden bij het bijwerken van het rapportveld. Probeer het opnieuw.', }, + invoiceFields: { + subtitle: 'Factuurvelden kunnen handig zijn als je extra informatie wilt toevoegen.', + importedFromAccountingSoftware: 'De onderstaande factuurvelden zijn geïmporteerd uit je', + disableInvoiceFields: 'Factuurvelden uitschakelen', + disableInvoiceFieldsConfirmation: 'Weet je het zeker? Factuurvelden worden uitgeschakeld op facturen.', + delete: 'Factuurveld verwijderen', + deleteConfirmation: 'Weet je zeker dat je dit factuurveld wilt verwijderen?', + findInvoiceField: 'Factuurveld zoeken', + nameInputSubtitle: 'Kies een naam voor het factuurveld.', + typeInputSubtitle: 'Kies welk type factuurveld je wilt gebruiken.', + initialValueInputSubtitle: 'Voer een beginwaarde in om in het factuurveld weer te geven.', + listValuesInputSubtitle: 'Deze waarden verschijnen in de vervolgkeuzelijst van je factuurveld. Ingeschakelde waarden kunnen door leden worden geselecteerd.', + listInputSubtitle: 'Deze waarden verschijnen in je factuurveldenlijst. Ingeschakelde waarden kunnen door leden worden geselecteerd.', + emptyInvoiceFieldsValues: { + title: 'Nog geen lijstwaarden', + subtitle: 'Voeg aangepaste waarden toe om op facturen te tonen.', + }, + existingInvoiceFieldNameError: 'Er bestaat al een factuurveld met deze naam', + invoiceFieldNameRequiredError: 'Voer een naam voor een factuurveld in', + invoiceFieldTypeRequiredError: 'Kies een veldtype voor de factuur', + invoiceFieldInitialValueRequiredError: 'Kies een beginwaarde voor een factuurveld', + addField: 'Veld toevoegen', + }, tags: { tagName: 'Tagnaam', requiresTag: 'Leden moeten alle uitgaven taggen', @@ -6793,6 +6817,12 @@ Weet je zeker dat je ze opnieuw wilt exporteren?`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Rapportvelden zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, }, + invoiceFields: { + title: 'Factuurvelden', + description: `Met factuurvelden kun je extra gegevens op factuurniveau toevoegen aan facturen.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Factuurvelden zijn alleen beschikbaar in het Control-abonnement, vanaf ${formattedPrice} ${hasTeam2025Pricing ? `per lid per maand.` : `per actieve deelnemer per maand.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Profiteer van automatische synchronisatie en verminder handmatige invoer met de Expensify + NetSuite-integratie. Krijg diepgaande realtime financiële inzichten met ondersteuning voor native en aangepaste segmenten, inclusief project- en klanttoewijzing.`, diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 110b2280f27b..fb1ee1de7dd3 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -4315,6 +4315,7 @@ ${amount} dla ${merchant} - ${date}`, customFieldHint: 'Dodaj niestandardowe kodowanie, które będzie stosowane do wszystkich wydatków tego członka.', reports: 'Raporty', reportFields: 'Pola raportu', + invoiceFields: 'Pola faktury', reportTitle: 'Tytuł raportu', reportField: 'Pole raportu', taxes: 'Podatki', @@ -5988,6 +5989,29 @@ _Aby uzyskać bardziej szczegółowe instrukcje, [odwiedź naszą stronę pomocy reportFieldInitialValueRequiredError: 'Wybierz początkową wartość pola raportu', genericFailureMessage: 'Wystąpił błąd podczas aktualizowania pola raportu. Spróbuj ponownie.', }, + invoiceFields: { + subtitle: 'Pola faktury mogą być pomocne, gdy chcesz dodać dodatkowe informacje.', + importedFromAccountingSoftware: 'Poniższe pola faktury są importowane z Twojego', + disableInvoiceFields: 'Wyłącz pola faktury', + disableInvoiceFieldsConfirmation: 'Czy na pewno? Pola faktury zostaną wyłączone na fakturach.', + delete: 'Usuń pole faktury', + deleteConfirmation: 'Czy na pewno chcesz usunąć to pole faktury?', + findInvoiceField: 'Znajdź pole faktury', + nameInputSubtitle: 'Wybierz nazwę pola faktury.', + typeInputSubtitle: 'Wybierz typ pola faktury, którego chcesz użyć.', + initialValueInputSubtitle: 'Wprowadź wartość początkową, która ma być wyświetlana w polu faktury.', + listValuesInputSubtitle: 'Te wartości pojawią się na liście rozwijanej pola faktury. Członkowie mogą wybierać włączone wartości.', + listInputSubtitle: 'Te wartości pojawią się na liście pola faktury. Członkowie mogą wybierać włączone wartości.', + emptyInvoiceFieldsValues: { + title: 'Brak wartości listy', + subtitle: 'Dodaj niestandardowe wartości, które mają pojawiać się na fakturach.', + }, + existingInvoiceFieldNameError: 'Pole faktury o tej nazwie już istnieje', + invoiceFieldNameRequiredError: 'Wprowadź nazwę pola faktury', + invoiceFieldTypeRequiredError: 'Wybierz typ pola faktury', + invoiceFieldInitialValueRequiredError: 'Wybierz początkową wartość pola faktury', + addField: 'Dodaj pole', + }, tags: { tagName: 'Nazwa tagu', requiresTag: 'Członkowie muszą otagować wszystkie wydatki', @@ -6786,6 +6810,12 @@ Czy na pewno chcesz wyeksportować je ponownie?`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Pola raportu są dostępne tylko w planie Control, od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`}`, }, + invoiceFields: { + title: 'Pola faktury', + description: `Pola faktury pozwalają dodać dodatkowe szczegóły na poziomie faktury.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Pola faktury są dostępne tylko w planie Control, od ${formattedPrice} ${hasTeam2025Pricing ? `za użytkownika miesięcznie.` : `na aktywnego członka miesięcznie.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Korzystaj z automatycznej synchronizacji i ograniczaj ręczne wprowadzanie danych dzięki integracji Expensify + NetSuite. Uzyskaj dogłębny, aktualny w czasie rzeczywistym wgląd w finanse dzięki obsłudze natywnych i niestandardowych segmentów, w tym mapowaniu projektów i klientów.`, diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index db6c475d8f3f..fea5b2ac04a4 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -4318,6 +4318,7 @@ ${amount} para ${merchant} - ${date}`, reportFields: 'Campos do relatório', reportTitle: 'Título do relatório', reportField: 'Campo de relatório', + invoiceFields: 'Campos de fatura', taxes: 'Impostos', bills: 'Contas', invoices: 'Faturas', @@ -5993,6 +5994,29 @@ _Para instruções mais detalhadas, [visite nossa central de ajuda](${CONST.NETS reportFieldInitialValueRequiredError: 'Escolha um valor inicial para o campo de relatório', genericFailureMessage: 'Ocorreu um erro ao atualizar o campo do relatório. Tente novamente.', }, + invoiceFields: { + subtitle: 'Os campos da fatura podem ser úteis quando você quiser incluir informações extras.', + importedFromAccountingSoftware: 'Os campos da fatura abaixo são importados do seu', + disableInvoiceFields: 'Desativar campos da fatura', + disableInvoiceFieldsConfirmation: 'Tem certeza? Os campos da fatura serão desativados nas faturas.', + delete: 'Excluir campo da fatura', + deleteConfirmation: 'Tem certeza de que deseja excluir este campo da fatura?', + findInvoiceField: 'Encontrar campo da fatura', + nameInputSubtitle: 'Escolha um nome para o campo da fatura.', + typeInputSubtitle: 'Escolha o tipo de campo da fatura que deseja usar.', + initialValueInputSubtitle: 'Insira um valor inicial para mostrar no campo da fatura.', + listValuesInputSubtitle: 'Esses valores aparecerão no menu suspenso do campo da fatura. Valores ativados podem ser selecionados pelos membros.', + listInputSubtitle: 'Esses valores aparecerão na lista do campo da fatura. Valores ativados podem ser selecionados pelos membros.', + emptyInvoiceFieldsValues: { + title: 'Ainda não há valores de lista', + subtitle: 'Adicione valores personalizados para aparecerem nas faturas.', + }, + existingInvoiceFieldNameError: 'Já existe um campo da fatura com este nome', + invoiceFieldNameRequiredError: 'Insira um nome para o campo da fatura', + invoiceFieldTypeRequiredError: 'Escolha um tipo de campo da fatura', + invoiceFieldInitialValueRequiredError: 'Escolha um valor inicial para o campo da fatura', + addField: 'Adicionar campo', + }, tags: { tagName: 'Nome da tag', requiresTag: 'Membros devem marcar todas as despesas', @@ -6791,6 +6815,12 @@ Tem certeza de que deseja exportá-los novamente?`, onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `Os campos de relatório estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, }, + invoiceFields: { + title: 'Campos de fatura', + description: `Os campos da fatura permitem incluir detalhes extras no nível da fatura.`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `Os campos da fatura estão disponíveis apenas no plano Control, a partir de ${formattedPrice} ${hasTeam2025Pricing ? `por membro por mês.` : `por membro ativo por mês.`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `Aproveite a sincronização automática e reduza lançamentos manuais com a integração Expensify + NetSuite. Obtenha insights financeiros profundos e em tempo real com suporte a segmentos nativos e personalizados, incluindo mapeamento de projetos e clientes.`, diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 46883a97c6ce..81b1019d361b 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -4223,6 +4223,7 @@ ${amount},商户:${merchant} - 日期:${date}`, customFieldHint: '为该成员的所有支出添加适用的自定义编码。', reports: '报表', reportFields: '报表字段', + invoiceFields: '发票字段', reportTitle: '报表标题', reportField: '报表字段', taxes: '税费', @@ -5838,6 +5839,29 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM reportFieldInitialValueRequiredError: '请选择报表字段的初始值', genericFailureMessage: '更新报表字段时出错。请重试。', }, + invoiceFields: { + subtitle: '当你想添加更多信息时,发票字段会很有帮助。', + importedFromAccountingSoftware: '以下发票字段是从你的', + disableInvoiceFields: '禁用发票字段', + disableInvoiceFieldsConfirmation: '确定吗?发票字段将在发票中被禁用。', + delete: '删除发票字段', + deleteConfirmation: '确定要删除此发票字段吗?', + findInvoiceField: '查找发票字段', + nameInputSubtitle: '为发票字段选择一个名称。', + typeInputSubtitle: '选择要使用的发票字段类型。', + initialValueInputSubtitle: '输入要在发票字段中显示的起始值。', + listValuesInputSubtitle: '这些值将显示在你的发票字段下拉菜单中。成员可以选择已启用的值。', + listInputSubtitle: '这些值将显示在你的发票字段列表中。成员可以选择已启用的值。', + emptyInvoiceFieldsValues: { + title: '还没有列表值', + subtitle: '添加要显示在发票上的自定义值。', + }, + existingInvoiceFieldNameError: '已存在同名发票字段', + invoiceFieldNameRequiredError: '请输入发票字段名称', + invoiceFieldTypeRequiredError: '请选择发票字段类型', + invoiceFieldInitialValueRequiredError: '请选择发票字段的初始值', + addField: '添加字段', + }, tags: { tagName: '标签名称', requiresTag: '成员必须为所有报销添加标签', @@ -6618,6 +6642,12 @@ ${reportName} onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => `报表字段仅在 Control 方案中提供,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, }, + invoiceFields: { + title: '发票字段', + description: `发票字段可用于在发票上包含额外的发票级别详情。`, + onlyAvailableOnPlan: ({formattedPrice, hasTeam2025Pricing}: {formattedPrice: string; hasTeam2025Pricing: boolean}) => + `发票字段仅在 Control 方案中提供,起价为 ${formattedPrice} ${hasTeam2025Pricing ? `每位成员每月。` : `每位活跃成员每月。`}`, + }, [CONST.POLICY.CONNECTIONS.NAME.NETSUITE]: { title: 'NetSuite', description: `通过 Expensify + NetSuite 集成实现自动同步,减少手动录入。借助对原生和自定义维度的支持(包括项目和客户映射),获取深入的实时财务洞察。`, diff --git a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts new file mode 100644 index 000000000000..fa22805f4e71 --- /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 000000000000..5cce12e49273 --- /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 000000000000..5b3c37fb1e07 --- /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/EnablePolicyInvoiceFieldsParams.ts b/src/libs/API/parameters/EnablePolicyInvoiceFieldsParams.ts new file mode 100644 index 000000000000..5afc5c912d90 --- /dev/null +++ b/src/libs/API/parameters/EnablePolicyInvoiceFieldsParams.ts @@ -0,0 +1,6 @@ +type EnablePolicyInvoiceFieldsParams = { + policyID: string; + enabled: boolean; +}; + +export default EnablePolicyInvoiceFieldsParams; diff --git a/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts new file mode 100644 index 000000000000..a52405efad54 --- /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 000000000000..c793b4bdf10a --- /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 000000000000..eeb3cd5106aa --- /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 d56b78063826..c62dad8ad4da 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -259,6 +259,7 @@ export type {default as SetPolicyTravelSettingsParams} from './SetPolicyTravelSe 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 UpdateGustoApprovalModeParams} from './UpdateGustoApprovalModeParams'; @@ -337,15 +338,21 @@ export type {default as DowngradeToTeamParams} from './DowngradeToTeamParams'; export type {default as RejectMoneyRequestInBulkParams} from './RejectMoneyRequestInBulkParams'; export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyRequestOnSearchParams'; 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 EnablePolicyTimeTrackingParams} from './EnablePolicyTimeTrackingParams'; 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 7fd1e712e9f7..e95b14aba185 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -193,6 +193,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', @@ -270,6 +271,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', @@ -409,9 +411,14 @@ const WRITE_COMMANDS = { OPEN_SIDE_PANEL: 'OpenSidePanel', 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', @@ -815,6 +822,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; @@ -911,6 +919,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; @@ -1123,10 +1132,15 @@ type WriteCommandParameters = { // Workspace report field parameters [WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_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>; @@ -1357,6 +1371,7 @@ const READ_COMMANDS = { OPEN_POLICY_TAGS_PAGE: 'OpenPolicyTagsPage', OPEN_POLICY_TAXES_PAGE: 'OpenPolicyTaxesPage', OPEN_POLICY_REPORT_FIELDS_PAGE: 'OpenPolicyReportFieldsPage', + OPEN_POLICY_INVOICES_PAGE: 'OpenPolicyInvoicesPage', OPEN_POLICY_RULES_PAGE: 'OpenPolicyRulesPage', OPEN_POLICY_EXPENSIFY_CARDS_PAGE: 'OpenPolicyExpensifyCardsPage', OPEN_POLICY_TRAVEL_PAGE: 'OpenPolicyTravelPage', @@ -1461,6 +1476,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_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 d52b7d5060df..8c3c82978dfe 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -1000,6 +1000,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.CERTINIA_IMPORT]: () => require('../../../../pages/workspace/accounting/certinia/import/CertiniaImportPage').default, [SCREENS.WORKSPACE.ACCOUNTING.CERTINIA_DIMENSION_MAPPING]: () => require('../../../../pages/workspace/accounting/certinia/import/CertiniaDimensionMappingPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index a1d83a6686b7..d826e3285910 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -297,7 +297,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, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index e317bd7dca93..fd857510c60d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -644,31 +644,62 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { policyID: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_CREATE]: { + policyID: string; + }; + [SCREENS.WORKSPACE.DYNAMIC_REPORT_FIELDS_INITIAL_LIST_VALUE]: { + 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/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index d32403f4eec8..d4180a7d0fdd 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -4787,6 +4787,7 @@ export { getExportIntegrationMessageHTML, getUpdateRoomDescriptionMessage, getRoomAvatarUpdatedMessage, + getMessageOfOldDotReportAction, didMessageMentionCurrentUser, getForwardedReportActionMessage, getPolicyChangeLogAddEmployeeMessage, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e04eb841bee3..0796ce5218e4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -170,6 +170,7 @@ import { getLastVisibleAction as getLastVisibleActionReportActionsUtils, getLastVisibleMessage as getLastVisibleMessageActionUtils, getLastVisibleMessage as getLastVisibleMessageReportActionsUtils, + getMessageOfOldDotReportAction, getMostRecentActiveDEWApproveFailedAction, getMostRecentActiveDEWSubmitFailedAction, getNumberOfMoneyRequests, @@ -4463,9 +4464,6 @@ function isHoldCreator(transaction: OnyxEntry, reportID: string | u * 2. Report is settled, closed, approved, or submitted and already forwarded for review */ function isReportFieldDisabled(report: OnyxEntry, reportField: OnyxEntry, policy: OnyxEntry): boolean { - if (isInvoiceReport(report)) { - return true; - } const isReportSettled = isSettled(report?.reportID); const isReportClosed = isClosedReport(report); const isTitleField = isReportFieldOfTypeTitle(reportField); @@ -4584,7 +4582,7 @@ function getAvailableReportFields(report: OnyxEntry, policyReportFields: const mergedFieldIds = Array.from(new Set([...policyReportFields.map(({fieldID}) => fieldID), ...reportFields.map(({fieldID}) => fieldID)])); const fields = mergedFieldIds.map((id) => { - const field = report?.fieldList?.[getReportFieldKey(id)]; + const field = report?.fieldList?.[getReportFieldKey(id)] ?? report?.fieldList?.[id]; const policyReportField = policyReportFields.find(({fieldID}) => fieldID === id); if (field) { @@ -5852,6 +5850,13 @@ function getParentNavigationSubtitle( return {}; } + const parentReportAction = getReportAction(report?.parentReportID, report?.parentReportActionID); + if (parentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CHANGE_FIELD) { + return { + reportName: getMessageOfOldDotReportAction(translateLocal, parentReportAction, false), + }; + } + if (isInvoiceReport(report) || isInvoiceRoom(parentReport)) { const senderWorkspaceName = getPolicyName({report: parentReport, policy}); const invoiceReceiverPolicyID = getInvoiceReceiverPolicyID(parentReport); @@ -8953,7 +8958,8 @@ function hasViolations( } function hasVisibleReportFieldViolations(report: OnyxEntry, policy: OnyxEntry, currentUserAccountID: number | undefined): boolean { - if (!report || !policy?.fieldList || !policy?.areReportFieldsEnabled) { + const areFieldsEnabledForReport = isInvoiceReport(report) ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled; + if (!report || !policy?.fieldList || !areFieldsEnabledForReport) { return false; } @@ -8975,7 +8981,8 @@ function hasVisibleReportFieldViolations(report: OnyxEntry, policy: Onyx const {fieldsByName} = getReportFieldMaps(report, policy.fieldList); return Object.values(fieldsByName).some((field) => { - if (field.target !== report.type) { + const isFieldTargetMatchingReport = field.target === report.type || (report.type === CONST.REPORT.TYPE.EXPENSE && !field.target); + if (!isFieldTargetMatchingReport) { return false; } if (shouldHideSingleReportField(field)) { @@ -12891,19 +12898,39 @@ function shouldHideSingleReportField(reportField: PolicyReportField) { return isReportFieldOfTypeTitle(reportField) || !hasEnableOption; } +function isReportNameValuePairField(value: unknown): value is PolicyReportField { + return typeof value === 'object' && value !== null && 'fieldID' in value && typeof value.fieldID === 'string' && 'name' in value && typeof value.name === 'string'; +} + +function getReportFieldFromReportNameValuePairs(reportNameValuePairs: OnyxEntry, reportFieldIDOrKey: string): PolicyReportField | undefined { + for (const [key, value] of Object.entries(reportNameValuePairs ?? {})) { + if (key === reportFieldIDOrKey && isReportNameValuePairField(value)) { + return value; + } + } + + return undefined; +} + /** * Get both field values map and fields-by-name map in a single pass */ function getReportFieldMaps(report: OnyxEntry, fieldList: Record): {fieldValues: Record; fieldsByName: Record} { const fields = getAvailableReportFields(report, Object.values(fieldList ?? {})); + const reportNameValuePairs = allReportNameValuePair?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report?.reportID}`]; const fieldValues: Record = {}; const fieldsByName: Record = {}; for (const field of fields) { if (field.name) { + const fieldKey = getReportFieldKey(field.fieldID); + const shouldReadReportNameValuePair = report?.type === CONST.REPORT.TYPE.INVOICE && field.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + const reportNameValuePairField = shouldReadReportNameValuePair + ? (getReportFieldFromReportNameValuePairs(reportNameValuePairs, fieldKey) ?? getReportFieldFromReportNameValuePairs(reportNameValuePairs, field.fieldID)) + : undefined; const key = field.name.toLowerCase(); - fieldValues[key] = field.value ?? field.defaultValue ?? ''; - fieldsByName[key] = field; + fieldValues[key] = reportNameValuePairField?.value ?? field.value ?? field.defaultValue ?? ''; + fieldsByName[key] = reportNameValuePairField ? {...field, value: reportNameValuePairField.value} : field; } } @@ -13336,6 +13363,7 @@ export { buildOptimisticResolvedDuplicatesReportAction, getTitleReportField, getTitleFieldWithFallback, + getReportFieldFromReportNameValuePairs, getReportFieldsByPolicyID, getChatListItemReportName, buildOptimisticMovedTransactionAction, diff --git a/src/libs/WorkspaceReportFieldUtils.ts b/src/libs/WorkspaceReportFieldUtils.ts index a7d127e9d728..306bd7048a3c 100644 --- a/src/libs/WorkspaceReportFieldUtils.ts +++ b/src/libs/WorkspaceReportFieldUtils.ts @@ -1,3 +1,4 @@ +import type {ValueOf} from 'type-fest'; import type {FormInputErrors} from '@components/Form/types'; import type {LocalizedTranslate} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; @@ -63,8 +64,9 @@ function validateReportFieldListValueName( /** * Generates a field ID based on the field name. */ -function generateFieldID(name: string) { - return `field_id_${name.replaceAll(CONST.REGEX.ANY_SPACE, '_').toUpperCase()}`; +function generateFieldID(name: string, target?: ValueOf) { + const targetPrefix = target ? `${target.toUpperCase()}_` : ''; + return `field_id_${targetPrefix}${name.replaceAll(CONST.REGEX.ANY_SPACE, '_').toUpperCase()}`; } /** @@ -106,8 +108,33 @@ function hasFormulaPartsInInitialValue(initialValue?: string): boolean { /** * Checks if a report field name already exists in the policy's field list (case-insensitive). */ -function isReportFieldNameExisting(fieldList: Record | undefined, fieldName: string): boolean { - return Object.values(fieldList ?? {}).some((reportField) => reportField.name.toLowerCase() === fieldName.toLowerCase()); +function isReportFieldNameExisting(fieldList: Record | undefined, fieldName: string, expectedTarget?: ValueOf): boolean { + return Object.values(fieldList ?? {}).some((reportField) => { + if (!isReportFieldTargetValid(reportField, expectedTarget)) { + return false; + } + + return reportField.name.toLowerCase() === fieldName.toLowerCase(); + }); +} + +/** + * Determines whether a report field matches the expected target. + */ +function isReportFieldTargetValid(reportField: PolicyReportField | null, expectedTarget?: ValueOf): boolean { + if (!reportField) { + return false; + } + + if (expectedTarget === CONST.REPORT_FIELD_TARGETS.INVOICE) { + return reportField.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + } + + if (expectedTarget === CONST.REPORT_FIELD_TARGETS.EXPENSE) { + return !reportField.target || reportField.target === CONST.REPORT_FIELD_TARGETS.EXPENSE; + } + + return true; } /** @@ -218,4 +245,5 @@ export { getUnsupportedReportFieldFormulaParts, hasFormulaPartsInInitialValue, isReportFieldNameExisting, + isReportFieldTargetValid, }; diff --git a/src/libs/actions/Policy/CopyPolicySettings.ts b/src/libs/actions/Policy/CopyPolicySettings.ts index 00fbef58e206..3ea3ea9ebb74 100644 --- a/src/libs/actions/Policy/CopyPolicySettings.ts +++ b/src/libs/actions/Policy/CopyPolicySettings.ts @@ -55,7 +55,7 @@ const PARTS_TO_POLICY_FIELDS = { codingRules: ['rules'], distanceRates: ['areDistanceRatesEnabled', 'customUnits'], perDiem: ['arePerDiemRatesEnabled', 'customUnits'], - invoices: ['areInvoicesEnabled', 'invoice'], + invoices: ['areInvoicesEnabled', 'areInvoiceFieldsEnabled', 'invoice', 'fieldList'], travel: ['isTravelEnabled', 'travelSettings'], timeTracking: [], } as const satisfies Record>; @@ -132,11 +132,17 @@ function buildTimeTrackingPatch(sourcePolicy: Policy): Pick | u */ function buildPolicyFieldPatch(sourcePolicy: Policy, parts: Part[]): Partial { const patch: Partial = {}; + const shouldCopyReportFields = parts.includes('reports'); + const shouldCopyInvoiceFields = parts.includes('invoices'); + for (const part of parts) { for (const field of PARTS_TO_POLICY_FIELDS[part]) { if (field === 'customUnits') { continue; } + if (field === 'fieldList') { + continue; + } if (part === 'codingRules' && field === 'rules') { continue; } @@ -144,6 +150,16 @@ function buildPolicyFieldPatch(sourcePolicy: Policy, parts: Part[]): Partial)[field] = sourcePolicy[field as keyof Policy]; } } + + if (shouldCopyReportFields || shouldCopyInvoiceFields) { + patch.fieldList = Object.fromEntries( + Object.entries(sourcePolicy.fieldList ?? {}).filter(([, field]) => { + const isInvoiceField = field.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + return (shouldCopyReportFields && !isInvoiceField) || (shouldCopyInvoiceFields && isInvoiceField); + }), + ); + } + return patch; } diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index ad7bfe87022b..d0db18f3fc9b 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -29,6 +29,7 @@ import type { EnablePolicyConnectionsParams, EnablePolicyExpensifyCardsParams, EnablePolicyHRParams, + EnablePolicyInvoiceFieldsParams, EnablePolicyInvoicingParams, EnablePolicyReportFieldsParams, EnablePolicyTaxesParams, @@ -3303,6 +3304,16 @@ function buildOptimisticDuplicatePolicy( const willCopyRulesDocument = isOverviewFeatureSelected && !!sourcePolicy?.rulesDocumentURL; const employeeListWithoutPendingDelete = filterPendingDeleteData(sourcePolicy?.employeeList); const fieldListWithoutPendingDelete = filterPendingDeleteData(sourcePolicy?.fieldList); + const reportFieldListWithoutPendingDelete = fieldListWithoutPendingDelete + ? Object.fromEntries(Object.entries(fieldListWithoutPendingDelete).filter(([, field]) => field.target !== CONST.REPORT_FIELD_TARGETS.INVOICE)) + : undefined; + const invoiceFieldListWithoutPendingDelete = fieldListWithoutPendingDelete + ? Object.fromEntries(Object.entries(fieldListWithoutPendingDelete).filter(([, field]) => field.target === CONST.REPORT_FIELD_TARGETS.INVOICE)) + : undefined; + const copiedFieldList = { + ...(isReportsFeatureSelected ? reportFieldListWithoutPendingDelete : undefined), + ...(isInvoicesFeatureSelected ? invoiceFieldListWithoutPendingDelete : undefined), + }; const connectionsWithoutPendingDelete = filterPendingDeleteData(sourcePolicy?.connections); const taxRatesWithoutPendingDelete = { ...sourcePolicy?.taxRates, @@ -3318,6 +3329,7 @@ function buildOptimisticDuplicatePolicy( areRulesEnabled: isRulesFeatureSelected, areWorkflowsEnabled: isWorkflowsFeatureSelected, areReportFieldsEnabled: isReportsFeatureSelected ? sourcePolicy?.areReportFieldsEnabled : false, + areInvoiceFieldsEnabled: isInvoicesFeatureSelected ? sourcePolicy?.areInvoiceFieldsEnabled : false, areConnectionsEnabled: isConnectionsFeatureSelected, arePerDiemRatesEnabled: isPerDiemFeatureSelected, isTravelEnabled: isTravelFeatureSelected ? sourcePolicy?.isTravelEnabled : undefined, @@ -3327,7 +3339,7 @@ function buildOptimisticDuplicatePolicy( employeeList: isMemberFeatureSelected ? employeeListWithoutPendingDelete : {[sourcePolicy.owner]: sourcePolicy?.employeeList?.[sourcePolicy.owner]}, id: duplicatedPolicyID, name: duplicatedPolicyName, - fieldList: isReportsFeatureSelected ? fieldListWithoutPendingDelete : undefined, + fieldList: Object.keys(copiedFieldList).length > 0 ? copiedFieldList : undefined, connections: isConnectionsFeatureSelected ? connectionsWithoutPendingDelete : undefined, customUnits: getCustomUnitsForDuplication(sourcePolicy, isDistanceRatesFeatureSelected, isPerDiemFeatureSelected, { distanceCustomUnitID: duplicatedDistanceCustomUnitID, @@ -3347,6 +3359,7 @@ function buildOptimisticDuplicatePolicy( description: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, type: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, areReportFieldsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, + areInvoiceFieldsEnabled: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD, ...(willCopyRulesDocument ? {rulesDocumentURL: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD} : {}), }, avatarURL: duplicatedPolicyFile?.uri, @@ -4901,16 +4914,19 @@ 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, }, }, }, @@ -4921,7 +4937,7 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { pendingFields: { - areReportFieldsEnabled: null, + [enableFieldKey]: null, }, }, }, @@ -4931,18 +4947,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: true, currentTaxRates: TaxRatesWithDefault | undefined): void; @@ -7532,6 +7556,7 @@ export { enablePolicyHR, enablePolicyReceiptPartners, enablePolicyReportFields, + enablePolicyInvoiceFields, enablePolicyTaxes, enablePolicyWorkflows, enablePolicyTimeTracking, diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index b802d6a4a1d2..27d182329f0c 100644 --- a/src/libs/actions/Policy/ReportField.ts +++ b/src/libs/actions/Policy/ReportField.ts @@ -3,12 +3,18 @@ import type {OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import type { + CreateWorkspaceInvoiceFieldListValueParams, + CreateWorkspaceInvoiceFieldParams, CreateWorkspaceReportFieldListValueParams, CreateWorkspaceReportFieldParams, + DeletePolicyInvoiceField, DeletePolicyReportField, + EnableWorkspaceInvoiceFieldListValueParams, EnableWorkspaceReportFieldListValueParams, OpenPolicyReportFieldsPageParams, + RemoveWorkspaceInvoiceFieldListValueParams, RemoveWorkspaceReportFieldListValueParams, + UpdateWorkspaceInvoiceFieldInitialValueParams, UpdateWorkspaceReportFieldInitialValueParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; @@ -51,7 +57,8 @@ type DeleteReportFieldsListValueParams = { type CreateReportFieldParams = Pick & { listValues: string[]; disabledListValues: boolean[]; - policyExpenseReportIDs: Array | undefined; + policyReportIDs: Array | undefined; + isInvoiceField?: boolean; policy: OnyxEntry; }; @@ -98,6 +105,19 @@ function openPolicyReportFieldsPage(policyID: string) { API.read(READ_COMMANDS.OPEN_POLICY_REPORT_FIELDS_PAGE, params); } +function openPolicyInvoicesPage(policyID: string) { + if (!policyID) { + Log.warn('openPolicyInvoicesPage invalid params', {policyID}); + return; + } + + const params: OpenPolicyReportFieldsPageParams = { + policyID, + }; + + API.read(READ_COMMANDS.OPEN_POLICY_INVOICES_PAGE, params); +} + /** * Sets the initial form values for the workspace report fields form. */ @@ -165,14 +185,19 @@ 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, isInvoiceField = false, policy}: CreateReportFieldParams) { if (!policy) { Log.warn('Policy data is not present'); return; } const previousFieldList = policy?.fieldList ?? {}; - const fieldID = WorkspaceReportFieldUtils.generateFieldID(name); + const target = isInvoiceField ? CONST.REPORT_FIELD_TARGETS.INVOICE : CONST.REPORT_FIELD_TARGETS.EXPENSE; + const defaultFieldID = WorkspaceReportFieldUtils.generateFieldID(name); + const defaultFieldKey = ReportUtils.getReportFieldKey(defaultFieldID); + const defaultField = previousFieldList[defaultFieldKey]; + const shouldUseTargetSpecificFieldID = isInvoiceField || (defaultField && !WorkspaceReportFieldUtils.isReportFieldTargetValid(defaultField, target)); + const fieldID = shouldUseTargetSpecificFieldID ? WorkspaceReportFieldUtils.generateFieldID(name, target) : defaultFieldID; const fieldKey = ReportUtils.getReportFieldKey(fieldID); // User selected type Text but entered a formula Initial value, treat it as a Formula type for optimistic UI @@ -182,7 +207,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa const optimisticReportFieldDataForPolicy: Omit, 'value'> = { name, type: optimisticType, - target: 'expense', + target, defaultValue: initialValue, values: listValues, disabledOptions: disabledListValues, @@ -205,7 +230,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa errorFields: null, }, }, - ...(policyExpenseReportIDs ?? []).map( + ...(policyReportIDs ?? []).map( (reportID): OnyxUpdate => ({ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, onyxMethod: Onyx.METHOD.MERGE, @@ -231,7 +256,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa }, }, }, - ...(policyExpenseReportIDs ?? []).map( + ...(policyReportIDs ?? []).map( (reportID): OnyxUpdate => ({ key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, onyxMethod: Onyx.METHOD.MERGE, @@ -261,12 +286,18 @@ function createReportField({name, type, initialValue, listValues, disabledListVa failureData, }; - const parameters: CreateWorkspaceReportFieldParams = { - policyID: policy?.id, - reportFields: JSON.stringify([optimisticReportFieldDataForPolicy]), - }; - - API.write(WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD, parameters, onyxData); + 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); } function deleteReportFields({policy, reportFieldsToUpdate}: DeleteReportFieldsParams) { @@ -276,19 +307,24 @@ function deleteReportFields({policy, reportFieldsToUpdate}: DeleteReportFieldsPa } const allReportFields = policy?.fieldList ?? {}; + const resolvedReportFieldKeys = reportFieldsToUpdate.filter((reportFieldKey) => !!allReportFields[reportFieldKey]); + if (resolvedReportFieldKeys.length === 0) { + Log.warn('No valid report fields to delete', {reportFieldsToUpdate}); + return; + } - const updatedReportFields = Object.fromEntries(Object.entries(allReportFields).filter(([key]) => !reportFieldsToUpdate.includes(key))); - const optimisticReportFields = reportFieldsToUpdate.reduce>>>((acc, reportFieldKey) => { + const updatedReportFields = Object.fromEntries(Object.entries(allReportFields).filter(([key]) => !resolvedReportFieldKeys.includes(key))); + const optimisticReportFields = resolvedReportFieldKeys.reduce>>>((acc, reportFieldKey) => { acc[reportFieldKey] = {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}; return acc; }, {}); - const successReportFields = reportFieldsToUpdate.reduce>((acc, reportFieldKey) => { + const successReportFields = resolvedReportFieldKeys.reduce>((acc, reportFieldKey) => { acc[reportFieldKey] = null; return acc; }, {}); - const failureReportFields = reportFieldsToUpdate.reduce>>>((acc, reportFieldKey) => { + const failureReportFields = resolvedReportFieldKeys.reduce>>>((acc, reportFieldKey) => { acc[reportFieldKey] = {pendingAction: null}; return acc; }, {}); @@ -327,12 +363,24 @@ 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 = resolvedReportFieldKeys.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 updatedReportFieldsValues = Object.values(updatedReportFields); + const remainingInvoiceFields = updatedReportFieldsValues.filter((reportField) => reportField.target === CONST.REPORT_FIELD_TARGETS.INVOICE); + const remainingExpenseFields = updatedReportFieldsValues.filter((reportField) => !reportField.target || reportField.target === CONST.REPORT_FIELD_TARGETS.EXPENSE); + const invoiceFieldsPayload = remainingInvoiceFields; + const parameters: DeletePolicyReportField | DeletePolicyInvoiceField = isInvoiceField + ? { + policyID: policy?.id, + invoiceFields: JSON.stringify(invoiceFieldsPayload), + } + : { + policyID: policy?.id, + reportFields: JSON.stringify(remainingExpenseFields), + }; + const deleteCommand = isInvoiceField ? WRITE_COMMANDS.DELETE_POLICY_INVOICE_FIELD : WRITE_COMMANDS.DELETE_POLICY_REPORT_FIELD; + + API.write(deleteCommand, parameters, onyxData); } /** @@ -400,12 +448,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) { @@ -444,12 +499,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); } /** @@ -484,12 +546,19 @@ function addReportFieldListValue({policy, reportFieldID, valueName}: AddReportFi ], }; - const parameters: CreateWorkspaceReportFieldListValueParams = { - policyID: policy?.id, - reportFields: JSON.stringify([updatedReportField]), - }; - - API.write(WRITE_COMMANDS.CREATE_WORKSPACE_REPORT_FIELD_LIST_VALUE, parameters, onyxData); + 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(createCommand, parameters, onyxData); } /** @@ -532,12 +601,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}; @@ -553,6 +629,7 @@ export { updateReportFieldInitialValue, updateReportFieldListValueEnabled, openPolicyReportFieldsPage, + openPolicyInvoicesPage, addReportFieldListValue, removeReportFieldListValue, }; diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index c5bfa6d6c1e6..0d5628b7f993 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -3629,15 +3629,25 @@ function updateReportField({ isASAPSubmitBetaEnabled, }); + const isInvoiceField = report.type === CONST.REPORT.TYPE.INVOICE && reportField.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + const optimisticReportFieldValue = isInvoiceField ? null : reportField; + const failureReportFieldValue = isInvoiceField ? null : previousReportField; + const optimisticData: Array< - OnyxUpdate + OnyxUpdate< + | typeof ONYXKEYS.COLLECTION.REPORT + | typeof ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS + | typeof ONYXKEYS.COLLECTION.NEXT_STEP + | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS + | typeof ONYXKEYS.RECENTLY_USED_REPORT_FIELDS + > > = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [fieldKey]: reportField, + [fieldKey]: optimisticReportFieldValue, }, nextStep: optimisticNextStep, pendingFields: { @@ -3660,6 +3670,17 @@ function updateReportField({ }, ]; + if (isInvoiceField) { + optimisticData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, + value: { + [fieldKey]: reportField, + [reportField.fieldID]: reportField, + }, + }); + } + if (reportField.type === 'dropdown' && reportField.value) { optimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -3670,13 +3691,17 @@ function updateReportField({ }); } - const failureData: Array> = [ + const failureData: Array< + OnyxUpdate< + typeof ONYXKEYS.COLLECTION.REPORT | typeof ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS | typeof ONYXKEYS.COLLECTION.REPORT_ACTIONS | typeof ONYXKEYS.RECENTLY_USED_REPORT_FIELDS + > + > = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { fieldList: { - [fieldKey]: previousReportField, + [fieldKey]: failureReportFieldValue, }, nextStep: report.nextStep ?? null, pendingFields: { @@ -3699,6 +3724,17 @@ function updateReportField({ }, ]; + if (isInvoiceField) { + failureData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, + value: { + [fieldKey]: previousReportField, + [reportField.fieldID]: previousReportField, + }, + }); + } + if (reportField.type === 'dropdown') { failureData.push({ onyxMethod: Onyx.METHOD.MERGE, @@ -3709,7 +3745,7 @@ function updateReportField({ }); } - const successData: Array> = [ + const successData: Array> = [ { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, @@ -3734,6 +3770,21 @@ function updateReportField({ }, ]; + if (isInvoiceField) { + successData.push({ + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, + value: { + [fieldKey]: { + pendingAction: null, + }, + [reportField.fieldID]: { + pendingAction: null, + }, + }, + }); + } + const parameters = { reportID, reportFields: JSON.stringify({[fieldKey]: reportField}), diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index b554ccdf42ec..f5b052d99230 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -32,6 +32,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/DynamicEditReportFieldPage.tsx b/src/pages/DynamicEditReportFieldPage.tsx index 55d3522dd929..5937f2ec322d 100644 --- a/src/pages/DynamicEditReportFieldPage.tsx +++ b/src/pages/DynamicEditReportFieldPage.tsx @@ -23,6 +23,7 @@ import type {EditRequestNavigatorParamList} from '@libs/Navigation/types'; import {isPolicyFieldListEmpty} from '@libs/PolicyUtils'; import {getReportName as getReportNameFromReportNameUtils} from '@libs/ReportNameUtils'; import { + getReportFieldFromReportNameValuePairs, getReportFieldKey, getTitleFieldWithFallback, hasViolations as hasViolationsReportUtils, @@ -48,6 +49,7 @@ function DynamicEditReportFieldPage({route}: DynamicEditReportFieldPageProps) { const fieldKey = getReportFieldKey(route.params.fieldID); const backPath = useDynamicBackPath(DYNAMIC_ROUTES.EDIT_REPORT_FIELD.path); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`); + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`); const reportAttributesSelector = useCallback((attributes: OnyxEntry) => reportByIDsSelector([reportID])(attributes), [reportID]); const [reportAttributesByReportID] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, { @@ -56,8 +58,10 @@ function DynamicEditReportFieldPage({route}: DynamicEditReportFieldPageProps) { const [recentlyUsedReportFields] = useOnyx(ONYXKEYS.RECENTLY_USED_REPORT_FIELDS); const isTitleField = route.params.fieldID === CONST.REPORT_FIELD_TITLE_FIELD_ID; - let reportField = report?.fieldList?.[fieldKey] ?? policy?.fieldList?.[fieldKey]; - let policyField = policy?.fieldList?.[fieldKey] ?? reportField; + const reportFieldFromNVP = getReportFieldFromReportNameValuePairs(reportNameValuePairs, fieldKey) ?? getReportFieldFromReportNameValuePairs(reportNameValuePairs, route.params.fieldID); + let reportField = + reportFieldFromNVP ?? report?.fieldList?.[fieldKey] ?? report?.fieldList?.[route.params.fieldID] ?? policy?.fieldList?.[fieldKey] ?? policy?.fieldList?.[route.params.fieldID]; + let policyField = policy?.fieldList?.[fieldKey] ?? policy?.fieldList?.[route.params.fieldID] ?? reportField; // If the title field is missing, use fallback so that it can still be edited and matches the OldDot behavior. if (isTitleField && !reportField && !policyField) { @@ -77,7 +81,8 @@ function DynamicEditReportFieldPage({route}: DynamicEditReportFieldPageProps) { const {showConfirmModal} = useConfirmModal(); const icons = useMemoizedLazyExpensifyIcons(['Trashcan']); const isReportFieldTitle = isReportFieldOfTypeTitle(reportField); - const reportFieldsEnabled = ((isGroupPolicyExpenseReport(report) || isInvoiceReport(report)) && !!policy?.areReportFieldsEnabled) || isReportFieldTitle; + const isReportFieldsFeatureEnabled = report?.type === CONST.REPORT.TYPE.INVOICE ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled; + const reportFieldsEnabled = ((isGroupPolicyExpenseReport(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/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx index 408b9c7ec403..91951c47d4ba 100644 --- a/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx +++ b/src/pages/workspace/duplicate/WorkspaceDuplicateSelectFeaturesForm.tsx @@ -45,7 +45,10 @@ function WorkspaceDuplicateSelectFeaturesForm({policyID}: WorkspaceDuplicateForm const categoriesCount = Object.values(policyCategories ?? {}).filter((category) => category.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length; const codingRulesCount = Object.values(policy?.rules?.codingRules ?? {}).filter((rule) => rule.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length; const [selectedItems, setSelectedItems] = useState([]); - const reportFields = Object.values(getReportFieldsByPolicyID(policy) ?? {}).filter((field) => field.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length ?? 0; + const reportFields = + Object.values(getReportFieldsByPolicyID(policy) ?? {}).filter( + (field) => field.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && field.target !== CONST.REPORT_FIELD_TARGETS.INVOICE, + ).length ?? 0; const customUnits = getPerDiemCustomUnit(policy); const customUnitRates: Record = customUnits?.rates ?? {}; const allRates = Object.values(customUnitRates)?.filter((rate) => rate.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE).length ?? 0; diff --git a/src/pages/workspace/fields/CreateFieldsPage.tsx b/src/pages/workspace/fields/CreateFieldsPage.tsx new file mode 100644 index 000000000000..31e063ec3206 --- /dev/null +++ b/src/pages/workspace/fields/CreateFieldsPage.tsx @@ -0,0 +1,284 @@ +import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import {View} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +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 {getUnsupportedReportFieldFormulaParts, 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 type {Route as Routes} from '@src/ROUTES'; +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: Routes; + 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); + + const reportTypeForTarget = useMemo(() => (isInvoiceField ? CONST.REPORT.TYPE.INVOICE : CONST.REPORT.TYPE.EXPENSE), [isInvoiceField]); + const fieldTarget = useMemo(() => (isInvoiceField ? CONST.REPORT_FIELD_TARGETS.INVOICE : CONST.REPORT_FIELD_TARGETS.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, {selector: policyReportIDsSelector}); + + const availableListValuesLength = (formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? []).filter((disabledListValue) => !disabledListValue).length; + + const submitForm = useCallback( + (values: FormOnyxValues) => { + const shouldClearListInitialValue = values[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength === 0; + createReportField({ + policy, + name: values[INPUT_IDS.NAME], + type: values[INPUT_IDS.TYPE], + initialValue: shouldClearListInitialValue ? '' : values[INPUT_IDS.INITIAL_VALUE], + listValues: formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], + disabledListValues: formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? [], + policyReportIDs, + isInvoiceField, + }); + Navigation.goBack(); + }, + [availableListValuesLength, formDraft, isInvoiceField, policy, policyReportIDs], + ); + + const validateForm = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const {name, type, initialValue: formInitialValue} = values; + const errors: FormInputErrors = {}; + + if (!isRequiredFulfilled(name)) { + errors[INPUT_IDS.NAME] = translate(isInvoiceField ? 'workspace.invoiceFields.invoiceFieldNameRequiredError' : 'workspace.reportFields.reportFieldNameRequiredError'); + } else if (isReportFieldNameExisting(policy?.fieldList, name, fieldTarget)) { + errors[INPUT_IDS.NAME] = translate(isInvoiceField ? 'workspace.invoiceFields.existingInvoiceFieldNameError' : 'workspace.reportFields.existingReportFieldNameError'); + } else if ([...name].length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) { + 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(isInvoiceField ? 'workspace.invoiceFields.invoiceFieldTypeRequiredError' : 'workspace.reportFields.reportFieldTypeRequiredError'); + } + + 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.TEXT || type === CONST.REPORT_FIELD_TYPES.FORMULA) && !!formInitialValue && !errors[INPUT_IDS.INITIAL_VALUE]) { + const unsupportedFormulaParts = getUnsupportedReportFieldFormulaParts(formInitialValue); + if (unsupportedFormulaParts.length > 0) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.unsupportedFormulaValueError', { + value: unsupportedFormulaParts.join(', '), + }); + } + } + + if (type === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength > 0 && !isRequiredFulfilled(formInitialValue)) { + errors[INPUT_IDS.INITIAL_VALUE] = translate( + isInvoiceField ? 'workspace.invoiceFields.invoiceFieldInitialValueRequiredError' : 'workspace.reportFields.reportFieldInitialValueRequiredError', + ); + } + + return errors; + }, + [availableListValuesLength, fieldTarget, isInvoiceField, policy?.fieldList, translate], + ); + + const validateName = useCallback( + (values: Record) => { + const errors: Record = {}; + const name = values[INPUT_IDS.NAME]; + if (isReportFieldNameExisting(policy?.fieldList, name, fieldTarget)) { + errors[INPUT_IDS.NAME] = translate(isInvoiceField ? 'workspace.invoiceFields.existingInvoiceFieldNameError' : 'workspace.reportFields.existingReportFieldNameError'); + } + return errors; + }, + [fieldTarget, isInvoiceField, policy?.fieldList, translate], + ); + + const handleOnValueCommitted = useCallback( + (inputValues: FormOnyxValues) => (initialValue: string) => { + 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.DATE && ( + + )} + + {inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength > 0 && ( + + )} + + )} + + + + ); +} + +export default CreateFieldsPage; diff --git a/src/pages/workspace/fields/FieldsAddListValuePage.tsx b/src/pages/workspace/fields/FieldsAddListValuePage.tsx new file mode 100644 index 000000000000..48bceb9f0166 --- /dev/null +++ b/src/pages/workspace/fields/FieldsAddListValuePage.tsx @@ -0,0 +1,122 @@ +import React, {useCallback, useMemo} from 'react'; +import {Keyboard} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +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 usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import type {PolicyFeature} 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'; + +type FieldsAddListValuePageProps = { + policy: OnyxEntry; + policyID: string; + reportFieldID?: string; + featureName: ValueOf; + policyFeature: PolicyFeature; + testID: string; +}; + +function FieldsAddListValuePage({policy, policyID, reportFieldID, featureName, policyFeature, testID}: FieldsAddListValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT); + const {canWrite} = usePolicyFeatureWriteAccess(policy, policyFeature); + + 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 000000000000..dcd021dd23a0 --- /dev/null +++ b/src/pages/workspace/fields/FieldsEditValuePage.tsx @@ -0,0 +1,112 @@ +import React, {useCallback} from 'react'; +import {Keyboard} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +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 usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import type {PolicyFeature} 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'; + +type FieldsEditValuePageProps = { + policy: OnyxEntry; + policyID: string; + valueIndex: number; + featureName: ValueOf; + policyFeature: PolicyFeature; + testID: string; +}; + +function FieldsEditValuePage({policy, policyID, valueIndex, featureName, policyFeature, testID}: FieldsEditValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT); + const {canWrite} = usePolicyFeatureWriteAccess(policy, policyFeature); + + 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 000000000000..7d44ab1951bb --- /dev/null +++ b/src/pages/workspace/fields/FieldsInitialValuePage.tsx @@ -0,0 +1,171 @@ +import React, {useCallback, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +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, getUnsupportedReportFieldFormulaParts} from '@libs/WorkspaceReportFieldUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import ReportFieldsInitialListValuePicker from '@pages/workspace/reports/InitialListValueSelector/ReportFieldsInitialListValuePicker'; +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'; + +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 isInvoiceField = reportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + 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.TEXT || reportField?.type === CONST.REPORT_FIELD_TYPES.FORMULA) && !!formInitialValue && !errors[INPUT_IDS.INITIAL_VALUE]) { + const unsupportedFormulaParts = getUnsupportedReportFieldFormulaParts(formInitialValue); + if (unsupportedFormulaParts.length > 0) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.unsupportedFormulaValueError', { + value: unsupportedFormulaParts.join(', '), + }); + } + } + + if (reportField?.type === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength > 0 && !isRequiredFulfilled(formInitialValue)) { + errors[INPUT_IDS.INITIAL_VALUE] = translate( + isInvoiceField ? 'workspace.invoiceFields.invoiceFieldInitialValueRequiredError' : 'workspace.reportFields.reportFieldInitialValueRequiredError', + ); + } + + return errors; + }, + [availableListValuesLength, isInvoiceField, 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 && !isInvoiceField && ( + + + {translate(isInvoiceField ? 'workspace.invoiceFields.listValuesInputSubtitle' : '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 000000000000..9e7f2386bfd8 --- /dev/null +++ b/src/pages/workspace/fields/FieldsListValuesPage.tsx @@ -0,0 +1,438 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import Button from '@components/Button'; +import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; +import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; +import EmptyStateComponent from '@components/EmptyStateComponent'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import {ModalActions} from '@components/Modal/Global/ModalContext'; +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 Switch from '@components/Switch'; +import Text from '@components/Text'; +import useConfirmModal from '@hooks/useConfirmModal'; +import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import useOnyx from '@hooks/useOnyx'; +import usePolicyFeatureWriteAccess from '@hooks/usePolicyFeatureWriteAccess'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; +import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; +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 type {PolicyFeature} 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 {Route as Routes} from '@src/ROUTES'; +import type {Policy} from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; + +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; + policyFeature: PolicyFeature; + getValueSettingsRoute: (isInvoiceRoute: boolean, policyID: string, valueIndex: number, reportFieldID?: string) => Routes; + getAddValueRoute: (isInvoiceRoute: boolean, policyID: string, reportFieldID?: string) => Routes; + testID: string; +}; + +function FieldsListValuesPage({policy, policyID, reportFieldID, isInvoicePage, featureName, policyFeature, 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); + const isMobileSelectionModeEnabled = useMobileSelectionMode(); + const icons = useMemoizedLazyExpensifyIcons(['Checkmark', 'Close', 'Plus', 'Trashcan']); + const illustrations = useMemoizedLazyIllustrations(['FolderWithPapers']); + const {showConfirmModal} = useConfirmModal(); + const {canWrite, withReadOnlyFallback} = usePolicyFeatureWriteAccess(policy, policyFeature); + + const [selectedValues, setSelectedValues] = useState>({}); + const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); + const reportField = reportFieldID ? policy?.fieldList?.[getReportFieldKey(reportFieldID)] : undefined; + const shouldUseInvoiceRoutes = isInvoicePage || reportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + const listInputSubtitleKey = shouldUseInvoiceRoutes ? 'workspace.invoiceFields.listInputSubtitle' : 'workspace.reportFields.listInputSubtitle'; + const findFieldKey = shouldUseInvoiceRoutes ? 'workspace.invoiceFields.findInvoiceField' : 'workspace.reportFields.findReportField'; + const emptyValuesSubtitleKey = shouldUseInvoiceRoutes ? 'workspace.invoiceFields.emptyInvoiceFieldsValues.subtitle' : 'workspace.reportFields.emptyReportFieldsValues.subtitle'; + + const canSelectMultiple = canWrite && (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)} + disabled={!canWrite} + disabledAction={withReadOnlyFallback()} + showLockIcon={!canWrite} + /> + ), + })), + [canSelectMultiple, canWrite, disabledListValues, listValues, selectedValues, translate, updateReportFieldListValueEnabled, withReadOnlyFallback], + ); + + 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 shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); + + 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, + }); + } + + 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 (canWrite && (isSmallScreenWidth ? isMobileSelectionModeEnabled : selectedValuesArray.length > 0)) { + if (selectedValuesArray.length > 0 && !hasAccountingConnections) { + options.push({ + icon: icons.Trashcan, + text: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), + value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, + onSelected: () => { + showConfirmModal({ + danger: true, + title: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), + prompt: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValuePrompt' : 'workspace.reportFields.deleteValuesPrompt'), + confirmText: translate('common.delete'), + cancelText: translate('common.cancel'), + }).then((result) => { + if (result.action !== ModalActions.CONFIRM) { + return; + } + + handleDeleteValues(); + }); + }, + }); + } + 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: icons.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: icons.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={[shouldDisplayButtonsInSeparateLine && styles.flexGrow1, shouldDisplayButtonsInSeparateLine && styles.mb3]} + isDisabled={!selectedValuesArray.length} + /> + ); + } + + if (canWrite && !hasAccountingConnections) { + return ( + ); } };