diff --git a/src/CONST.ts b/src/CONST.ts index c00b33be1c31..546a5db4fd3d 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -5752,6 +5752,7 @@ const CONST = { ICON_HEIGHT: 160, CATEGORIES_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-categories#import-custom-categories', + TAGS_ARTICLE_LINK: 'https://help.expensify.com/articles/expensify-classic/workspaces/Create-tags#import-a-spreadsheet-1', }, } as const; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 27504998c49c..768c33cd46f3 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -863,6 +863,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/tag/:orderWeight/:tagName/gl-code', getRoute: (policyID: string, orderWeight: number, tagName: string) => `settings/workspaces/${policyID}/tag/${orderWeight}/${encodeURIComponent(tagName)}/gl-code` as const, }, + WORKSPACE_TAGS_IMPORT: { + route: 'settings/workspaces/:policyID/tags/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/import` as const, + }, + WORKSPACE_TAGS_IMPORTED: { + route: 'settings/workspaces/:policyID/tags/imported', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/tags/imported` as const, + }, WORKSPACE_TAXES: { route: 'settings/workspaces/:policyID/taxes', getRoute: (policyID: string) => `settings/workspaces/${policyID}/taxes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 8168afba89ab..2369e231f519 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -411,6 +411,8 @@ const SCREENS = { TAGS: 'Workspace_Tags', TAGS_SETTINGS: 'Tags_Settings', TAGS_EDIT: 'Tags_Edit', + TAGS_IMPORT: 'Tags_Import', + TAGS_IMPORTED: 'Tags_Imported', TAG_EDIT: 'Tag_Edit', TAXES: 'Workspace_Taxes', REPORT_FIELDS: 'Workspace_ReportFields', diff --git a/src/components/ImportSpreadsheetColumns.tsx b/src/components/ImportSpreadsheetColumns.tsx index a38e3c636e86..95a6ea4516ac 100644 --- a/src/components/ImportSpreadsheetColumns.tsx +++ b/src/components/ImportSpreadsheetColumns.tsx @@ -36,14 +36,11 @@ type ImportSpreadsheetColumnsProps = { // An optional boolean indicating whether the import button is in a loading state. isButtonLoading?: boolean; - // A string representing the header text to be rendered. - headerText: string; - // Link to learn more about the file preparation for import. learnMoreLink?: string; }; -function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, headerText, learnMoreLink}: ImportSpreadsheetColumnsProps) { +function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles, errors, importFunction, isButtonLoading, learnMoreLink}: ImportSpreadsheetColumnsProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -55,7 +52,7 @@ function ImportSpreeadsheetColumns({spreadsheetColumns, columnNames, columnRoles - {headerText} + {translate('spreadsheet.importDescription')} {` ${translate('common.learnMore')}`} diff --git a/src/languages/en.ts b/src/languages/en.ts index f36db113a2aa..3f24c5fe8b1e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -687,8 +687,10 @@ export default { singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`, importSuccessfullTitle: 'Import successful', importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'), + importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'), importFailedTitle: 'Import failed', importFailedDescription: 'Please ensure all fields are filled out correctly and try again. If the problem persists, please reach out to Concierge.', + importDescription: 'Choose which fields to map from your spreadsheet by clicking the dropdown next to each imported column below.', sizeNotMet: 'File size must be greater than 0 bytes', invalidFileMessage: 'The file you uploaded is either empty or contains invalid data. Please ensure that the file is correctly formatted and contains the necessary information before uploading it again.', @@ -2968,7 +2970,6 @@ export default { glCode: 'GL code', updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', importCategories: 'Import categories', - importedCategoriesMessage: 'Choose which fields to map from your spreadsheet by clicking the dropdown next to each imported column below.', }, moreFeatures: { subtitle: 'Use the toggles below to enable more features as you grow. Each feature will appear in the navigation menu for further customization.', @@ -3181,6 +3182,9 @@ export default { importedFromAccountingSoftware: 'The tags below are imported from your', glCode: 'GL code', updateGLCodeFailureMessage: 'An error occurred while updating the GL code, please try again.', + importTags: 'Import tags', + importedTagsMessage: (columnCounts: number) => + `We found *${columnCounts} columns* in your spreadsheet. Select *Name* next to the column that contains tags names. You can also select *Enabled* next to the column that sets tags status.`, }, taxes: { subtitle: 'Add tax names, rates, and set defaults.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 8a18c6c002e0..3eaedeace7bf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -681,7 +681,9 @@ export default { importFailedTitle: 'Fallo en la importación', importFailedDescription: 'Por favor, asegúrate de que todos los campos estén llenos correctamente e inténtalo de nuevo. Si el problema persiste, por favor contacta a Concierge.', importCategoriesSuccessfullDescription: (categories: number) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'), + importTagsSuccessfullDescription: (tags: number) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'), importSuccessfullTitle: 'Importar categorías', + importDescription: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.', sizeNotMet: 'El archivo adjunto debe ser más grande que 0 bytes.', invalidFileMessage: 'El archivo que subiste está vacío o contiene datos no válidos. Asegúrate de que el archivo esté correctamente formateado y contenga la información necesaria antes de volver a subirlo.', @@ -3016,7 +3018,6 @@ export default { glCode: 'Código de Libro Mayor', updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Inténtelo nuevamente.', importCategories: 'Importar categorías', - importedCategoriesMessage: 'Elige qué campos mapear desde tu hoja de cálculo haciendo clic en el menú desplegable junto a cada columna importada a continuación.', }, moreFeatures: { subtitle: 'Utiliza los botones de abajo para activar más funciones a medida que creces. Cada función aparecerá en el menú de navegación para una mayor personalización.', @@ -3230,6 +3231,9 @@ export default { importedFromAccountingSoftware: 'Etiquetas importadas desde', glCode: 'Código de Libro Mayor', updateGLCodeFailureMessage: 'Se produjo un error al actualizar el código de Libro Mayor. Por favor, inténtelo nuevamente.', + importTags: 'Importar categorías', + importedTagsMessage: (columnCounts: number) => + `Hemos encontrado *${columnCounts} columnas* en su hoja de cálculo. Seleccione *Nombre* junto a la columna que contiene los nombres de las etiquetas. También puede seleccionar *Habilitado* junto a la columna que establece el estado de la etiqueta.`, }, taxes: { subtitle: 'Añade nombres, tasas y establezca valores por defecto para los impuestos.', diff --git a/src/libs/API/parameters/ExportTagsSpreadsheet.ts b/src/libs/API/parameters/ExportTagsSpreadsheet.ts new file mode 100644 index 000000000000..fc4e78968511 --- /dev/null +++ b/src/libs/API/parameters/ExportTagsSpreadsheet.ts @@ -0,0 +1,6 @@ +type ExportTagsSpreadsheetParams = { + /** ID of the policy */ + policyID: string; +}; + +export default ExportTagsSpreadsheetParams; diff --git a/src/libs/API/parameters/ImportTagsSpreadsheet.ts b/src/libs/API/parameters/ImportTagsSpreadsheet.ts new file mode 100644 index 000000000000..9b35b6646496 --- /dev/null +++ b/src/libs/API/parameters/ImportTagsSpreadsheet.ts @@ -0,0 +1,12 @@ +type ImportTagsSpreadsheetParams = { + /** ID of the policy to which the tags will be imported */ + policyID: string; + + /** + * Stringified JSON object with type of following structure: + * Array<{name: string, enabled: boolean, 'GL Code': string}> + */ + tags: string; +}; + +export default ImportTagsSpreadsheetParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index c780003c3e3f..97a5ddadf7c9 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -311,7 +311,9 @@ export type {default as SetPolicyCategoryReceiptsRequiredParams} from './SetPoli export type {default as RemovePolicyCategoryReceiptsRequiredParams} from './RemovePolicyCategoryReceiptsRequiredParams'; export type {default as UpdateQuickbooksOnlineAutoCreateVendorParams} from './UpdateQuickbooksOnlineAutoCreateVendorParams'; export type {default as ImportCategoriesSpreadsheetParams} from './ImportCategoriesSpreadsheet'; +export type {default as ImportTagsSpreadsheetParams} from './ImportTagsSpreadsheet'; export type {default as ExportCategoriesSpreadsheetParams} from './ExportCategoriesSpreadsheet'; +export type {default as ExportTagsSpreadsheetParams} from './ExportTagsSpreadsheet'; export type {default as UpdateXeroGenericTypeParams} from './UpdateXeroGenericTypeParams'; export type {default as UpdateCardSettlementFrequencyParams} from './UpdateCardSettlementFrequencyParams'; export type {default as UpdateCardSettlementAccountParams} from './UpdateCardSettlementAccountParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index 81b748ce6edb..4ccbce269948 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -142,8 +142,10 @@ const WRITE_COMMANDS = { SET_WORKSPACE_CATEGORIES_ENABLED: 'SetWorkspaceCategoriesEnabled', SET_POLICY_TAGS_ENABLED: 'SetPolicyTagsEnabled', CREATE_WORKSPACE_CATEGORIES: 'CreateWorkspaceCategories', + IMPORT_TAGS_SREADSHEET: 'ImportTagsSpreadsheet', IMPORT_CATEGORIES_SPREADSHEET: 'ImportCategoriesSpreadsheet', EXPORT_CATEGORIES_CSV: 'ExportCategoriesCSV', + EXPORT_TAGS_CSV: 'ExportTagsCSV', RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory', CREATE_POLICY_TAG: 'CreatePolicyTag', RENAME_POLICY_TAG: 'RenamePolicyTag', @@ -524,8 +526,10 @@ type WriteCommandParameters = { [WRITE_COMMANDS.CREATE_WORKSPACE_FROM_IOU_PAYMENT]: Parameters.CreateWorkspaceFromIOUPaymentParams; [WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams; [WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams; + [WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET]: Parameters.ImportTagsSpreadsheetParams; [WRITE_COMMANDS.IMPORT_CATEGORIES_SPREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams; [WRITE_COMMANDS.EXPORT_CATEGORIES_CSV]: Parameters.ExportCategoriesSpreadsheetParams; + [WRITE_COMMANDS.EXPORT_TAGS_CSV]: Parameters.ExportTagsSpreadsheetParams; [WRITE_COMMANDS.RENAME_WORKSPACE_CATEGORY]: Parameters.RenameWorkspaceCategoriesParams; [WRITE_COMMANDS.SET_WORKSPACE_REQUIRES_CATEGORY]: Parameters.SetWorkspaceRequiresCategoryParams; [WRITE_COMMANDS.DELETE_WORKSPACE_CATEGORIES]: Parameters.DeleteWorkspaceCategoriesParams; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index b41b58530a6b..b4b676bbdec5 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -261,6 +261,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxReclaimableEditPage').default, [SCREENS.WORKSPACE.DISTANCE_RATE_TAX_RATE_EDIT]: () => require('../../../../pages/workspace/distanceRates/PolicyDistanceRateTaxRateEditPage').default, + [SCREENS.WORKSPACE.TAGS_IMPORT]: () => require('../../../../pages/workspace/tags/ImportTagsPage').default, + [SCREENS.WORKSPACE.TAGS_IMPORTED]: () => require('../../../../pages/workspace/tags/ImportedTagsPage').default, [SCREENS.WORKSPACE.TAGS_SETTINGS]: () => require('../../../../pages/workspace/tags/WorkspaceTagsSettingsPage').default, [SCREENS.WORKSPACE.TAG_SETTINGS]: () => require('../../../../pages/workspace/tags/TagSettingsPage').default, [SCREENS.WORKSPACE.TAG_LIST_VIEW]: () => require('../../../../pages/workspace/tags/WorkspaceViewTagsPage').default, diff --git a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts index a42427cf0f60..bc86a2d77388 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -133,6 +133,8 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.TAG_EDIT, SCREENS.WORKSPACE.TAG_LIST_VIEW, SCREENS.WORKSPACE.TAG_GL_CODE, + SCREENS.WORKSPACE.TAGS_IMPORT, + SCREENS.WORKSPACE.TAGS_IMPORTED, ], [SCREENS.WORKSPACE.CATEGORIES]: [ SCREENS.WORKSPACE.CATEGORY_CREATE, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 2ca2db10a1a7..d2e6191349bd 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -699,6 +699,12 @@ const config: LinkingOptions['config'] = { orderWeight: Number, }, }, + [SCREENS.WORKSPACE.TAGS_IMPORT]: { + path: ROUTES.WORKSPACE_TAGS_IMPORT.route, + }, + [SCREENS.WORKSPACE.TAGS_IMPORTED]: { + path: ROUTES.WORKSPACE_TAGS_IMPORTED.route, + }, [SCREENS.WORKSPACE.TAG_CREATE]: { path: ROUTES.WORKSPACE_TAG_CREATE.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index c6a4ce90c214..9f76814740d3 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -276,6 +276,12 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.TAGS_SETTINGS]: { policyID: string; }; + [SCREENS.WORKSPACE.TAGS_IMPORT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.TAGS_IMPORTED]: { + policyID: string; + }; [SCREENS.WORKSPACE.TAG_SETTINGS]: { policyID: string; orderWeight: number; diff --git a/src/libs/actions/Policy/Category.ts b/src/libs/actions/Policy/Category.ts index 5716eed8947d..2e11143ca00d 100644 --- a/src/libs/actions/Policy/Category.ts +++ b/src/libs/actions/Policy/Category.ts @@ -1006,12 +1006,14 @@ function downloadCategoriesCSV(policyID: string) { policyID, }); + const fileName = 'Categories.csv'; + const formData = new FormData(); Object.entries(finalParameters).forEach(([key, value]) => { formData.append(key, String(value)); }); - fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), 'Categories.csv', '', false, formData, CONST.NETWORK.METHOD.POST); + fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_CATEGORIES_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); } function setWorkspaceCategoryDescriptionHint(policyID: string, categoryName: string, commentHint: string) { diff --git a/src/libs/actions/Policy/Tag.ts b/src/libs/actions/Policy/Tag.ts index 49a285c12bbe..6455e7fad947 100644 --- a/src/libs/actions/Policy/Tag.ts +++ b/src/libs/actions/Policy/Tag.ts @@ -11,9 +11,13 @@ import type { UpdatePolicyTagGLCodeParams, } from '@libs/API/parameters'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ApiUtils from '@libs/ApiUtils'; import * as ErrorUtils from '@libs/ErrorUtils'; +import fileDownload from '@libs/fileDownload'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {translateLocal} from '@libs/Localize'; import Log from '@libs/Log'; +import enhanceParameters from '@libs/Network/enhanceParameters'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; @@ -122,6 +126,34 @@ function buildOptimisticPolicyRecentlyUsedTags(policyID?: string, transactionTag return newOptimisticPolicyRecentlyUsedTags; } +function updateImportSpreadsheetData(tagsLength: number): OnyxData { + const onyxData: OnyxData = { + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: {title: translateLocal('spreadsheet.importSuccessfullTitle'), prompt: translateLocal('spreadsheet.importTagsSuccessfullDescription', tagsLength)}, + }, + }, + ], + + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: {title: translateLocal('spreadsheet.importFailedTitle'), prompt: translateLocal('spreadsheet.importFailedDescription')}, + }, + }, + ], + }; + + return onyxData; +} + function createPolicyTag(policyID: string, tagName: string) { const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[0] ?? {}; const newTagName = PolicyUtils.escapeTagName(tagName); @@ -187,6 +219,18 @@ function createPolicyTag(policyID: string, tagName: string) { API.write(WRITE_COMMANDS.CREATE_POLICY_TAG, parameters, onyxData); } +function importPolicyTags(policyID: string, tags: PolicyTag[]) { + const onyxData = updateImportSpreadsheetData(tags.length); + + const parameters = { + policyID, + // eslint-disable-next-line @typescript-eslint/naming-convention + tags: JSON.stringify(tags.map((tag) => ({name: tag.name, enabled: tag.enabled, 'GL Code': tag['GL Code']}))), + }; + + API.write(WRITE_COMMANDS.IMPORT_TAGS_SREADSHEET, parameters, onyxData); +} + function setWorkspaceTagEnabled(policyID: string, tagsToUpdate: Record, tagListIndex: number) { const policyTag = PolicyUtils.getTagLists(allPolicyTags?.[`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`] ?? {})?.[tagListIndex] ?? {}; @@ -846,6 +890,20 @@ function setPolicyTagGLCode(policyID: string, tagName: string, tagListIndex: num API.write(WRITE_COMMANDS.UPDATE_POLICY_TAG_GL_CODE, parameters, onyxData); } +function downloadTagsCSV(policyID: string) { + const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_TAGS_CSV, { + policyID, + }); + const fileName = 'Tags.csv'; + + const formData = new FormData(); + Object.entries(finalParameters).forEach(([key, value]) => { + formData.append(key, String(value)); + }); + + fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_TAGS_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST); +} + export { buildOptimisticPolicyRecentlyUsedTags, setPolicyRequiresTag, @@ -861,6 +919,8 @@ export { renamePolicyTaglist, setWorkspaceTagEnabled, setPolicyTagGLCode, + importPolicyTags, + downloadTagsCSV, }; export type {NewCustomUnit}; diff --git a/src/pages/workspace/categories/ImportedCategoriesPage.tsx b/src/pages/workspace/categories/ImportedCategoriesPage.tsx index 6a8d8895aaaa..0bdc587b325c 100644 --- a/src/pages/workspace/categories/ImportedCategoriesPage.tsx +++ b/src/pages/workspace/categories/ImportedCategoriesPage.tsx @@ -32,8 +32,6 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) { const policy = usePolicy(policyID); const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0); - const isControl = isControlPolicy(policy); - const getColumnRoles = (): ColumnRole[] => { const roles = []; roles.push( @@ -42,7 +40,7 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) { {text: translate('common.enabled'), value: CONST.CSV_IMPORT_COLUMNS.ENABLED, isRequired: true}, ); - if (isControl) { + if (isControlPolicy(policy)) { roles.push({text: translate('workspace.categories.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE}); } @@ -134,7 +132,6 @@ function ImportedCategoriesPage({route}: ImportedCategoriesPageProps) { errors={isValidationEnabled ? validate() : undefined} columnRoles={columnRoles} isButtonLoading={isImportingCategories} - headerText={translate('workspace.categories.importedCategoriesMessage')} learnMoreLink={CONST.IMPORT_SPREADSHEET.CATEGORIES_ARTICLE_LINK} /> diff --git a/src/pages/workspace/tags/ImportTagsPage.tsx b/src/pages/workspace/tags/ImportTagsPage.tsx new file mode 100644 index 000000000000..0dd7c01f0af3 --- /dev/null +++ b/src/pages/workspace/tags/ImportTagsPage.tsx @@ -0,0 +1,21 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import ImportSpreedsheet from '@components/ImportSpreadsheet'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ImportTagsPageProps = StackScreenProps; + +function ImportTagsPage({route}: ImportTagsPageProps) { + const policyID = route.params.policyID; + + return ( + + ); +} + +export default ImportTagsPage; diff --git a/src/pages/workspace/tags/ImportedTagsPage.tsx b/src/pages/workspace/tags/ImportedTagsPage.tsx new file mode 100644 index 000000000000..210a14f56998 --- /dev/null +++ b/src/pages/workspace/tags/ImportedTagsPage.tsx @@ -0,0 +1,156 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, useMemo, useState} from 'react'; +import {useOnyx} from 'react-native-onyx'; +import ConfirmModal from '@components/ConfirmModal'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import type {ColumnRole} from '@components/ImportColumn'; +import ImportSpreadsheetColumns from '@components/ImportSpreadsheetColumns'; +import ScreenWrapper from '@components/ScreenWrapper'; +import useLocalize from '@hooks/useLocalize'; +import usePolicy from '@hooks/usePolicy'; +import {closeImportPage} from '@libs/actions/ImportSpreadsheet'; +import {importPolicyTags} from '@libs/actions/Policy/Tag'; +import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import {isControlPolicy} from '@libs/PolicyUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +type ImportedTagsPageProps = StackScreenProps; + +function ImportedTagsPage({route}: ImportedTagsPageProps) { + const {translate} = useLocalize(); + const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); + const [isImportingTags, setIsImportingTags] = useState(false); + const {containsHeader = true} = spreadsheet ?? {}; + const [isValidationEnabled, setIsValidationEnabled] = useState(false); + const policyID = route.params.policyID; + const [policyTags] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); + const policy = usePolicy(policyID); + const columnNames = generateColumnNames(spreadsheet?.data?.length ?? 0); + + const getColumnRoles = (): ColumnRole[] => { + const roles = []; + roles.push( + {text: translate('common.ignore'), value: CONST.CSV_IMPORT_COLUMNS.IGNORE}, + {text: translate('common.name'), value: CONST.CSV_IMPORT_COLUMNS.NAME, isRequired: true}, + {text: translate('common.enabled'), value: CONST.CSV_IMPORT_COLUMNS.ENABLED, isRequired: true}, + ); + + if (isControlPolicy(policy)) { + roles.push({text: translate('workspace.tags.glCode'), value: CONST.CSV_IMPORT_COLUMNS.GL_CODE}); + } + + return roles; + }; + + const columnRoles = getColumnRoles(); + + const requiredColumns = columnRoles.filter((role) => role.isRequired).map((role) => role); + + const validate = useCallback(() => { + const columns = Object.values(spreadsheet?.columns ?? {}); + let errors: Errors = {}; + + if (!requiredColumns.every((requiredColumn) => columns.includes(requiredColumn.value))) { + // eslint-disable-next-line rulesdir/prefer-early-return + requiredColumns.forEach((requiredColumn) => { + if (!columns.includes(requiredColumn.value)) { + errors.required = translate('spreadsheet.fieldNotMapped', requiredColumn.text); + } + }); + } else { + const duplicate = findDuplicate(columns); + if (duplicate) { + errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', duplicate); + } else { + errors = {}; + } + } + return errors; + }, [requiredColumns, spreadsheet?.columns, translate]); + + const importTags = useCallback(() => { + setIsValidationEnabled(true); + const errors = validate(); + if (Object.keys(errors).length > 0) { + return; + } + + const columns = Object.values(spreadsheet?.columns ?? {}); + const tagsNamesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.NAME); + const tagsGLCodeColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.GL_CODE); + const tagsEnabledColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ENABLED); + const tagsNames = spreadsheet?.data[tagsNamesColumn].map((name) => name); + const tagsEnabled = tagsEnabledColumn !== -1 ? spreadsheet?.data[tagsEnabledColumn].map((enabled) => enabled) : []; + const tagsGLCode = tagsGLCodeColumn !== -1 ? spreadsheet?.data[tagsGLCodeColumn].map((glCode) => glCode) : []; + const tags = tagsNames?.slice(containsHeader ? 1 : 0).map((name, index) => { + // Right now we support only single-level tags, this check should be updated when we add multi-level support + const tagAlreadyExists = policyTagLists[0]?.tags?.[name]; + const existingGLCodeOrDefault = tagAlreadyExists?.['GL Code'] ?? ''; + return { + name, + enabled: tagsEnabledColumn !== -1 ? tagsEnabled?.[containsHeader ? index + 1 : index] === 'true' : true, + // eslint-disable-next-line @typescript-eslint/naming-convention + 'GL Code': tagsGLCodeColumn !== -1 ? tagsGLCode?.[containsHeader ? index + 1 : index] ?? '' : existingGLCodeOrDefault, + }; + }); + + if (tags) { + setIsImportingTags(true); + importPolicyTags(policyID, tags); + } + }, [validate, spreadsheet, containsHeader, policyTagLists, policyID]); + + const spreadsheetColumns = spreadsheet?.data; + if (!spreadsheetColumns) { + return; + } + + const closeImportPageAndModal = () => { + setIsImportingTags(false); + closeImportPage(); + Navigation.navigate(ROUTES.WORKSPACE_TAGS.getRoute(policyID)); + }; + + return ( + + Navigation.goBack(ROUTES.WORKSPACE_TAGS_IMPORT.getRoute(policyID))} + /> + + + + + ); +} + +ImportedTagsPage.displayName = 'ImportedTagsPage'; + +export default ImportedTagsPage; diff --git a/src/pages/workspace/tags/WorkspaceTagsPage.tsx b/src/pages/workspace/tags/WorkspaceTagsPage.tsx index 9eb7a594bdb5..84722a1f0b5b 100644 --- a/src/pages/workspace/tags/WorkspaceTagsPage.tsx +++ b/src/pages/workspace/tags/WorkspaceTagsPage.tsx @@ -12,6 +12,7 @@ import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; import * as Illustrations from '@components/Icon/Illustrations'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import ScreenWrapper from '@components/ScreenWrapper'; import ListItemRightCaretWithLabel from '@components/SelectionList/ListItemRightCaretWithLabel'; import TableListItem from '@components/SelectionList/TableListItem'; @@ -27,6 +28,7 @@ import usePolicy from '@hooks/usePolicy'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import localeCompare from '@libs/LocaleCompare'; @@ -34,6 +36,7 @@ import Navigation from '@libs/Navigation/Navigation'; import type {FullScreenNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import * as Modal from '@userActions/Modal'; import * as Tag from '@userActions/Policy/Tag'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -50,8 +53,10 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); + const {windowWidth} = useWindowDimensions(); const [selectedTags, setSelectedTags] = useState>({}); const [isDeleteTagsConfirmModalVisible, setIsDeleteTagsConfirmModalVisible] = useState(false); + const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const isFocused = useIsFocused(); const policyID = route.params.policyID ?? '-1'; const policy = usePolicy(policyID); @@ -194,7 +199,7 @@ function WorkspaceTagsPage({route}: WorkspaceTagsPageProps) { if (shouldUseNarrowLayout ? !selectionMode?.isEnabled : selectedTagsArray.length === 0) { return ( - + {!hasAccountingConnections && !isMultiLevelTags && (