Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/languages/de.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ups! Ein erforderliches Feld („${fieldName}“) wurde nicht zugeordnet. Bitte überprüfe es und versuche es erneut.`,
singleFieldMultipleColumns: (fieldName: string) => `Ups! Du hast ein einzelnes Feld („${fieldName}“) mehreren Spalten zugeordnet. Bitte überprüfe dies und versuche es erneut.`,
emptyMappedField: (fieldName: string) => `Ups! Das Feld („${fieldName}“) enthält einen oder mehrere leere Werte. Bitte überprüfe es und versuche es erneut.`,
invalidApprovalLimit: 'Genehmigungslimits müssen gültige Beträge in der Währung des Arbeitsbereichs sein.',
fieldValueTooLong: (fieldName: string, limit: number) =>
`Ups! Das Feld („${fieldName}“) enthält einen oder mehrere Werte mit mehr als ${limit} Zeichen. Bitte überprüfe es und versuche es erneut.`,
importSuccessfulTitle: 'Import erfolgreich',
Expand Down
1 change: 1 addition & 0 deletions src/languages/el.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1242,6 +1242,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ουπς! Ένα υποχρεωτικό πεδίο («${fieldName}») δεν έχει αντιστοιχιστεί. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
singleFieldMultipleColumns: (fieldName: string) => `Ωχ! Έχετε αντιστοιχίσει ένα μόνο πεδίο («${fieldName}») σε πολλές στήλες. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
emptyMappedField: (fieldName: string) => `Ωχ! Το πεδίο («${fieldName}») περιέχει μία ή περισσότερες κενές τιμές. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
invalidApprovalLimit: 'Τα όρια έγκρισης πρέπει να είναι έγκυρα ποσά στο νόμισμα του χώρου εργασίας.',
fieldValueTooLong: (fieldName: string, limit: number) =>
`Ωχ! Το πεδίο («${fieldName}») περιέχει μία ή περισσότερες τιμές μεγαλύτερες από ${limit} χαρακτήρες. Παρακαλούμε ελέγξτε και δοκιμάστε ξανά.`,
importSuccessfulTitle: 'Η εισαγωγή ήταν επιτυχής',
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1329,6 +1329,7 @@ const translations = {
fieldNotMapped: (fieldName: string) => `Oops! A required field ("${fieldName}") hasn't been mapped. Please review and try again.`,
singleFieldMultipleColumns: (fieldName: string) => `Oops! You've mapped a single field ("${fieldName}") to multiple columns. Please review and try again.`,
emptyMappedField: (fieldName: string) => `Oops! The field ("${fieldName}") contains one or more empty values. Please review and try again.`,
invalidApprovalLimit: 'Approval limits must be valid amounts for the workspace currency.',
fieldValueTooLong: (fieldName: string, limit: number) => `Oops! The field ("${fieldName}") contains one or more values longer than ${limit} characters. Please review and try again.`,
importSuccessfulTitle: 'Import successful',
importCategoriesNoneAddedOrUpdated: 'No categories have been added or updated.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName) => `¡Vaya! Un campo obligatorio ("${fieldName}") no ha sido mapeado. Por favor, revisa e inténtalo de nuevo.`,
singleFieldMultipleColumns: (fieldName) => `¡Vaya! Has mapeado un solo campo ("${fieldName}") a varias columnas. Por favor, revisa e inténtalo de nuevo.`,
emptyMappedField: (fieldName) => `¡Vaya! El campo ("${fieldName}") contiene uno o más valores vacíos. Por favor, revísalo e inténtalo de nuevo.`,
invalidApprovalLimit: 'Los límites de aprobación deben ser importes válidos en la moneda del espacio de trabajo.',
fieldValueTooLong: (fieldName, limit) => `¡Vaya! El campo ("${fieldName}") contiene uno o más valores de más de ${limit} caracteres. Por favor, revísalo e inténtalo de nuevo.`,
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.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/fr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1202,6 +1202,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Oups ! Un champ obligatoire (« ${fieldName} ») n’a pas été associé. Veuillez examiner et réessayer.`,
singleFieldMultipleColumns: (fieldName: string) => `Oups ! Vous avez associé un seul champ (« ${fieldName} ») à plusieurs colonnes. Veuillez vérifier et réessayer.`,
emptyMappedField: (fieldName: string) => `Oups ! Le champ (« ${fieldName} ») contient une ou plusieurs valeurs vides. Veuillez vérifier et réessayer.`,
invalidApprovalLimit: 'Les limites d’approbation doivent être des montants valides dans la devise de l’espace de travail.',
fieldValueTooLong: (fieldName: string, limit: number) =>
`Oups ! Le champ (« ${fieldName} ») contient une ou plusieurs valeurs de plus de ${limit} caractères. Veuillez vérifier et réessayer.`,
importSuccessfulTitle: 'Importation réussie',
Expand Down
1 change: 1 addition & 0 deletions src/languages/it.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ops! Un campo obbligatorio ("${fieldName}") non è stato mappato. Controlla e riprova.`,
singleFieldMultipleColumns: (fieldName: string) => `Ops! Hai associato un singolo campo ("${fieldName}") a più colonne. Controlla e riprova.`,
emptyMappedField: (fieldName: string) => `Ops! Il campo ("${fieldName}") contiene uno o più valori vuoti. Controlla e riprova.`,
invalidApprovalLimit: 'I limiti di approvazione devono essere importi validi nella valuta dello spazio di lavoro.',
fieldValueTooLong: (fieldName: string, limit: number) => `Ops! Il campo ("${fieldName}") contiene uno o più valori più lunghi di ${limit} caratteri. Controlla e riprova.`,
importSuccessfulTitle: 'Importazione riuscita',
importCategoriesNoneAddedOrUpdated: 'Nessuna categoria è stata aggiunta o aggiornata.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/ja.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1183,6 +1183,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `おっと!必須フィールド(「${fieldName}」)がマッピングされていません。確認してもう一度お試しください。`,
singleFieldMultipleColumns: (fieldName: string) => `おっと!1 つのフィールド(「${fieldName}」)を複数の列に割り当てています。確認してもう一度お試しください。`,
emptyMappedField: (fieldName: string) => `おっと!フィールド(「${fieldName}」)に1つ以上の空の値が含まれています。確認してもう一度お試しください。`,
invalidApprovalLimit: '承認上限額は、ワークスペースの通貨で有効な金額である必要があります。',
fieldValueTooLong: (fieldName: string, limit: number) => `おっと!フィールド(「${fieldName}」)に${limit}文字を超える値が1つ以上含まれています。確認してもう一度お試しください。`,
importSuccessfulTitle: 'インポートに成功しました',
importCategoriesNoneAddedOrUpdated: 'カテゴリーは追加も更新もされていません。',
Expand Down
1 change: 1 addition & 0 deletions src/languages/nl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Oeps! Een vereist veld ("${fieldName}") is niet toegewezen. Controleer het en probeer het opnieuw.`,
singleFieldMultipleColumns: (fieldName: string) => `Oeps! Je hebt één veld (‘${fieldName}’) aan meerdere kolommen gekoppeld. Controleer dit en probeer het opnieuw.`,
emptyMappedField: (fieldName: string) => `Oeps! Het veld („${fieldName}”) bevat een of meer lege waarden. Controleer het en probeer het opnieuw.`,
invalidApprovalLimit: 'Goedkeuringslimieten moeten geldige bedragen in de valuta van de werkruimte zijn.',
fieldValueTooLong: (fieldName: string, limit: number) =>
`Oeps! Het veld („${fieldName}”) bevat een of meer waarden die langer zijn dan ${limit} tekens. Controleer het en probeer het opnieuw.`,
importSuccessfulTitle: 'Import geslaagd',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1217,6 +1217,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ups! Wymagane pole („${fieldName}”) nie zostało zmapowane. Sprawdź i spróbuj ponownie.`,
singleFieldMultipleColumns: (fieldName: string) => `Ups! Przypisałeś jedno pole („${fieldName}”) do wielu kolumn. Sprawdź ustawienia i spróbuj ponownie.`,
emptyMappedField: (fieldName: string) => `Ups! Pole („${fieldName}”) zawiera jedną lub więcej pustych wartości. Sprawdź je i spróbuj ponownie.`,
invalidApprovalLimit: 'Limity zatwierdzania muszą być prawidłowymi kwotami w walucie przestrzeni roboczej.',
fieldValueTooLong: (fieldName: string, limit: number) => `Ups! Pole („${fieldName}”) zawiera jedną lub więcej wartości dłuższych niż ${limit} znaków. Sprawdź je i spróbuj ponownie.`,
importSuccessfulTitle: 'Import zakończony powodzeniem',
importCategoriesNoneAddedOrUpdated: 'Nie dodano ani nie zaktualizowano żadnych kategorii.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/pt-BR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1196,6 +1196,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `Ops! Um campo obrigatório ("${fieldName}") não foi mapeado. Revise e tente novamente.`,
singleFieldMultipleColumns: (fieldName: string) => `Ops! Você mapeou um único campo ("${fieldName}") para várias colunas. Revise e tente novamente.`,
emptyMappedField: (fieldName: string) => `Ops! O campo ("${fieldName}") contém um ou mais valores vazios. Revise e tente novamente.`,
invalidApprovalLimit: 'Os limites de aprovação devem ser valores válidos na moeda do espaço de trabalho.',
fieldValueTooLong: (fieldName: string, limit: number) => `Ops! O campo ("${fieldName}") contém um ou mais valores com mais de ${limit} caracteres. Revise e tente novamente.`,
importSuccessfulTitle: 'Importação concluída',
importCategoriesNoneAddedOrUpdated: 'Nenhuma categoria foi adicionada ou atualizada.',
Expand Down
1 change: 1 addition & 0 deletions src/languages/zh-hans.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1148,6 +1148,7 @@ const translations: TranslationDeepObject<typeof en> = {
fieldNotMapped: (fieldName: string) => `哎呀!有一个必填字段(“${fieldName}”)尚未映射。请检查后重试。`,
singleFieldMultipleColumns: (fieldName: string) => `哎呀!你已将单个字段(“${fieldName}”)映射到多个列。请检查后重试。`,
emptyMappedField: (fieldName: string) => `哎呀!字段(“${fieldName}”)包含一个或多个空值。请检查后重试。`,
invalidApprovalLimit: '审批限额必须是工作区币种的有效金额。',
fieldValueTooLong: (fieldName: string, limit: number) => `哎呀!字段(“${fieldName}”)包含一个或多个超过 ${limit} 个字符的值。请检查后重试。`,
importSuccessfulTitle: '导入成功',
importCategoriesNoneAddedOrUpdated: '尚未添加或更新任何类别。',
Expand Down
14 changes: 13 additions & 1 deletion src/libs/importSpreadsheetUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ import type {ColumnRole} from '@components/ImportColumn';

import CONST from '@src/CONST';

import {stripCommaFromAmount, stripSpacesFromAmount} from './MoneyRequestUtils';
import StringUtils from './StringUtils';

// cspell:disable
/**
* Maps a spreadsheet header string to the CSV import column role it most likely represents, or an empty string when
Expand Down Expand Up @@ -361,4 +364,13 @@ function generateColumnNames(length: number) {
return Array.from({length}, (_, i) => numberToColumn(i));
}

export {findColumnName, findDuplicate, generateColumnNames, getCompanyCardColumnMappings};
/**
* Normalizes an amount cell from an imported spreadsheet by removing its currency marker, grouping separators, and whitespace.
*/
function normalizeImportedAmount(value: string, currencySymbol?: string, currencyCode?: string): string {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be ideal to add a unit test.

const tokens = [currencySymbol, currencyCode].filter((token): token is string => !!token).map((token) => StringUtils.escapeRegExp(token));
const withoutCurrency = tokens.length > 0 ? value.trim().replace(new RegExp(`^(?:${tokens.join('|')})\\s*|\\s*(?:${tokens.join('|')})$`, 'i'), '') : value;
return stripCommaFromAmount(stripSpacesFromAmount(withoutCurrency.replaceAll(/\p{Sc}/gu, '')));
}

export {findColumnName, findDuplicate, generateColumnNames, getCompanyCardColumnMappings, normalizeImportedAmount};
33 changes: 29 additions & 4 deletions src/pages/workspace/members/ImportedMembersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import ImportSpreadsheetColumns from '@components/ImportSpreadsheetColumns';
import ScreenWrapper from '@components/ScreenWrapper';

import useCloseImportPage from '@hooks/useCloseImportPage';
import {useCurrencyListActions} from '@hooks/useCurrencyList';
import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails';
import useImportSpreadsheetConfirmModal from '@hooks/useImportSpreadsheetConfirmModal';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -12,7 +13,9 @@ import usePolicy from '@hooks/usePolicy';

import {importPolicyMembers, setImportedSpreadsheetMemberData} from '@libs/actions/Policy/Member';
import Tab from '@libs/actions/Tab';
import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils';
import {convertToBackendAmount} from '@libs/CurrencyUtils';
import {findDuplicate, generateColumnNames, normalizeImportedAmount} from '@libs/importSpreadsheetUtils';
import {validateAmount} from '@libs/MoneyRequestUtils';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SettingsNavigatorParamList} from '@libs/Navigation/types';
Expand All @@ -39,8 +42,11 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) {
const showImportSpreadsheetConfirmModal = useImportSpreadsheetConfirmModal();
const policyID = route.params.policyID;
const policy = usePolicy(policyID);
const {getCurrencyDecimals, getCurrencySymbol} = useCurrencyListActions();
const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails();
const canAssignElevatedRoles = canMemberAssignElevatedRole(policy, currentUserLogin);
const currency = policy?.outputCurrency;
const currencySymbol = getCurrencySymbol(currency ?? '');

// The same mapping screen is reused for the Members importer and the Workflows importer. When it is reached from the
// Workflows page we keep the user in the Workflows context (title + back + return + confirmation navigation).
Expand Down Expand Up @@ -81,12 +87,23 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) {
if (duplicate) {
errors.duplicates = translate('spreadsheet.singleFieldMultipleColumns', duplicate);
} else {
errors = {};
const approvalLimitColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.REPORT_THRESHOLD);
if (approvalLimitColumn !== -1) {
const decimals = getCurrencyDecimals(currency);
const hasInvalidApprovalLimit = spreadsheet?.data?.[approvalLimitColumn]?.some((value, index) => {
if (containsHeader && index === 0) {
return false;
}
const normalizedValue = normalizeImportedAmount(String(value), currencySymbol, currency);
return !validateAmount(normalizedValue, decimals);
});
errors = hasInvalidApprovalLimit ? {approvalLimit: translate('spreadsheet.invalidApprovalLimit')} : {};
}
}
}

