diff --git a/src/CONST.ts b/src/CONST.ts index 76d09c01140c..843034b334af 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -6290,6 +6290,7 @@ const CONST = { RATE_ID: 'rateID', ENABLED: 'enabled', IGNORE: 'ignore', + DESTINATION: 'destination', }, IMPORT_SPREADSHEET: { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 6eafb3a02650..45a6345622fc 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1308,6 +1308,14 @@ const ROUTES = { route: 'settings/workspaces/:policyID/per-diem', getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem` as const, }, + WORKSPACE_PER_DIEM_IMPORT: { + route: 'settings/workspaces/:policyID/per-diem/import', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/import` as const, + }, + WORKSPACE_PER_DIEM_IMPORTED: { + route: 'settings/workspaces/:policyID/per-diem/imported', + getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/imported` as const, + }, WORKSPACE_PER_DIEM_SETTINGS: { route: 'settings/workspaces/:policyID/per-diem/settings', getRoute: (policyID: string) => `settings/workspaces/${policyID}/per-diem/settings` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 0e9c54352c32..1b4ecb1ea1c8 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -548,6 +548,8 @@ const SCREENS = { RULES_MAX_EXPENSE_AGE: 'Rules_Max_Expense_Age', RULES_BILLABLE_DEFAULT: 'Rules_Billable_Default', PER_DIEM: 'Per_Diem', + PER_DIEM_IMPORT: 'Per_Diem_Import', + PER_DIEM_IMPORTED: 'Per_Diem_Imported', PER_DIEM_SETTINGS: 'Per_Diem_Settings', }, diff --git a/src/components/ImportColumn.tsx b/src/components/ImportColumn.tsx index d3f5eb75f7e8..0e1ae935a2b7 100644 --- a/src/components/ImportColumn.tsx +++ b/src/components/ImportColumn.tsx @@ -87,7 +87,7 @@ function findColumnName(header: string): string { break; case 'destination': - attribute = CONST.CSV_IMPORT_COLUMNS.NAME; + attribute = CONST.CSV_IMPORT_COLUMNS.DESTINATION; break; case 'subrate': diff --git a/src/components/ImportSpreadsheet.tsx b/src/components/ImportSpreadsheet.tsx index 6865e8ae6c82..09f8c1d5b925 100644 --- a/src/components/ImportSpreadsheet.tsx +++ b/src/components/ImportSpreadsheet.tsx @@ -33,7 +33,7 @@ type ImportSpreedsheetProps = { goTo: Routes; }; -function ImportSpreedsheet({backTo, goTo}: ImportSpreedsheetProps) { +function ImportSpreadsheet({backTo, goTo}: ImportSpreedsheetProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isReadingFile, setIsReadingFIle] = useState(false); @@ -160,7 +160,7 @@ function ImportSpreedsheet({backTo, goTo}: ImportSpreedsheetProps) { @@ -214,6 +214,6 @@ function ImportSpreedsheet({backTo, goTo}: ImportSpreedsheetProps) { ); } -ImportSpreedsheet.displayName = 'ImportSpreedsheet'; +ImportSpreadsheet.displayName = 'ImportSpreadsheet'; -export default ImportSpreedsheet; +export default ImportSpreadsheet; diff --git a/src/languages/en.ts b/src/languages/en.ts index 855854c58dba..54cff7b78575 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -76,6 +76,7 @@ import type { ImportedTypesParams, ImportFieldParams, ImportMembersSuccessfullDescriptionParams, + ImportPerDiemRatesSuccessfullDescriptionParams, ImportTagsSuccessfullDescriptionParams, IncorrectZipFormatParams, InstantSummaryParams, @@ -777,6 +778,8 @@ const translations = { importCategoriesSuccessfullDescription: ({categories}: SpreadCategoriesParams) => (categories > 1 ? `${categories} categories have been added.` : '1 category has been added.'), importMembersSuccessfullDescription: ({members}: ImportMembersSuccessfullDescriptionParams) => (members > 1 ? `${members} members have been added.` : '1 member has been added.'), importTagsSuccessfullDescription: ({tags}: ImportTagsSuccessfullDescriptionParams) => (tags > 1 ? `${tags} tags have been added.` : '1 tag has been added.'), + importPerDiemRatesSuccessfullDescription: ({rates}: ImportPerDiemRatesSuccessfullDescriptionParams) => + rates > 1 ? `${rates} per diem rates have been added.` : '1 per diem rate 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.', @@ -2558,6 +2561,7 @@ const translations = { title: 'Per diem', subtitle: 'Set per diem rates to control daily employee spend. Import rates from a spreadsheet to get started.', }, + importPerDiemRates: 'Import per diem rates', }, qbd: { exportOutOfPocketExpensesDescription: 'Set how out-of-pocket expenses export to QuickBooks Desktop.', diff --git a/src/languages/es.ts b/src/languages/es.ts index 57ff99f80b6b..b40a65c6eb6e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -75,6 +75,7 @@ import type { ImportedTypesParams, ImportFieldParams, ImportMembersSuccessfullDescriptionParams, + ImportPerDiemRatesSuccessfullDescriptionParams, ImportTagsSuccessfullDescriptionParams, IncorrectZipFormatParams, InstantSummaryParams, @@ -773,6 +774,8 @@ const translations = { importCategoriesSuccessfullDescription: ({categories}: SpreadCategoriesParams) => (categories > 1 ? `Se han agregado ${categories} categorías.` : 'Se ha agregado 1 categoría.'), importMembersSuccessfullDescription: ({members}: ImportMembersSuccessfullDescriptionParams) => (members > 1 ? `Se han agregado ${members} miembros.` : 'Se ha agregado 1 miembro.'), importTagsSuccessfullDescription: ({tags}: ImportTagsSuccessfullDescriptionParams) => (tags > 1 ? `Se han agregado ${tags} etiquetas.` : 'Se ha agregado 1 etiqueta.'), + importPerDiemRatesSuccessfullDescription: ({rates}: ImportPerDiemRatesSuccessfullDescriptionParams) => + rates > 1 ? `Se han añadido ${rates} tasas de per diem.` : 'Se ha añadido 1 tasa de per diem.', 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.', @@ -2582,6 +2585,7 @@ const translations = { title: 'Per diem', subtitle: 'Establece dietas per diem para controlar el gasto diario de los empleados. Importa las tarifas desde una hoja de cálculo para comenzar.', }, + importPerDiemRates: 'Importar tasas de per diem', }, qbd: { exportOutOfPocketExpensesDescription: 'Establezca cómo se exportan los gastos de bolsillo a QuickBooks Desktop.', diff --git a/src/languages/params.ts b/src/languages/params.ts index a2d5e1bee124..a4307fa0a32f 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -531,6 +531,10 @@ type ImportMembersSuccessfullDescriptionParams = { members: number; }; +type ImportPerDiemRatesSuccessfullDescriptionParams = { + rates: number; +}; + type AuthenticationErrorParams = { connectionName: string; }; @@ -767,6 +771,7 @@ export type { OptionalParam, AssignCardParams, ImportedTypesParams, + ImportPerDiemRatesSuccessfullDescriptionParams, CurrencyCodeParams, WorkspaceLockedPlanTypeParams, CompanyNameParams, diff --git a/src/libs/API/parameters/ExportPerDiemCSVParams.ts b/src/libs/API/parameters/ExportPerDiemCSVParams.ts new file mode 100644 index 000000000000..553621534df5 --- /dev/null +++ b/src/libs/API/parameters/ExportPerDiemCSVParams.ts @@ -0,0 +1,6 @@ +type ExportPerDiemCSVParams = { + /** ID of the policy */ + policyID: string; +}; + +export default ExportPerDiemCSVParams; diff --git a/src/libs/API/parameters/ImportPerDiemRatesParams.ts b/src/libs/API/parameters/ImportPerDiemRatesParams.ts new file mode 100644 index 000000000000..b67517150f2d --- /dev/null +++ b/src/libs/API/parameters/ImportPerDiemRatesParams.ts @@ -0,0 +1,26 @@ +type ImportPerDiemRatesParams = { + /** ID of the policy */ + policyID: string; + + /** Custom Unit ID of the per diem unit */ + customUnitID: string; + + /** + * Stringified JSON object with type of following structure: + * Array<{ + * customUnitRateID: ; + * name: “Spain”; + * currency: “EUR”; + * enabled: true (since we are importing); + * rate: 0 (since we have subrates); + * Attributes: []; + * subRates: An array with each element in the following shape: + * id: “66d5ae9a0379d”; + * name: “Full Day”; + * rate: 3000 (in cents); + * }> + */ + customUnitRates: string; +}; + +export default ImportPerDiemRatesParams; diff --git a/src/libs/API/parameters/index.ts b/src/libs/API/parameters/index.ts index 61b0c68f874f..6a510d074f98 100644 --- a/src/libs/API/parameters/index.ts +++ b/src/libs/API/parameters/index.ts @@ -350,3 +350,5 @@ export type {default as UpdateQuickbooksDesktopCompanyCardExpenseAccountTypePara export type {default as TogglePolicyPerDiemParams} from './TogglePolicyPerDiemParams'; export type {default as OpenPolicyPerDiemRatesPageParams} from './OpenPolicyPerDiemRatesPageParams'; export type {default as TogglePlatformMuteParams} from './TogglePlatformMuteParams'; +export type {default as ImportPerDiemRatesParams} from './ImportPerDiemRatesParams'; +export type {default as ExportPerDiemCSVParams} from './ExportPerDiemCSVParams'; diff --git a/src/libs/API/types.ts b/src/libs/API/types.ts index d7258f1dd49e..d31da53304f6 100644 --- a/src/libs/API/types.ts +++ b/src/libs/API/types.ts @@ -148,9 +148,11 @@ const WRITE_COMMANDS = { IMPORT_CATEGORIES_SPREADSHEET: 'ImportCategoriesSpreadsheet', IMPORT_MEMBERS_SPREADSHEET: 'ImportMembersSpreadsheet', IMPORT_TAGS_SPREADSHEET: 'ImportTagsSpreadsheet', + IMPORT_PER_DIEM_RATES: 'ImportPerDiemRates', EXPORT_CATEGORIES_CSV: 'ExportCategoriesCSV', EXPORT_MEMBERS_CSV: 'ExportMembersCSV', EXPORT_TAGS_CSV: 'ExportTagsCSV', + EXPORT_PER_DIEM_CSV: 'ExportPerDiemCSV', EXPORT_REPORT_TO_CSV: 'ExportReportToCSV', RENAME_WORKSPACE_CATEGORY: 'RenameWorkspaceCategory', CREATE_POLICY_TAG: 'CreatePolicyTag', @@ -571,11 +573,13 @@ type WriteCommandParameters = { [WRITE_COMMANDS.SET_WORKSPACE_CATEGORIES_ENABLED]: Parameters.SetWorkspaceCategoriesEnabledParams; [WRITE_COMMANDS.CREATE_WORKSPACE_CATEGORIES]: Parameters.CreateWorkspaceCategoriesParams; [WRITE_COMMANDS.IMPORT_CATEGORIES_SPREADSHEET]: Parameters.ImportCategoriesSpreadsheetParams; + [WRITE_COMMANDS.IMPORT_PER_DIEM_RATES]: Parameters.ImportPerDiemRatesParams; [WRITE_COMMANDS.IMPORT_MEMBERS_SPREADSHEET]: Parameters.ImportMembersSpreadsheetParams; [WRITE_COMMANDS.IMPORT_TAGS_SPREADSHEET]: Parameters.ImportTagsSpreadsheetParams; [WRITE_COMMANDS.EXPORT_CATEGORIES_CSV]: Parameters.ExportCategoriesSpreadsheetParams; [WRITE_COMMANDS.EXPORT_MEMBERS_CSV]: Parameters.ExportMembersSpreadsheetParams; [WRITE_COMMANDS.EXPORT_TAGS_CSV]: Parameters.ExportTagsSpreadsheetParams; + [WRITE_COMMANDS.EXPORT_PER_DIEM_CSV]: Parameters.ExportPerDiemCSVParams; [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/CurrencyUtils.ts b/src/libs/CurrencyUtils.ts index b701a32a7c98..26a5d209b0ff 100644 --- a/src/libs/CurrencyUtils.ts +++ b/src/libs/CurrencyUtils.ts @@ -198,6 +198,10 @@ function isValidCurrencyCode(currencyCode: string): boolean { return !!currency; } +function sanitizeCurrencyCode(currencyCode: string): string { + return isValidCurrencyCode(currencyCode) ? currencyCode : CONST.CURRENCY.USD; +} + export { getCurrencyDecimals, getCurrencyUnit, @@ -212,4 +216,5 @@ export { convertToDisplayStringWithoutCurrency, isValidCurrencyCode, convertToShortDisplayString, + sanitizeCurrencyCode, }; diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 64482e692663..41168a686f22 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -572,6 +572,8 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/rules/RulesMaxExpenseAmountPage').default, [SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE]: () => require('../../../../pages/workspace/rules/RulesMaxExpenseAgePage').default, [SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: () => require('../../../../pages/workspace/rules/RulesBillableDefaultPage').default, + [SCREENS.WORKSPACE.PER_DIEM_IMPORT]: () => require('../../../../pages/workspace/perDiem/ImportPerDiemPage').default, + [SCREENS.WORKSPACE.PER_DIEM_IMPORTED]: () => require('../../../../pages/workspace/perDiem/ImportedPerDiemPage').default, [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: () => require('../../../../pages/workspace/perDiem/WorkspacePerDiemSettingsPage').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 108cd86c05d6..4365c5e65e25 100755 --- a/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts +++ b/src/libs/Navigation/linkingConfig/FULL_SCREEN_TO_RHP_MAPPING.ts @@ -246,7 +246,7 @@ const FULL_SCREEN_TO_RHP_MAPPING: Partial> = { SCREENS.WORKSPACE.RULES_MAX_EXPENSE_AGE, SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT, ], - [SCREENS.WORKSPACE.PER_DIEM]: [SCREENS.WORKSPACE.PER_DIEM_SETTINGS], + [SCREENS.WORKSPACE.PER_DIEM]: [SCREENS.WORKSPACE.PER_DIEM_IMPORT, SCREENS.WORKSPACE.PER_DIEM_IMPORTED, SCREENS.WORKSPACE.PER_DIEM_SETTINGS], }; export default FULL_SCREEN_TO_RHP_MAPPING; diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 55382e2d0889..e9f627ee4341 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -947,6 +947,12 @@ const config: LinkingOptions['config'] = { [SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: { path: ROUTES.RULES_BILLABLE_DEFAULT.route, }, + [SCREENS.WORKSPACE.PER_DIEM_IMPORT]: { + path: ROUTES.WORKSPACE_PER_DIEM_IMPORT.route, + }, + [SCREENS.WORKSPACE.PER_DIEM_IMPORTED]: { + path: ROUTES.WORKSPACE_PER_DIEM_IMPORTED.route, + }, [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: { path: ROUTES.WORKSPACE_PER_DIEM_SETTINGS.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 56aaf05ebf51..475784e509bd 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -899,6 +899,12 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.RULES_BILLABLE_DEFAULT]: { policyID: string; }; + [SCREENS.WORKSPACE.PER_DIEM_IMPORT]: { + policyID: string; + }; + [SCREENS.WORKSPACE.PER_DIEM_IMPORTED]: { + policyID: string; + }; [SCREENS.WORKSPACE.PER_DIEM_SETTINGS]: { policyID: string; }; diff --git a/src/libs/actions/Policy/PerDiem.ts b/src/libs/actions/Policy/PerDiem.ts index 9dfb78b11564..81898dfb34e0 100644 --- a/src/libs/actions/Policy/PerDiem.ts +++ b/src/libs/actions/Policy/PerDiem.ts @@ -2,7 +2,11 @@ import type {NullishDeep, OnyxCollection} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import {READ_COMMANDS, WRITE_COMMANDS} from '@libs/API/types'; +import * as ApiUtils from '@libs/ApiUtils'; +import fileDownload from '@libs/fileDownload'; import getIsNarrowLayout from '@libs/getIsNarrowLayout'; +import {translateLocal} from '@libs/Localize'; +import enhanceParameters from '@libs/Network/enhanceParameters'; import * as NumberUtils from '@libs/NumberUtils'; import {navigateWhenEnableFeature} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -10,6 +14,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Policy, Report} from '@src/types/onyx'; import type {ErrorFields} from '@src/types/onyx/OnyxCommon'; +import type {Rate} from '@src/types/onyx/Policy'; import type {OnyxData} from '@src/types/onyx/Request'; const allPolicies: OnyxCollection = {}; @@ -120,6 +125,64 @@ function openPolicyPerDiemPage(policyID?: string) { API.read(READ_COMMANDS.OPEN_POLICY_PER_DIEM_RATES_PAGE, params); } +function updateImportSpreadsheetData(ratesLength: number) { + const onyxData: OnyxData = { + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: { + title: translateLocal('spreadsheet.importSuccessfullTitle'), + prompt: translateLocal('spreadsheet.importPerDiemRatesSuccessfullDescription', {rates: ratesLength}), + }, + }, + }, + ], + + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.IMPORTED_SPREADSHEET, + value: { + shouldFinalModalBeOpened: true, + importFinalModal: {title: translateLocal('spreadsheet.importFailedTitle'), prompt: translateLocal('spreadsheet.importFailedDescription')}, + }, + }, + ], + }; + + return onyxData; +} + +function importPerDiemRates(policyID: string, customUnitID: string, rates: Rate[], rowsLength: number) { + const onyxData = updateImportSpreadsheetData(rowsLength); + + const parameters = { + policyID, + customUnitID, + customUnitRates: JSON.stringify(rates), + }; + + API.write(WRITE_COMMANDS.IMPORT_PER_DIEM_RATES, parameters, onyxData); +} + +function downloadPerDiemCSV(policyID: string, onDownloadFailed: () => void) { + const finalParameters = enhanceParameters(WRITE_COMMANDS.EXPORT_PER_DIEM_CSV, { + policyID, + }); + + const fileName = 'PerDiem.csv'; + + const formData = new FormData(); + Object.entries(finalParameters).forEach(([key, value]) => { + formData.append(key, String(value)); + }); + + fileDownload(ApiUtils.getCommandURL({command: WRITE_COMMANDS.EXPORT_PER_DIEM_CSV}), fileName, '', false, formData, CONST.NETWORK.METHOD.POST, onDownloadFailed); +} + function clearPolicyPerDiemRatesErrorFields(policyID: string, customUnitID: string, updatedErrorFields: ErrorFields) { Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, { customUnits: { @@ -130,4 +193,4 @@ function clearPolicyPerDiemRatesErrorFields(policyID: string, customUnitID: stri }); } -export {enablePerDiem, openPolicyPerDiemPage, clearPolicyPerDiemRatesErrorFields}; +export {generateCustomUnitID, enablePerDiem, openPolicyPerDiemPage, importPerDiemRates, downloadPerDiemCSV, clearPolicyPerDiemRatesErrorFields}; diff --git a/src/pages/workspace/categories/ImportCategoriesPage.tsx b/src/pages/workspace/categories/ImportCategoriesPage.tsx index 930096d0dd1b..aabb91c0012d 100644 --- a/src/pages/workspace/categories/ImportCategoriesPage.tsx +++ b/src/pages/workspace/categories/ImportCategoriesPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import ImportSpreedsheet from '@components/ImportSpreadsheet'; +import ImportSpreadsheet from '@components/ImportSpreadsheet'; import usePolicy from '@hooks/usePolicy'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -30,7 +30,7 @@ function ImportCategoriesPage({route}: ImportCategoriesPageProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} fullPageNotFoundViewProps={{subtitleKey: isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized', onLinkPress: PolicyUtils.goBackFromInvalidPolicy}} > - diff --git a/src/pages/workspace/members/ImportMembersPage.tsx b/src/pages/workspace/members/ImportMembersPage.tsx index 34b55be31981..1bbc1f36a8aa 100644 --- a/src/pages/workspace/members/ImportMembersPage.tsx +++ b/src/pages/workspace/members/ImportMembersPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import ImportSpreedsheet from '@components/ImportSpreadsheet'; +import ImportSpreadsheet from '@components/ImportSpreadsheet'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; @@ -22,7 +22,7 @@ function ImportMembersPage({policy}: ImportMembersPageProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} fullPageNotFoundViewProps={{subtitleKey: isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized', onLinkPress: PolicyUtils.goBackFromInvalidPolicy}} > - diff --git a/src/pages/workspace/perDiem/ImportPerDiemPage.tsx b/src/pages/workspace/perDiem/ImportPerDiemPage.tsx new file mode 100644 index 000000000000..60d7099b1f1f --- /dev/null +++ b/src/pages/workspace/perDiem/ImportPerDiemPage.tsx @@ -0,0 +1,33 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React from 'react'; +import ImportSpreadsheet from '@components/ImportSpreadsheet'; +import usePolicy from '@hooks/usePolicy'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import * as PolicyUtils from '@libs/PolicyUtils'; +import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; +import CONST from '@src/CONST'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; + +type ImportPerDiemPageProps = StackScreenProps; + +function ImportPerDiemPage({route}: ImportPerDiemPageProps) { + const policyID = route.params.policyID; + const policy = usePolicy(policyID); + + return ( + + + + ); +} + +export default ImportPerDiemPage; diff --git a/src/pages/workspace/perDiem/ImportedPerDiemPage.tsx b/src/pages/workspace/perDiem/ImportedPerDiemPage.tsx new file mode 100644 index 000000000000..9a0759b3608a --- /dev/null +++ b/src/pages/workspace/perDiem/ImportedPerDiemPage.tsx @@ -0,0 +1,174 @@ +import type {StackScreenProps} from '@react-navigation/stack'; +import React, {useCallback, 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 * as PerDiem from '@libs/actions/Policy/PerDiem'; +import {sanitizeCurrencyCode} from '@libs/CurrencyUtils'; +import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getPerDiemCustomUnit} 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'; +import type {Rate} from '@src/types/onyx/Policy'; + +function generatePerDiemUnits(perDiemDestination: string[], perDiemSubRate: string[], perDiemCurrency: string[], perDiemAmount: string[]) { + const perDiemUnits: Record = {}; + for (let i = 0; i < perDiemDestination.length; i++) { + perDiemUnits[perDiemDestination[i]] = perDiemUnits[perDiemDestination[i]] ?? { + customUnitRateID: perDiemDestination.at(i), + name: perDiemDestination.at(i), + rate: 0, + currency: perDiemCurrency.at(i), + enabled: true, + attributes: [], + subRates: [], + }; + perDiemUnits[perDiemDestination[i]].subRates?.push({ + id: PerDiem.generateCustomUnitID(), + name: perDiemSubRate.at(i) ?? '', + rate: Number(perDiemAmount.at(i)) ?? 0, + }); + } + return Object.values(perDiemUnits); +} + +type ImportedPerDiemPageProps = StackScreenProps; +function ImportedPerDiemPage({route}: ImportedPerDiemPageProps) { + const {translate} = useLocalize(); + const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET); + const [isImportingPerDiemRates, setIsImportingPerDiemRates] = useState(false); + const {containsHeader = true} = spreadsheet ?? {}; + const [isValidationEnabled, setIsValidationEnabled] = useState(false); + const policyID = route.params.policyID; + const policy = usePolicy(policyID); + const perDiemCustomUnit = getPerDiemCustomUnit(policy); + 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('workspace.perDiem.destination'), value: CONST.CSV_IMPORT_COLUMNS.DESTINATION, isRequired: true}, + {text: translate('workspace.perDiem.subrate'), value: CONST.CSV_IMPORT_COLUMNS.SUBRATE, isRequired: true}, + {text: translate('common.currency'), value: CONST.CSV_IMPORT_COLUMNS.CURRENCY, isRequired: true}, + {text: translate('workspace.perDiem.amount'), value: CONST.CSV_IMPORT_COLUMNS.AMOUNT, isRequired: true}, + ); + + 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', {fieldName: requiredColumn.text}); + } + }); + } else { + const duplicate = findDuplicate(columns); + const duplicateColumn = columnRoles.find((role) => role.value === duplicate); + if (duplicateColumn) { + errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', {fieldName: duplicateColumn.text}); + } else { + errors = {}; + } + } + return errors; + }, [requiredColumns, spreadsheet?.columns, translate, columnRoles]); + + const importPerDiemRates = useCallback(() => { + setIsValidationEnabled(true); + const errors = validate(); + if (Object.keys(errors).length > 0) { + return; + } + + const columns = Object.values(spreadsheet?.columns ?? {}); + const perDiemDestinationColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.DESTINATION); + const perDiemSubRateColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.SUBRATE); + const perDiemCurrencyColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.CURRENCY); + const perDiemAmountColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.AMOUNT); + const perDiemDestination = spreadsheet?.data[perDiemDestinationColumn].map((destination) => destination) ?? []; + const perDiemSubRate = spreadsheet?.data[perDiemSubRateColumn].map((subRate) => subRate) ?? []; + const perDiemCurrency = spreadsheet?.data[perDiemCurrencyColumn].map((currency) => sanitizeCurrencyCode(currency)) ?? []; + const perDiemAmount = spreadsheet?.data[perDiemAmountColumn].map((amount) => amount) ?? []; + const perDiemUnits = generatePerDiemUnits( + perDiemDestination?.slice(containsHeader ? 1 : 0), + perDiemSubRate?.slice(containsHeader ? 1 : 0), + perDiemCurrency?.slice(containsHeader ? 1 : 0), + perDiemAmount?.slice(containsHeader ? 1 : 0), + ); + + const rowsLength = perDiemDestination.length - (containsHeader ? 1 : 0); + + if (perDiemUnits) { + setIsImportingPerDiemRates(true); + PerDiem.importPerDiemRates(policyID, perDiemCustomUnit?.customUnitID ?? '', perDiemUnits, rowsLength); + } + }, [validate, spreadsheet?.columns, spreadsheet?.data, containsHeader, policyID, perDiemCustomUnit?.customUnitID]); + + const spreadsheetColumns = spreadsheet?.data; + if (!spreadsheetColumns) { + return; + } + + const closeImportPageAndModal = () => { + setIsImportingPerDiemRates(false); + closeImportPage(); + Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM.getRoute(policyID)); + }; + + return ( + + Navigation.goBack(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID))} + /> + + + + + ); +} + +ImportedPerDiemPage.displayName = 'ImportedPerDiemPage'; + +export default ImportedPerDiemPage; diff --git a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx index a7b37d5a0c96..ac4185aefcc6 100644 --- a/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx +++ b/src/pages/workspace/perDiem/WorkspacePerDiemPage.tsx @@ -7,6 +7,7 @@ import Button from '@components/Button'; import ButtonWithDropdownMenu from '@components/ButtonWithDropdownMenu'; import type {DropdownOption} from '@components/ButtonWithDropdownMenu/types'; import ConfirmModal from '@components/ConfirmModal'; +import DecisionModal from '@components/DecisionModal'; import EmptyStateComponent from '@components/EmptyStateComponent'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -107,7 +108,9 @@ function generateSingleSubRateData(customUnitRates: Rate[], rateID: string, subR type WorkspacePerDiemPageProps = StackScreenProps; function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { - const {shouldUseNarrowLayout} = useResponsiveLayout(); + // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to apply the correct modal type for the decision modal + // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth + const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); const {windowWidth} = useWindowDimensions(); const styles = useThemeStyles(); const theme = useTheme(); @@ -115,6 +118,7 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [selectedPerDiem, setSelectedPerDiem] = useState([]); const [deletePerDiemConfirmModalVisible, setDeletePerDiemConfirmModalVisible] = useState(false); + const [isDownloadFailureModalVisible, setIsDownloadFailureModalVisible] = useState(false); const isFocused = useIsFocused(); const policyID = route.params.policyID ?? '-1'; const backTo = route.params?.backTo; @@ -304,34 +308,31 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { { icon: Expensicons.Table, text: translate('spreadsheet.importSpreadsheet'), - // eslint-disable-next-line rulesdir/prefer-early-return onSelected: () => { if (isOffline) { Modal.close(() => setIsOfflineModalVisible(true)); - // eslint-disable-next-line no-useless-return return; } - // TODO: Uncomment this when the import feature is ready - // Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID)); }, }, { icon: Expensicons.Download, text: translate('spreadsheet.downloadCSV'), - // eslint-disable-next-line rulesdir/prefer-early-return onSelected: () => { if (isOffline) { Modal.close(() => setIsOfflineModalVisible(true)); - // eslint-disable-next-line no-useless-return return; } - // Category.downloadPerDiemCSV(policyID); + PerDiem.downloadPerDiemCSV(policyID, () => { + setIsDownloadFailureModalVisible(true); + }); }, }, ]; return menuItems; - }, [translate, isOffline]); + }, [translate, isOffline, policyID]); const selectionModeHeader = selectionMode?.isEnabled && shouldUseNarrowLayout; @@ -399,15 +400,12 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { buttons={[ { buttonText: translate('spreadsheet.importSpreadsheet'), - // eslint-disable-next-line rulesdir/prefer-early-return buttonAction: () => { if (isOffline) { setIsOfflineModalVisible(true); - // eslint-disable-next-line no-useless-return return; } - // TODO: Uncomment this when the import feature is ready - // Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID)); + Navigation.navigate(ROUTES.WORKSPACE_PER_DIEM_IMPORT.getRoute(policyID)); }, success: true, }, @@ -441,6 +439,15 @@ function WorkspacePerDiemPage({route}: WorkspacePerDiemPageProps) { confirmText={translate('common.buttonConfirm')} shouldShowCancelButton={false} /> + setIsDownloadFailureModalVisible(false)} + secondOptionText={translate('common.buttonConfirm')} + isVisible={isDownloadFailureModalVisible} + onClose={() => setIsDownloadFailureModalVisible(false)} + /> ); diff --git a/src/pages/workspace/tags/ImportTagsPage.tsx b/src/pages/workspace/tags/ImportTagsPage.tsx index 7f5275e8d67b..8bc57ea07c3e 100644 --- a/src/pages/workspace/tags/ImportTagsPage.tsx +++ b/src/pages/workspace/tags/ImportTagsPage.tsx @@ -1,6 +1,6 @@ import type {StackScreenProps} from '@react-navigation/stack'; import React from 'react'; -import ImportSpreedsheet from '@components/ImportSpreadsheet'; +import ImportSpreadsheet from '@components/ImportSpreadsheet'; import usePolicy from '@hooks/usePolicy'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -24,7 +24,7 @@ function ImportTagsPage({route}: ImportTagsPageProps) { accessVariants={[CONST.POLICY.ACCESS_VARIANTS.ADMIN]} fullPageNotFoundViewProps={{subtitleKey: isEmptyObject(policy) ? undefined : 'workspace.common.notAuthorized', onLinkPress: PolicyUtils.goBackFromInvalidPolicy}} > -