diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 08b1b94e0da68..cfcb360f481fd 100755 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -3188,6 +3188,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', @@ -7680,6 +7681,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 f391d08d02547..4284cd453348c 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1922,6 +1922,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 1871a47bc9c17..1412c2ca57845 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -658,6 +658,13 @@ const SCREENS = { INVOICES_VERIFY_ACCOUNT: 'Workspace_Invoices_Verify_Account', INVOICES_COMPANY_NAME: 'Workspace_Invoices_Company_Name', INVOICES_COMPANY_WEBSITE: 'Workspace_Invoices_Company_Website', + INVOICE_FIELDS_CREATE: 'Workspace_InvoiceFields_Create', + INVOICE_FIELDS_SETTINGS: 'Workspace_InvoiceFields_Settings', + INVOICE_FIELDS_LIST_VALUES: 'Workspace_InvoiceFields_ListValues', + INVOICE_FIELDS_ADD_VALUE: 'Workspace_InvoiceFields_AddValue', + INVOICE_FIELDS_VALUE_SETTINGS: 'Workspace_InvoiceFields_ValueSettings', + INVOICE_FIELDS_EDIT_VALUE: 'Workspace_InvoiceFields_EditValue', + INVOICE_FIELDS_EDIT_INITIAL_VALUE: 'Workspace_InvoiceFields_EditInitialValue', MEMBERS: 'Workspace_Members', MEMBERS_IMPORT: 'Members_Import', MEMBERS_IMPORTED: 'Members_Imported', diff --git a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx index 0eb61411e18ef..02e8b3bf47d21 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestViewReportFields.tsx @@ -117,8 +117,9 @@ function MoneyRequestViewReportFields({report, policy, isCombinedReport = false, const isOnlyTitleFieldEnabled = enabledReportFields.length === 1 && isReportFieldOfTypeTitle(enabledReportFields.at(0)); const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); + const areFieldsEnabledForReport = isInvoiceReport ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled; - const shouldDisplayReportFields = (isPaidGroupPolicyExpenseReport || isInvoiceReport) && !!policy?.areReportFieldsEnabled && (!isOnlyTitleFieldEnabled || !isCombinedReport); + const shouldDisplayReportFields = (isPaidGroupPolicyExpenseReport || isInvoiceReport) && !!areFieldsEnabledForReport && (!isOnlyTitleFieldEnabled || !isCombinedReport); return ( shouldDisplayReportFields && diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index b484b58de41ba..06406ee40cacf 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -106,6 +106,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo const isPaidGroupPolicyExpenseReport = isPaidGroupPolicyExpenseReportUtils(report); const isInvoiceReport = isInvoiceReportUtils(report); + const areFieldsEnabledForReport = isInvoiceReport ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled; const shouldShowReportField = !isClosedExpenseReportWithNoExpenses && (isPaidGroupPolicyExpenseReport || isInvoiceReport) && @@ -135,7 +136,7 @@ function MoneyReportView({report, policy, isCombinedReport = false, shouldShowTo {!isClosedExpenseReportWithNoExpenses && ( <> {(isPaidGroupPolicyExpenseReport || 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 864c6e8ced9dc..a2b912bf0b24b 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -3882,6 +3882,7 @@ ${ customFieldHint: 'Fügen Sie benutzerdefinierte Kodierung hinzu, die auf alle Ausgaben dieses Mitglieds angewendet wird.', reports: 'Berichte', reportFields: 'Berichtsfelder', + invoiceFields: 'Rechnungsfelder', reportTitle: 'Berichtstitel', reportField: 'Berichtsfeld', taxes: 'Steuern', @@ -5396,6 +5397,14 @@ _Für ausführlichere Anweisungen [besuchen Sie unsere Hilfeseite](${CONST.NETSU reportFieldInitialValueRequiredError: 'Bitte wählen Sie einen Anfangswert für das Berichtsfeld', genericFailureMessage: 'Beim Aktualisieren des Berichtsfelds ist ein Fehler aufgetreten. Bitte versuche es erneut.', }, + 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?', + }, tags: { tagName: 'Tag-Name', requiresTag: 'Mitglieder müssen alle Ausgaben taggen', diff --git a/src/languages/en.ts b/src/languages/en.ts index e9750ea9ad094..d17fd31dd7f93 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -3914,6 +3914,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', @@ -5351,6 +5352,14 @@ 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?', + }, tags: { tagName: 'Tag name', requiresTag: 'Members must tag all expenses', diff --git a/src/languages/es.ts b/src/languages/es.ts index ac8c71a6ae0a9..44a0c4bf00951 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3646,6 +3646,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', @@ -5105,6 +5106,14 @@ ${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?', + }, tags: { tagName: 'Nombre de etiqueta', requiresTag: 'Los miembros deben etiquetar todos los gastos', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 19b8d0be841ca..b083d8ceaa498 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -3890,6 +3890,7 @@ ${ customFieldHint: 'Ajouter un codage personnalisé qui s’applique à toutes les dépenses de ce membre.', reports: 'Rapports', reportFields: 'Champs du rapport', + invoiceFields: 'Champs de facture', reportTitle: 'Titre du rapport', reportField: 'Champ de rapport', taxes: 'Taxes', @@ -5405,6 +5406,14 @@ _Pour des instructions plus détaillées, [visitez notre site d’aide](${CONST. reportFieldInitialValueRequiredError: 'Veuillez choisir une valeur initiale pour le champ de rapport', genericFailureMessage: 'Une erreur s’est produite lors de la mise à jour du champ de rapport. Veuillez réessayer.', }, + 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?', + }, tags: { tagName: 'Nom de balise', requiresTag: 'Les membres doivent étiqueter toutes les dépenses', diff --git a/src/languages/it.ts b/src/languages/it.ts index 16d087929329b..578fd1c1edc05 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -3871,6 +3871,7 @@ ${ customFieldHint: 'Aggiungi una codifica personalizzata che si applica a tutte le spese di questo membro.', reports: 'Report', reportFields: 'Campi del report', + invoiceFields: 'Campi fattura', reportTitle: 'Titolo del report', reportField: 'Campo del report', taxes: 'Imposte', @@ -5382,6 +5383,14 @@ _Per istruzioni più dettagliate, [visita il nostro sito di assistenza](${CONST. reportFieldInitialValueRequiredError: 'Scegli un valore iniziale per il campo del rendiconto', genericFailureMessage: 'Si è verificato un errore durante l’aggiornamento del campo del report. Riprova.', }, + 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?', + }, tags: { tagName: 'Nome tag', requiresTag: 'I membri devono taggare tutte le spese', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 3618db1228e3f..f2c3602c00c81 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -3858,6 +3858,7 @@ ${ customFieldHint: 'このメンバーからのすべての支出に適用されるカスタムコーディングを追加します。', reports: 'レポート', reportFields: 'レポート項目', + invoiceFields: '請求書項目', reportTitle: 'レポートタイトル', reportField: 'レポートフィールド', taxes: '税金', @@ -5346,6 +5347,14 @@ _より詳しい手順については、[ヘルプサイトをご覧ください reportFieldInitialValueRequiredError: 'レポート項目の初期値を選択してください', genericFailureMessage: 'レポートフィールドの更新中にエラーが発生しました。もう一度お試しください。', }, + 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?', + }, tags: { tagName: 'タグ名', requiresTag: 'メンバーはすべての経費にタグを付ける必要があります', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index a81da8b143d58..6ea6b27a29ec2 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -3871,6 +3871,7 @@ ${ customFieldHint: 'Aangepaste codering toevoegen die van toepassing is op alle uitgaven van dit lid.', reports: 'Rapporten', reportFields: 'Rapportvelden', + invoiceFields: 'Factuurvelden', reportTitle: 'Rapporttitel', reportField: 'Rapportveld', taxes: 'Belastingen', @@ -5372,6 +5373,14 @@ _Voor gedetailleerdere instructies, [bezoek onze helpsite](${CONST.NETSUITE_IMPO reportFieldInitialValueRequiredError: 'Kies een beginwaarde voor het rapportveld', genericFailureMessage: 'Er is een fout opgetreden bij het bijwerken van het rapportveld. Probeer het opnieuw.', }, + 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?', + }, tags: { tagName: 'Tagnaam', requiresTag: 'Leden moeten alle uitgaven taggen', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 78b41d4c47fe4..d13234084076e 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -3864,6 +3864,7 @@ ${ customFieldHint: 'Dodaj niestandardowe kodowanie, które ma zastosowanie do wszystkich wydatków tego członka.', reports: 'Raporty', reportFields: 'Pola raportu', + invoiceFields: 'Pola faktury', reportTitle: 'Tytuł raportu', reportField: 'Pole raportu', taxes: 'Podatki', @@ -5363,6 +5364,14 @@ _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: "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?', + }, tags: { tagName: 'Nazwa tagu', requiresTag: 'Członkowie muszą oznaczyć wszystkie wydatki', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 1874aa8c5ca5e..ece538c3d9c9c 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -3863,6 +3863,7 @@ ${ customFieldHint: 'Adicionar codificação personalizada que se aplique a todos os gastos deste membro.', reports: 'Relatórios', reportFields: 'Campos de relatório', + invoiceFields: 'Campos de fatura', reportTitle: 'Título do relatório', reportField: 'Campo de relatório', taxes: 'Impostos', @@ -5363,6 +5364,14 @@ _Para instruções mais detalhadas, [visite nosso site de ajuda](${CONST.NETSUIT reportFieldInitialValueRequiredError: 'Por favor, 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: "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?', + }, tags: { tagName: 'Nome da tag', requiresTag: 'Membros devem etiquetar todas as despesas', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 34dd0148bc2f5..b098499fc431c 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -3798,6 +3798,7 @@ ${ customFieldHint: '为该成员的所有支出添加适用的自定义编码。', reports: '报表', reportFields: '报表字段', + invoiceFields: '发票字段', reportTitle: '报表标题', reportField: '报表字段', taxes: '税费', @@ -5254,6 +5255,14 @@ _如需更详细的说明,请[访问我们的帮助网站](${CONST.NETSUITE_IM reportFieldInitialValueRequiredError: '请选择报表字段的初始值', genericFailureMessage: '更新报表字段时出错。请重试。', }, + 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?', + }, tags: { tagName: '标签名称', requiresTag: '成员必须为所有报销添加标签', diff --git a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts new file mode 100644 index 0000000000000..fa22805f4e71c --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldListValueParams.ts @@ -0,0 +1,10 @@ +type CreateWorkspaceInvoiceFieldListValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default CreateWorkspaceInvoiceFieldListValueParams; diff --git a/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts new file mode 100644 index 0000000000000..5cce12e49273e --- /dev/null +++ b/src/libs/API/parameters/CreateWorkspaceInvoiceFieldParams.ts @@ -0,0 +1,10 @@ +type CreateWorkspaceInvoiceFieldParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default CreateWorkspaceInvoiceFieldParams; diff --git a/src/libs/API/parameters/DeletePolicyInvoiceField.ts b/src/libs/API/parameters/DeletePolicyInvoiceField.ts new file mode 100644 index 0000000000000..5b3c37fb1e07c --- /dev/null +++ b/src/libs/API/parameters/DeletePolicyInvoiceField.ts @@ -0,0 +1,10 @@ +type DeletePolicyInvoiceField = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default DeletePolicyInvoiceField; diff --git a/src/libs/API/parameters/EnablePolicyInvoiceFieldsParams.ts b/src/libs/API/parameters/EnablePolicyInvoiceFieldsParams.ts new file mode 100644 index 0000000000000..5afc5c912d904 --- /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 0000000000000..a52405efad545 --- /dev/null +++ b/src/libs/API/parameters/EnableWorkspaceInvoiceFieldListValueParams.ts @@ -0,0 +1,10 @@ +type EnableWorkspaceInvoiceFieldListValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default EnableWorkspaceInvoiceFieldListValueParams; diff --git a/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts b/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts new file mode 100644 index 0000000000000..c793b4bdf10a2 --- /dev/null +++ b/src/libs/API/parameters/RemoveWorkspaceInvoiceFieldListValueParams.ts @@ -0,0 +1,10 @@ +type RemoveWorkspaceInvoiceFieldListValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default RemoveWorkspaceInvoiceFieldListValueParams; diff --git a/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts b/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts new file mode 100644 index 0000000000000..eeb3cd5106aa7 --- /dev/null +++ b/src/libs/API/parameters/UpdateWorkspaceInvoiceFieldInitialValueParams.ts @@ -0,0 +1,10 @@ +type UpdateWorkspaceInvoiceFieldInitialValueParams = { + policyID: string; + /** + * Stringified JSON object with type of following structure: + * Array + */ + invoiceFields: string; +}; + +export default UpdateWorkspaceInvoiceFieldInitialValueParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 5cdbc9202dc4f..78be0b3663f2d 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -228,6 +228,7 @@ export type {default as EnablePolicyTravelParams} from './EnablePolicyTravelPara export type {default as EnablePolicyTagsParams} from './EnablePolicyTagsParams'; export type {default as SetPolicyTagsEnabled} from './SetPolicyTagsEnabled'; export type {default as EnablePolicyWorkflowsParams} from './EnablePolicyWorkflowsParams'; +export type {default as EnablePolicyInvoiceFieldsParams} from './EnablePolicyInvoiceFieldsParams'; export type {default as EnablePolicyReportFieldsParams} from './EnablePolicyReportFieldsParams'; export type {default as EnablePolicyExpensifyCardsParams} from './EnablePolicyExpensifyCardsParams'; export type {default as AcceptJoinRequestParams} from './AcceptJoinRequest'; @@ -301,15 +302,21 @@ export type {default as ApproveMoneyRequestOnSearchParams} from './ApproveMoneyR export type {default as PayMoneyRequestOnSearchParams} from './PayMoneyRequestOnSearchParams'; export type {default as UnholdMoneyRequestOnSearchParams} from './UnholdMoneyRequestOnSearchParams'; export type {default as UpdateNetSuiteSubsidiaryParams} from './UpdateNetSuiteSubsidiaryParams'; +export type {default as DeletePolicyInvoiceField} from './DeletePolicyInvoiceField'; export type {default as DeletePolicyReportField} from './DeletePolicyReportField'; export type {default as ConnectPolicyToNetSuiteParams} from './ConnectPolicyToNetSuiteParams'; export type {default as CreateWorkspaceReportFieldParams} from './CreateWorkspaceReportFieldParams'; +export type {default as CreateWorkspaceInvoiceFieldParams} from './CreateWorkspaceInvoiceFieldParams'; export type {default as UpdateWorkspaceReportFieldInitialValueParams} from './UpdateWorkspaceReportFieldInitialValueParams'; +export type {default as UpdateWorkspaceInvoiceFieldInitialValueParams} from './UpdateWorkspaceInvoiceFieldInitialValueParams'; export type {default as EnableWorkspaceReportFieldListValueParams} from './EnableWorkspaceReportFieldListValueParams'; +export type {default as EnableWorkspaceInvoiceFieldListValueParams} from './EnableWorkspaceInvoiceFieldListValueParams'; export type {default as EnablePolicyInvoicingParams} from './EnablePolicyInvoicingParams'; export type {default as 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 4bc6c6027aae5..b50bdcc0b4144 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -180,6 +180,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', @@ -250,6 +251,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', @@ -373,9 +375,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', @@ -714,6 +721,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; @@ -802,6 +810,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; @@ -977,10 +986,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>; @@ -1166,6 +1180,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', @@ -1252,6 +1267,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 9ed54065bb215..a15b710b4ce82 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -790,6 +790,13 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/reports/ReportFieldsValueSettingsPage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/reports/ReportFieldsInitialValuePage').default, [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/reports/ReportFieldsEditValuePage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_CREATE]: () => require('../../../../pages/workspace/invoices/CreateInvoiceFieldsPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_SETTINGS]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsSettingsPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_LIST_VALUES]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsListValuesPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_ADD_VALUE]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsAddListValuePage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_VALUE_SETTINGS]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsValueSettingsPage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_INITIAL_VALUE]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsInitialValuePage').default, + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_VALUE]: () => require('../../../../pages/workspace/invoices/InvoiceFieldsEditValuePage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_IMPORT]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctImportPage').default, [SCREENS.WORKSPACE.ACCOUNTING.SAGE_INTACCT_TOGGLE_MAPPING]: () => require('../../../../pages/workspace/accounting/intacct/import/SageIntacctToggleMappingsPage').default, diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index 6b9f67d4ba78f..c5244a4abf11a 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -236,7 +236,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 a29fb057fbda2..d46dc14532dc1 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -632,31 +632,59 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.REPORT_FIELDS_CREATE]: { policyID: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_CREATE]: { + policyID: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_LIST_VALUES]: { policyID: string; reportFieldID?: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_LIST_VALUES]: { + policyID: string; + reportFieldID?: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_ADD_VALUE]: { policyID: string; reportFieldID?: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_ADD_VALUE]: { + policyID: string; + reportFieldID?: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_VALUE_SETTINGS]: { policyID: string; valueIndex: number; reportFieldID?: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_VALUE_SETTINGS]: { + policyID: string; + valueIndex: number; + reportFieldID?: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_VALUE]: { policyID: string; valueIndex: number; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_VALUE]: { + policyID: string; + valueIndex: number; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_SETTINGS]: { policyID: string; reportFieldID: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_SETTINGS]: { + policyID: string; + reportFieldID: string; + }; [SCREENS.WORKSPACE.REPORT_FIELDS_EDIT_INITIAL_VALUE]: { policyID: string; reportFieldID: string; }; + [SCREENS.WORKSPACE.INVOICE_FIELDS_EDIT_INITIAL_VALUE]: { + policyID: string; + reportFieldID: string; + }; [SCREENS.WORKSPACE.MEMBER_DETAILS]: { policyID: string; accountID: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 7246c21c56ca5..345fabdea6259 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4380,9 +4380,6 @@ function isHoldCreator(transaction: OnyxEntry, reportID: string | u * 2. Report is settled or it is closed */ 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); @@ -4470,7 +4467,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) { @@ -13027,14 +13024,17 @@ function shouldHideSingleReportField(reportField: PolicyReportField) { */ 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}`] as Record | undefined; const fieldValues: Record = {}; const fieldsByName: Record = {}; for (const field of fields) { if (field.name) { + const fieldKey = getReportFieldKey(field.fieldID); + const reportNameValuePairField = reportNameValuePairs?.[fieldKey]; 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; } } diff --git a/src/libs/WorkspaceReportFieldUtils.ts b/src/libs/WorkspaceReportFieldUtils.ts index 250d98f0d9e05..2fb22c14fd76e 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'; @@ -109,6 +110,25 @@ function isReportFieldNameExisting(fieldList: Record return Object.values(fieldList ?? {}).some((reportField) => 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; +} + export { getReportFieldTypeTranslationKey, getReportFieldAlternativeTextTranslationKey, @@ -117,4 +137,5 @@ export { getReportFieldInitialValue, hasFormulaPartsInInitialValue, isReportFieldNameExisting, + isReportFieldTargetValid, }; diff --git a/src/libs/actions/Policy/Policy.ts b/src/libs/actions/Policy/Policy.ts index e2d4aca81a829..52751ae0ca289 100644 --- a/src/libs/actions/Policy/Policy.ts +++ b/src/libs/actions/Policy/Policy.ts @@ -23,6 +23,7 @@ import type { EnablePolicyCompanyCardsParams, EnablePolicyConnectionsParams, EnablePolicyExpensifyCardsParams, + EnablePolicyInvoiceFieldsParams, EnablePolicyInvoicingParams, EnablePolicyReportFieldsParams, EnablePolicyTaxesParams, @@ -4294,16 +4295,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, }, }, }, @@ -4314,7 +4318,7 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) { key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { pendingFields: { - areReportFieldsEnabled: null, + [enableFieldKey]: null, }, }, }, @@ -4324,18 +4328,26 @@ function enablePolicyReportFields(policyID: string, enabled: boolean) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { - areReportFieldsEnabled: !enabled, + [enableFieldKey]: !enabled, pendingFields: { - areReportFieldsEnabled: null, + [enableFieldKey]: null, }, }, }, ], }; - const parameters: EnablePolicyReportFieldsParams = {policyID, enabled}; + const parameters: EnablePolicyReportFieldsParams | EnablePolicyInvoiceFieldsParams = {policyID, enabled}; + + API.writeWithNoDuplicatesEnableFeatureConflicts(command, parameters, onyxData); +} + +function enablePolicyReportFields(policyID: string, enabled: boolean) { + enablePolicyFields(policyID, enabled, WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS); +} - API.writeWithNoDuplicatesEnableFeatureConflicts(WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, parameters, onyxData); +function enablePolicyInvoiceFields(policyID: string, enabled: boolean) { + enablePolicyFields(policyID, enabled, WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS); } function enablePolicyTaxes(policyID: string, enabled: boolean) { @@ -6846,6 +6858,7 @@ export { enablePolicyConnections, enablePolicyReceiptPartners, enablePolicyReportFields, + enablePolicyInvoiceFields, enablePolicyTaxes, enablePolicyWorkflows, enablePolicyTimeTracking, diff --git a/src/libs/actions/Policy/ReportField.ts b/src/libs/actions/Policy/ReportField.ts index b802d6a4a1d2e..4a1435edd635c 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,7 +185,7 @@ function deleteReportFieldsListValue({valueIndexes, listValues, disabledListValu /** * Creates a new report field. */ -function createReportField({name, type, initialValue, listValues, disabledListValues, policyExpenseReportIDs, policy}: CreateReportFieldParams) { +function createReportField({name, type, initialValue, listValues, disabledListValues, policyReportIDs, isInvoiceField = false, policy}: CreateReportFieldParams) { if (!policy) { Log.warn('Policy data is not present'); return; @@ -182,7 +202,7 @@ function createReportField({name, type, initialValue, listValues, disabledListVa const optimisticReportFieldDataForPolicy: Omit, 'value'> = { name, type: optimisticType, - target: 'expense', + target: isInvoiceField ? CONST.REPORT_FIELD_TARGETS.INVOICE : CONST.REPORT_FIELD_TARGETS.EXPENSE, defaultValue: initialValue, values: listValues, disabledOptions: disabledListValues, @@ -205,7 +225,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 +251,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 +281,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 +302,44 @@ function deleteReportFields({policy, reportFieldsToUpdate}: DeleteReportFieldsPa } const allReportFields = policy?.fieldList ?? {}; + const resolveReportFieldKey = (reportFieldIDOrKey: string) => { + if (allReportFields[reportFieldIDOrKey]) { + return reportFieldIDOrKey; + } + + const expensifyKey = ReportUtils.getReportFieldKey(reportFieldIDOrKey); + if (allReportFields[expensifyKey]) { + return expensifyKey; + } + + if (reportFieldIDOrKey.startsWith('expensify_')) { + const rawKey = reportFieldIDOrKey.slice('expensify_'.length); + if (allReportFields[rawKey]) { + return rawKey; + } + } + + return undefined; + }; + + const resolvedReportFieldKeys = reportFieldsToUpdate.map(resolveReportFieldKey).filter((key): key is string => !!key); + 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 +378,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 +463,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 +514,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 +561,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 +616,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 +644,7 @@ export { updateReportFieldInitialValue, updateReportFieldListValueEnabled, openPolicyReportFieldsPage, + openPolicyInvoicesPage, addReportFieldListValue, removeReportFieldListValue, }; diff --git a/src/libs/actions/RequestConflictUtils.ts b/src/libs/actions/RequestConflictUtils.ts index 638a3bccdb252..710aadfaf8656 100644 --- a/src/libs/actions/RequestConflictUtils.ts +++ b/src/libs/actions/RequestConflictUtils.ts @@ -31,6 +31,7 @@ const enablePolicyFeatureCommand = [ WRITE_COMMANDS.ENABLE_POLICY_TAGS, WRITE_COMMANDS.ENABLE_POLICY_TAXES, WRITE_COMMANDS.ENABLE_POLICY_REPORT_FIELDS, + WRITE_COMMANDS.ENABLE_POLICY_INVOICE_FIELDS, WRITE_COMMANDS.ENABLE_POLICY_WORKFLOWS, WRITE_COMMANDS.SET_POLICY_RULES_ENABLED, WRITE_COMMANDS.ENABLE_POLICY_INVOICING, diff --git a/src/pages/EditReportFieldPage.tsx b/src/pages/EditReportFieldPage.tsx index 01132a56b44ac..04cf61791ffaa 100644 --- a/src/pages/EditReportFieldPage.tsx +++ b/src/pages/EditReportFieldPage.tsx @@ -28,7 +28,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; -import type {Policy} from '@src/types/onyx'; +import type {Policy, PolicyReportField} from '@src/types/onyx'; import EditReportFieldDate from './EditReportFieldDate'; import EditReportFieldDropdown from './EditReportFieldDropdown'; import EditReportFieldText from './EditReportFieldText'; @@ -39,10 +39,14 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { const {backTo, reportID, policyID} = route.params; const fieldKey = getReportFieldKey(route.params.fieldID); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false}); + const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {canBeMissing: true}); const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, {canBeMissing: false}); const [recentlyUsedReportFields] = useOnyx(ONYXKEYS.RECENTLY_USED_REPORT_FIELDS, {canBeMissing: true}); - const reportField = report?.fieldList?.[fieldKey] ?? policy?.fieldList?.[fieldKey]; - const policyField = policy?.fieldList?.[fieldKey] ?? reportField; + const reportNameValuePairsRecord = reportNameValuePairs as Record | undefined; + const reportFieldFromNVP = reportNameValuePairsRecord?.[fieldKey] ?? reportNameValuePairsRecord?.[route.params.fieldID]; + const reportField = + reportFieldFromNVP ?? report?.fieldList?.[fieldKey] ?? report?.fieldList?.[route.params.fieldID] ?? policy?.fieldList?.[fieldKey] ?? policy?.fieldList?.[route.params.fieldID]; + const policyField = policy?.fieldList?.[fieldKey] ?? policy?.fieldList?.[route.params.fieldID] ?? reportField; const isDisabled = isReportFieldDisabledForUser(report, reportField, policy) && reportField?.type !== CONST.REPORT_FIELD_TYPES.FORMULA; const {isBetaEnabled} = usePermissions(); const isASAPSubmitBetaEnabled = isBetaEnabled(CONST.BETAS.ASAP_SUBMIT); @@ -53,7 +57,8 @@ function EditReportFieldPage({route}: EditReportFieldPageProps) { const [isDeleteModalVisible, setIsDeleteModalVisible] = useState(false); const {translate} = useLocalize(); const isReportFieldTitle = isReportFieldOfTypeTitle(reportField); - const reportFieldsEnabled = ((isPaidGroupPolicyExpenseReport(report) || isInvoiceReport(report)) && !!policy?.areReportFieldsEnabled) || isReportFieldTitle; + const isReportFieldsFeatureEnabled = report?.type === CONST.REPORT.TYPE.INVOICE ? policy?.areInvoiceFieldsEnabled : policy?.areReportFieldsEnabled; + const reportFieldsEnabled = ((isPaidGroupPolicyExpenseReport(report) || isInvoiceReport(report)) && !!isReportFieldsFeatureEnabled) || isReportFieldTitle; const hasOtherViolations = report?.fieldList && Object.entries(report.fieldList).some(([key, field]) => key !== fieldKey && field.value === '' && !isReportFieldDisabled(report, reportField, policy)); diff --git a/src/pages/workspace/fields/CreateFieldsPage.tsx b/src/pages/workspace/fields/CreateFieldsPage.tsx new file mode 100644 index 0000000000000..0ed8a97a25f27 --- /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 {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, {canBeMissing: true}); + + const reportTypeForTarget = useMemo(() => (isInvoiceField ? CONST.REPORT.TYPE.INVOICE : CONST.REPORT.TYPE.EXPENSE), [isInvoiceField]); + + const policyReportIDsSelector = useCallback( + (reports: OnyxCollection) => + Object.values(reports ?? {}) + .filter((report) => report?.policyID === policyID && report.type === reportTypeForTarget) + .map((report) => report?.reportID), + [policyID, reportTypeForTarget], + ); + + const [policyReportIDs] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { + canBeMissing: true, + selector: policyReportIDsSelector, + }); + + const listValuesCount = formDraft?.[INPUT_IDS.LIST_VALUES]?.length ?? 0; + + const submitForm = useCallback( + (values: FormOnyxValues) => { + const shouldClearListInitialValue = values[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && listValuesCount <= 1; + 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(); + }, + [formDraft, isInvoiceField, listValuesCount, policy, policyReportIDs], + ); + + const targetFieldList = useMemo( + () => + Object.fromEntries( + Object.entries(policy?.fieldList ?? {}).filter(([, reportField]) => + isInvoiceField ? reportField.target === CONST.REPORT_FIELD_TARGETS.INVOICE : !reportField.target || reportField.target === CONST.REPORT_FIELD_TARGETS.EXPENSE, + ), + ), + [isInvoiceField, policy?.fieldList], + ); + + const validateForm = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const {name, type, initialValue: formInitialValue} = values; + const errors: FormInputErrors = {}; + + if (!isRequiredFulfilled(name)) { + errors[INPUT_IDS.NAME] = translate('workspace.reportFields.reportFieldNameRequiredError'); + } else if (isReportFieldNameExisting(targetFieldList, name)) { + errors[INPUT_IDS.NAME] = translate('workspace.reportFields.existingReportFieldNameError'); + } else if ([...name].length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) { + // Uses the spread syntax to count the number of Unicode code points instead of the number of UTF-16 code units. + addErrorMessage(errors, INPUT_IDS.NAME, translate('common.error.characterLimitExceedCounter', [...name].length, CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH)); + } + + if (!isRequiredFulfilled(type)) { + errors[INPUT_IDS.TYPE] = translate('workspace.reportFields.reportFieldTypeRequiredError'); + } + + // formInitialValue can be undefined because the InitialValue component is rendered conditionally. + // If it's not been rendered when the validation is executed, formInitialValue will be undefined. + if (type === CONST.REPORT_FIELD_TYPES.TEXT && !!formInitialValue && formInitialValue.length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('common.error.characterLimitExceedCounter', formInitialValue.length, CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH); + } + + if ((type === CONST.REPORT_FIELD_TYPES.TEXT || type === CONST.REPORT_FIELD_TYPES.FORMULA) && hasCircularReferences(formInitialValue, name, policy?.fieldList)) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.circularReferenceError'); + } + + return errors; + }, + [policy?.fieldList, targetFieldList, translate], + ); + + const validateName = useCallback( + (values: Record) => { + const errors: Record = {}; + const name = values[INPUT_IDS.NAME]; + if (isReportFieldNameExisting(targetFieldList, name)) { + errors[INPUT_IDS.NAME] = translate('workspace.reportFields.existingReportFieldNameError'); + } + return errors; + }, + [targetFieldList, translate], + ); + + const handleOnValueCommitted = useCallback( + (inputValues: FormOnyxValues) => (initialValue: string) => { + // Mirror optimisticType logic from createReportField: if user enters a formula + // while type is Text, automatically switch the type to Formula in the form, otherwise back to Text. + const isFormula = hasFormulaPartsInInitialValue(initialValue); + if (isFormula) { + formRef.current?.resetForm({ + ...inputValues, + [INPUT_IDS.TYPE]: CONST.REPORT_FIELD_TYPES.FORMULA, + [INPUT_IDS.INITIAL_VALUE]: initialValue, + }); + } else { + formRef.current?.resetForm({ + ...inputValues, + [INPUT_IDS.TYPE]: CONST.REPORT_FIELD_TYPES.TEXT, + [INPUT_IDS.INITIAL_VALUE]: initialValue, + }); + } + }, + [], + ); + + useEffect(() => { + setInitialCreateReportFieldsForm(); + }, []); + + const listValues = [...(formDraft?.[INPUT_IDS.LIST_VALUES] ?? [])].sort(localeCompare).join(', '); + + return ( + + + + + {({inputValues}) => ( + + + { + let initialValue; + if (type === CONST.REPORT_FIELD_TYPES.DATE) { + initialValue = defaultDate; + } else if (type === CONST.REPORT_FIELD_TYPES.FORMULA) { + initialValue = '{report:id}'; + } else { + initialValue = ''; + } + + formRef.current?.resetForm({ + ...inputValues, + type, + initialValue, + }); + }} + /> + + {inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && ( + Navigation.navigate(listValuesRoute)} + title={listValues} + numberOfLinesTitle={5} + /> + )} + + {inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.LIST && listValuesCount > 1 && ( + + )} + + {inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.DATE && ( + + )} + + {(inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.TEXT || inputValues[INPUT_IDS.TYPE] === CONST.REPORT_FIELD_TYPES.FORMULA) && ( + + )} + + )} + + + + ); +} + +export default CreateFieldsPage; diff --git a/src/pages/workspace/fields/FieldsAddListValuePage.tsx b/src/pages/workspace/fields/FieldsAddListValuePage.tsx new file mode 100644 index 0000000000000..16ec77d042734 --- /dev/null +++ b/src/pages/workspace/fields/FieldsAddListValuePage.tsx @@ -0,0 +1,115 @@ +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 useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import {getReportFieldKey} from '@libs/ReportUtils'; +import {validateReportFieldListValueName} from '@libs/WorkspaceReportFieldUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {addReportFieldListValue, createReportFieldsListValue} from '@userActions/Policy/ReportField'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; +import type {Policy} from '@src/types/onyx'; + +type FieldsAddListValuePageProps = { + policy: OnyxEntry; + policyID: string; + reportFieldID?: string; + featureName: ValueOf; + testID: string; +}; + +function FieldsAddListValuePage({policy, policyID, reportFieldID, featureName, testID}: FieldsAddListValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); + + const listValues = useMemo(() => { + let reportFieldListValues: string[]; + if (reportFieldID) { + const reportFieldKey = getReportFieldKey(reportFieldID); + reportFieldListValues = Object.values(policy?.fieldList?.[reportFieldKey]?.values ?? {}); + } else { + reportFieldListValues = formDraft?.[INPUT_IDS.LIST_VALUES] ?? []; + } + return reportFieldListValues; + }, [formDraft, policy?.fieldList, reportFieldID]); + + const validate = useCallback( + (values: FormOnyxValues) => + validateReportFieldListValueName(values[INPUT_IDS.VALUE_NAME].trim(), '', listValues, INPUT_IDS.VALUE_NAME, translate), + [listValues, translate], + ); + + const createValue = useCallback( + (values: FormOnyxValues) => { + if (reportFieldID) { + addReportFieldListValue({policy, reportFieldID, valueName: values[INPUT_IDS.VALUE_NAME]}); + } else { + createReportFieldsListValue({ + valueName: values[INPUT_IDS.VALUE_NAME], + listValues: formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], + disabledListValues: formDraft?.[INPUT_IDS.DISABLED_LIST_VALUES] ?? [], + }); + } + Keyboard.dismiss(); + Navigation.goBack(); + }, + [formDraft, policy, reportFieldID], + ); + + return ( + + + + + + + + + ); +} + +export default FieldsAddListValuePage; diff --git a/src/pages/workspace/fields/FieldsEditValuePage.tsx b/src/pages/workspace/fields/FieldsEditValuePage.tsx new file mode 100644 index 0000000000000..2d3a31f51b82e --- /dev/null +++ b/src/pages/workspace/fields/FieldsEditValuePage.tsx @@ -0,0 +1,105 @@ +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 useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections} from '@libs/PolicyUtils'; +import {validateReportFieldListValueName} from '@libs/WorkspaceReportFieldUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import {renameReportFieldsListValue} from '@userActions/Policy/ReportField'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import INPUT_IDS from '@src/types/form/WorkspaceReportFieldForm'; +import type {Policy} from '@src/types/onyx'; + +type FieldsEditValuePageProps = { + policy: OnyxEntry; + policyID: string; + valueIndex: number; + featureName: ValueOf; + testID: string; +}; + +function FieldsEditValuePage({policy, policyID, valueIndex, featureName, testID}: FieldsEditValuePageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const {inputCallbackRef} = useAutoFocusInput(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); + + const currentValueName = formDraft?.listValues?.[valueIndex] ?? ''; + + const validate = useCallback( + (values: FormOnyxValues) => + validateReportFieldListValueName(values[INPUT_IDS.NEW_VALUE_NAME].trim(), currentValueName, formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], INPUT_IDS.NEW_VALUE_NAME, translate), + [currentValueName, formDraft, translate], + ); + + const editValue = useCallback( + (values: FormOnyxValues) => { + const valueName = values[INPUT_IDS.NEW_VALUE_NAME]?.trim(); + if (currentValueName !== valueName) { + renameReportFieldsListValue({ + valueIndex, + newValueName: valueName, + listValues: formDraft?.[INPUT_IDS.LIST_VALUES] ?? [], + }); + } + Keyboard.dismiss(); + Navigation.goBack(); + }, + [currentValueName, formDraft, valueIndex], + ); + + return ( + + + + + + + + + ); +} + +export default FieldsEditValuePage; diff --git a/src/pages/workspace/fields/FieldsInitialValuePage.tsx b/src/pages/workspace/fields/FieldsInitialValuePage.tsx new file mode 100644 index 0000000000000..c3f04e9748199 --- /dev/null +++ b/src/pages/workspace/fields/FieldsInitialValuePage.tsx @@ -0,0 +1,157 @@ +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} 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 availableListValuesLength = (reportField?.disabledOptions ?? []).filter((disabledListValue) => !disabledListValue).length; + const currentInitialValue = getReportFieldInitialValue(reportField, translate); + const [initialValue, setInitialValue] = useState(currentInitialValue); + + const submitForm = useCallback( + (values: FormOnyxValues) => { + if (currentInitialValue !== values.initialValue) { + updateReportFieldInitialValue({policy, reportFieldID, newInitialValue: values.initialValue}); + } + Navigation.goBack(); + }, + [currentInitialValue, policy, reportFieldID], + ); + + const submitListValueUpdate = (value: string) => { + updateReportFieldInitialValue({policy, reportFieldID, newInitialValue: currentInitialValue === value ? '' : value}); + Navigation.goBack(); + }; + + const validateForm = useCallback( + (values: FormOnyxValues): FormInputErrors => { + const {initialValue: formInitialValue} = values; + const errors: FormInputErrors = {}; + + if (formInitialValue.length > CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('common.error.characterLimitExceedCounter', formInitialValue.length, CONST.WORKSPACE_REPORT_FIELD_POLICY_MAX_LENGTH); + } + + if ( + (reportField?.type === CONST.REPORT_FIELD_TYPES.TEXT || reportField?.type === CONST.REPORT_FIELD_TYPES.FORMULA) && + hasCircularReferences(formInitialValue, reportField?.name, policy?.fieldList) + ) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.circularReferenceError'); + } + + if (reportField?.type === CONST.REPORT_FIELD_TYPES.LIST && availableListValuesLength > 0 && !isRequiredFulfilled(formInitialValue)) { + errors[INPUT_IDS.INITIAL_VALUE] = translate('workspace.reportFields.reportFieldInitialValueRequiredError'); + } + + return errors; + }, + [availableListValuesLength, reportField?.name, reportField?.type, policy?.fieldList, translate], + ); + + if (!reportField) { + return ; + } + + const isTextFieldType = reportField.type === CONST.REPORT_FIELD_TYPES.TEXT; + const isFormulaFieldType = reportField.type === CONST.REPORT_FIELD_TYPES.FORMULA; + const isListFieldType = reportField.type === CONST.REPORT_FIELD_TYPES.LIST; + + return ( + + + + {isListFieldType && ( + + {translate('workspace.reportFields.listValuesInputSubtitle')} + + )} + + {(isTextFieldType || isFormulaFieldType) && ( + + + + )} + {isListFieldType && ( + + )} + + + ); +} + +export default FieldsInitialValuePage; diff --git a/src/pages/workspace/fields/FieldsListValuesPage.tsx b/src/pages/workspace/fields/FieldsListValuesPage.tsx new file mode 100644 index 0000000000000..44640a105f383 --- /dev/null +++ b/src/pages/workspace/fields/FieldsListValuesPage.tsx @@ -0,0 +1,431 @@ +import React, {useCallback, useMemo, useState} from 'react'; +import {InteractionManager, 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 ConfirmModal from '@components/ConfirmModal'; +import EmptyStateComponent from '@components/EmptyStateComponent'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +// eslint-disable-next-line no-restricted-imports +import * as Expensicons from '@components/Icon/Expensicons'; +import ScreenWrapper from '@components/ScreenWrapper'; +import ScrollView from '@components/ScrollView'; +import SearchBar from '@components/SearchBar'; +import TableListItem from '@components/SelectionList/ListItem/TableListItem'; +import type {ListItem} from '@components/SelectionList/types'; +import SelectionListWithModal from '@components/SelectionListWithModal'; +import CustomListHeader from '@components/SelectionListWithModal/CustomListHeader'; +import TableListItemSkeleton from '@components/Skeletons/TableRowSkeleton'; +import Switch from '@components/Switch'; +import Text from '@components/Text'; +import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; +import useLocalize from '@hooks/useLocalize'; +import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; +import useOnyx from '@hooks/useOnyx'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useSearchBackPress from '@hooks/useSearchBackPress'; +import useSearchResults from '@hooks/useSearchResults'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; +import { + deleteReportFieldsListValue, + removeReportFieldListValue, + setReportFieldsListValueEnabled, + updateReportFieldListValueEnabled as updateReportFieldListValueEnabledReportField, +} from '@libs/actions/Policy/ReportField'; +import {canUseTouchScreen} from '@libs/DeviceCapabilities'; +import Navigation from '@libs/Navigation/Navigation'; +import {hasAccountingConnections as hasAccountingConnectionsPolicyUtils} from '@libs/PolicyUtils'; +import {getReportFieldKey} from '@libs/ReportUtils'; +import StringUtils from '@libs/StringUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {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; + 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, getValueSettingsRoute, getAddValueRoute, testID}: FieldsListValuesPageProps) { + const styles = useThemeStyles(); + const {translate, localeCompare} = useLocalize(); + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout here to use the mobile selection mode on small screens only + // See https://github.com/Expensify/App/issues/48724 for more details + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {isSmallScreenWidth} = useResponsiveLayout(); + const [formDraft] = useOnyx(ONYXKEYS.FORMS.WORKSPACE_REPORT_FIELDS_FORM_DRAFT, {canBeMissing: true}); + const isMobileSelectionModeEnabled = useMobileSelectionMode(); + const illustrations = useMemoizedLazyIllustrations(['FolderWithPapers']); + + const [selectedValues, setSelectedValues] = useState>({}); + const [deleteValuesConfirmModalVisible, setDeleteValuesConfirmModalVisible] = useState(false); + const hasAccountingConnections = hasAccountingConnectionsPolicyUtils(policy); + const reportField = reportFieldID ? policy?.fieldList?.[getReportFieldKey(reportFieldID)] : undefined; + const shouldUseInvoiceRoutes = isInvoicePage || reportField?.target === CONST.REPORT_FIELD_TARGETS.INVOICE; + + const canSelectMultiple = isSmallScreenWidth ? isMobileSelectionModeEnabled : true; + + const [listValues, disabledListValues] = useMemo(() => { + let reportFieldValues: string[]; + let reportFieldDisabledValues: boolean[]; + + if (reportFieldID) { + const reportFieldKey = getReportFieldKey(reportFieldID); + + reportFieldValues = Object.values(policy?.fieldList?.[reportFieldKey]?.values ?? {}); + reportFieldDisabledValues = Object.values(policy?.fieldList?.[reportFieldKey]?.disabledOptions ?? {}); + } else { + reportFieldValues = formDraft?.listValues ?? []; + reportFieldDisabledValues = formDraft?.disabledListValues ?? []; + } + + return [reportFieldValues, reportFieldDisabledValues]; + }, [formDraft?.disabledListValues, formDraft?.listValues, policy?.fieldList, reportFieldID]); + + const updateReportFieldListValueEnabled = useCallback( + (value: boolean, valueIndex: number) => { + if (reportFieldID) { + updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: [Number(valueIndex)], enabled: value}); + return; + } + + setReportFieldsListValueEnabled({ + valueIndexes: [valueIndex], + enabled: value, + disabledListValues, + }); + }, + [disabledListValues, policy, reportFieldID], + ); + + useSearchBackPress({ + onClearSelection: () => { + setSelectedValues({}); + }, + onNavigationCallBack: () => Navigation.goBack(), + }); + + const data = useMemo( + () => + listValues.map((value, index) => ({ + value, + index, + text: value, + keyForList: value, + isSelected: selectedValues[value] && canSelectMultiple, + enabled: !disabledListValues.at(index), + rightElement: ( + updateReportFieldListValueEnabled(newValue, index)} + /> + ), + })), + [canSelectMultiple, disabledListValues, listValues, selectedValues, translate, updateReportFieldListValueEnabled], + ); + + const filterListValue = useCallback((item: ValueListItem, searchInput: string) => { + const itemText = StringUtils.normalize(item.text?.toLowerCase() ?? ''); + const normalizedSearchInput = StringUtils.normalize(searchInput.toLowerCase()); + return itemText.includes(normalizedSearchInput); + }, []); + const sortListValues = useCallback((values: ValueListItem[]) => values.sort((a, b) => localeCompare(a.value, b.value)), [localeCompare]); + const [inputValue, setInputValue, filteredListValues] = useSearchResults(data, filterListValue, sortListValues); + + const filteredListValuesArray = filteredListValues.map((item) => item.value); + + const shouldShowEmptyState = Object.values(listValues ?? {}).length <= 0; + const selectedValuesArray = Object.keys(selectedValues).filter((key) => selectedValues[key] && listValues.includes(key)); + + const toggleValue = (valueItem: ValueListItem) => { + setSelectedValues((prev) => ({ + ...prev, + [valueItem.value]: !prev[valueItem.value], + })); + }; + + const toggleAllValues = () => { + setSelectedValues(selectedValuesArray.length > 0 ? {} : Object.fromEntries(filteredListValuesArray.map((value) => [value, true]))); + }; + + const handleDeleteValues = () => { + const valuesToDelete = selectedValuesArray.reduce((acc, valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + + if (index !== -1) { + acc.push(index); + } + + return acc; + }, []); + + if (reportFieldID) { + removeReportFieldListValue({policy, reportFieldID, valueIndexes: valuesToDelete}); + } else { + deleteReportFieldsListValue({ + valueIndexes: valuesToDelete, + listValues, + disabledListValues, + }); + } + + setDeleteValuesConfirmModalVisible(false); + + // eslint-disable-next-line @typescript-eslint/no-deprecated + InteractionManager.runAfterInteractions(() => { + setSelectedValues({}); + }); + }; + + const openListValuePage = (valueItem: ValueListItem) => { + if (valueItem.index === undefined) { + return; + } + + Navigation.navigate(getValueSettingsRoute(shouldUseInvoiceRoutes, policyID, valueItem.index, reportFieldID)); + }; + + const getCustomListHeader = () => { + if (filteredListValues.length === 0) { + return null; + } + return ( + + ); + }; + + const getHeaderButtons = () => { + const options: Array>> = []; + if (isSmallScreenWidth ? isMobileSelectionModeEnabled : selectedValuesArray.length > 0) { + if (selectedValuesArray.length > 0 && !hasAccountingConnections) { + options.push({ + icon: Expensicons.Trashcan, + text: translate(selectedValuesArray.length === 1 ? 'workspace.reportFields.deleteValue' : 'workspace.reportFields.deleteValues'), + value: CONST.POLICY.BULK_ACTION_TYPES.DELETE, + onSelected: () => setDeleteValuesConfirmModalVisible(true), + }); + } + const enabledValues = selectedValuesArray.filter((valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + return !disabledListValues?.at(index); + }); + + if (enabledValues.length > 0) { + const valuesToDisable = selectedValuesArray.reduce((acc, valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + if (!disabledListValues?.at(index) && index !== -1) { + acc.push(index); + } + + return acc; + }, []); + + options.push({ + icon: Expensicons.Close, + text: translate(enabledValues.length === 1 ? 'workspace.reportFields.disableValue' : 'workspace.reportFields.disableValues'), + value: CONST.POLICY.BULK_ACTION_TYPES.DISABLE, + onSelected: () => { + setSelectedValues({}); + + if (reportFieldID) { + updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: valuesToDisable, enabled: false}); + return; + } + + setReportFieldsListValueEnabled({ + valueIndexes: valuesToDisable, + enabled: false, + disabledListValues, + }); + }, + }); + } + + const disabledValues = selectedValuesArray.filter((valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + return disabledListValues?.at(index); + }); + + if (disabledValues.length > 0) { + const valuesToEnable = selectedValuesArray.reduce((acc, valueName) => { + const index = listValues?.indexOf(valueName) ?? -1; + if (disabledListValues?.at(index) && index !== -1) { + acc.push(index); + } + + return acc; + }, []); + + options.push({ + icon: Expensicons.Checkmark, + text: translate(disabledValues.length === 1 ? 'workspace.reportFields.enableValue' : 'workspace.reportFields.enableValues'), + value: CONST.POLICY.BULK_ACTION_TYPES.ENABLE, + onSelected: () => { + setSelectedValues({}); + + if (reportFieldID) { + updateReportFieldListValueEnabledReportField({policy, reportFieldID, valueIndexes: valuesToEnable, enabled: true}); + return; + } + + setReportFieldsListValueEnabled({ + valueIndexes: valuesToEnable, + enabled: true, + disabledListValues, + }); + }, + }); + } + + return ( + null} + shouldAlwaysShowDropdownMenu + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} + customText={translate('workspace.common.selected', {count: selectedValuesArray.length})} + options={options} + isSplitButton={false} + style={[isSmallScreenWidth && styles.flexGrow1, isSmallScreenWidth && styles.mb3]} + isDisabled={!selectedValuesArray.length} + /> + ); + } + + if (!hasAccountingConnections) { + return ( +