return errors;
}, [requiredColumns, spreadsheet?.columns, translate]);
}, [containsHeader, currency, currencySymbol, getCurrencyDecimals, requiredColumns, spreadsheet?.columns, spreadsheet?.data, translate]);

const closeImportPageAndModal = () => {
setIsClosing(true);
Expand Down Expand Up @@ -195,7 +212,15 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) {
}
const customField1 = membersCustomField1Column !== -1 ? (membersCustomField1?.[containsHeader ? index + 1 : index] ?? '') : undefined;
const customField2 = membersCustomField2Column !== -1 ? (membersCustomField2?.[containsHeader ? index + 1 : index] ?? '') : undefined;
const approvalLimit = membersApprovalLimitColumn !== -1 ? (membersApprovalLimit?.[containsHeader ? index + 1 : index] ?? '') : undefined;
let approvalLimit: string | undefined;
if (membersApprovalLimitColumn !== -1) {
const approvalLimitValue = membersApprovalLimit?.[containsHeader ? index + 1 : index] ?? '';
const normalizedApprovalLimit = normalizeImportedAmount(approvalLimitValue, currencySymbol, currency);
approvalLimit = normalizedApprovalLimit;
if (normalizedApprovalLimit !== '') {
approvalLimit = String(convertToBackendAmount(Number.parseFloat(normalizedApprovalLimit)));
}
}
const overLimitForwardsTo = membersOverLimitForwardsToColumn !== -1 ? (membersOverLimitForwardsTo?.[containsHeader ? index + 1 : index] ?? '') : undefined;

return {
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/NormalizeImportedAmountTest.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import {normalizeImportedAmount} from '@libs/importSpreadsheetUtils';

describe('normalizeImportedAmount', () => {
it.each([
['500', undefined, undefined, '500'],
['$1,234.56', '$', 'USD', '1234.56'],
[' USD 1,234.56 ', '$', 'USD', '1234.56'],
['1,234.56 usd', '$', 'USD', '1234.56'],
['R$ 1,234.56', 'R$', 'BRL', '1234.56'],
['€ 1\u00a0234.56', undefined, undefined, '1234.56'],
['', '$', 'USD', ''],
[' ', '$', 'USD', ''],
['invalid', '$', 'USD', 'invalid'],
])('normalizes %j with symbol %j and code %j', (value, symbol, code, expected) => {
// Given a CSV cell that may contain currency formatting or invalid text.
// When it is normalized before amount validation.
const result = normalizeImportedAmount(value ?? '', symbol, code);

// Then formatting is removed while blank and invalid input remain available for validation.
expect(result).toBe(expected);
});
});
Loading