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}}
>
-