diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index b5e16cfc4d7bb..68b53dc540839 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -471,9 +471,13 @@ const ONYXKEYS = { /** Stores the user search value for persistence across the screens */ ROOM_MEMBERS_USER_SEARCH_PHRASE: 'roomMembersUserSearchPhrase', + /** Stores information about recently uploaded spreadsheet file */ IMPORTED_SPREADSHEET: 'importedSpreadsheet', + /** Stores the information about the members imported from the spreadsheet */ + IMPORTED_SPREADSHEET_MEMBER_DATA: 'importedSpreadsheetMemberData', + /** Stores the route to open after changing app permission from settings */ LAST_ROUTE: 'lastRoute', @@ -1200,6 +1204,7 @@ type OnyxValuesMapping = { [ONYXKEYS.ROOM_MEMBERS_USER_SEARCH_PHRASE]: string; [ONYXKEYS.APPROVAL_WORKFLOW]: OnyxTypes.ApprovalWorkflowOnyx; [ONYXKEYS.IMPORTED_SPREADSHEET]: OnyxTypes.ImportedSpreadsheet; + [ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA]: OnyxTypes.ImportedSpreadsheetMemberData[]; [ONYXKEYS.LAST_ROUTE]: string; [ONYXKEYS.IS_USING_IMPORTED_STATE]: boolean; [ONYXKEYS.NVP_EXPENSIFY_COMPANY_CARDS_CUSTOM_NAMES]: Record; diff --git a/src/ROUTES.ts b/src/ROUTES.ts index cde90b223e78b..7e8b4b1c2af47 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1366,6 +1366,10 @@ const ROUTES = { route: 'workspaces/:policyID/members/imported', getRoute: (policyID: string) => `workspaces/${policyID}/members/imported` as const, }, + WORKSPACE_MEMBERS_IMPORTED_CONFIRMATION: { + route: 'workspaces/:policyID/members/imported/confirmation', + getRoute: (policyID: string) => `workspaces/${policyID}/members/imported/confirmation` as const, + }, POLICY_ACCOUNTING: { route: 'workspaces/:policyID/accounting', getRoute: (policyID: string | undefined, newConnectionName?: ConnectionName, integrationToDisconnect?: ConnectionName, shouldDisconnectIntegrationBeforeConnecting?: boolean) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 5e32d727073f4..e283c1cd98deb 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -552,6 +552,7 @@ const SCREENS = { MEMBERS: 'Workspace_Members', MEMBERS_IMPORT: 'Members_Import', MEMBERS_IMPORTED: 'Members_Imported', + MEMBERS_IMPORTED_CONFIRMATION: 'Members_Imported_Confirmation', INVITE: 'Workspace_Invite', INVITE_MESSAGE: 'Workspace_Invite_Message', INVITE_MESSAGE_ROLE: 'Workspace_Invite_Message_Role', diff --git a/src/languages/de.ts b/src/languages/de.ts index 9b46cfbe93dcc..2962b78f45c86 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -646,6 +646,8 @@ const translations = { getTheApp: 'Hole dir die App', scanReceiptsOnTheGo: 'Scannen Sie Belege von Ihrem Telefon aus', headsUp: 'Achtung!', + submitTo: 'Einreichen an', + forwardTo: 'Weiterleiten an', merge: 'Zusammenführen', unstableInternetConnection: 'Instabile Internetverbindung. Bitte überprüfe dein Netzwerk und versuche es erneut.', }, @@ -990,6 +992,11 @@ const translations = { 'Die Datei, die Sie hochgeladen haben, ist entweder leer oder enthält ungültige Daten. Bitte stellen Sie sicher, dass die Datei korrekt formatiert ist und die notwendigen Informationen enthält, bevor Sie sie erneut hochladen.', importSpreadsheet: 'Tabellenkalkulation importieren', downloadCSV: 'CSV herunterladen', + importMemberConfirmation: () => ({ + one: `Bitte bestätige die folgenden Angaben für ein neues Workspace-Mitglied, das mit diesem Upload hinzugefügt wird. Bestehende Mitglieder erhalten keine Rollenaktualisierungen oder Einladungshinweise.`, + other: (count: number) => + `Bitte bestätige die folgenden Angaben für die ${count} neuen Workspace-Mitglieder, die mit diesem Upload hinzugefügt werden. Bestehende Mitglieder erhalten keine Rollenaktualisierungen oder Einladungshinweise.`, + }), }, receipt: { upload: 'Beleg hochladen', diff --git a/src/languages/en.ts b/src/languages/en.ts index dee500576f1b5..0e128b0b0ff5e 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -637,6 +637,8 @@ const translations = { getTheApp: 'Get the app', scanReceiptsOnTheGo: 'Scan receipts from your phone', headsUp: 'Heads up!', + submitTo: 'Submit to', + forwardTo: 'Forward to', merge: 'Merge', unstableInternetConnection: 'Unstable internet connection. Please check your network and try again.', }, @@ -981,6 +983,11 @@ const translations = { '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.', importSpreadsheet: 'Import spreadsheet', downloadCSV: 'Download CSV', + importMemberConfirmation: () => ({ + one: `Please confirm the details below for a new workspace member that will be added as part of this upload. Existing members won’t receive any role updates or invite messages.`, + other: (count: number) => + `Please confirm the details below for the ${count} new workspace members that will be added as part of this upload. Existing members won’t receive any role updates or invite messages.`, + }), }, receipt: { upload: 'Upload receipt', diff --git a/src/languages/es.ts b/src/languages/es.ts index 07052dda86b8b..b4a8f2ef7c309 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -627,6 +627,8 @@ const translations = { getTheApp: 'Descarga la app', scanReceiptsOnTheGo: 'Escanea recibos desde tu teléfono', headsUp: '¡Atención!', + submitTo: 'Enviar a', + forwardTo: 'Reenviar a', merge: 'Fusionar', unstableInternetConnection: 'Conexión a internet inestable. Por favor, revisa tu red e inténtalo de nuevo.', }, @@ -974,6 +976,11 @@ const translations = { '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.', importSpreadsheet: 'Importar hoja de cálculo', downloadCSV: 'Descargar CSV', + importMemberConfirmation: () => ({ + one: `Por favor confirma los detalles a continuación para un nuevo miembro del espacio de trabajo que se agregará como parte de esta carga. Los miembros existentes no recibirán actualizaciones de rol ni mensajes de invitación.`, + other: (count: number) => + `Por favor confirma los detalles a continuación para los ${count} nuevos miembros del espacio de trabajo que se agregarán como parte de esta carga. Los miembros existentes no recibirán actualizaciones de rol ni mensajes de invitación.`, + }), }, receipt: { upload: 'Subir recibo', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index d676f289eea56..d36062263e528 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -646,6 +646,8 @@ const translations = { getTheApp: "Obtenez l'application", scanReceiptsOnTheGo: 'Numérisez les reçus depuis votre téléphone', headsUp: 'Attention !', + submitTo: 'Envoyer à', + forwardTo: 'Transférer à', merge: 'Fusionner', unstableInternetConnection: 'Connexion Internet instable. Veuillez vérifier votre réseau et réessayer.', }, @@ -992,6 +994,11 @@ const translations = { 'Le fichier que vous avez téléchargé est soit vide, soit contient des données invalides. Veuillez vous assurer que le fichier est correctement formaté et contient les informations nécessaires avant de le télécharger à nouveau.', importSpreadsheet: 'Importer une feuille de calcul', downloadCSV: 'Télécharger CSV', + importMemberConfirmation: () => ({ + one: `Veuillez confirmer les informations ci-dessous pour un nouveau membre de l’espace de travail qui sera ajouté dans le cadre de cet import. Les membres existants ne recevront aucune mise à jour de rôle ni de message d’invitation.`, + other: (count: number) => + `Veuillez confirmer les informations ci-dessous pour les ${count} nouveaux membres de l’espace de travail qui seront ajoutés dans le cadre de cet import. Les membres existants ne recevront aucune mise à jour de rôle ni de message d’invitation.`, + }), }, receipt: { upload: 'Télécharger le reçu', diff --git a/src/languages/it.ts b/src/languages/it.ts index 96a4aed028a22..568dffb32a5ba 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -646,6 +646,8 @@ const translations = { getTheApp: "Scarica l'app", scanReceiptsOnTheGo: 'Scansiona le ricevute dal tuo telefono', headsUp: 'Attenzione!', + submitTo: 'Invia a', + forwardTo: 'Inoltra a', merge: 'Unisci', unstableInternetConnection: 'Connessione Internet instabile. Controlla la tua rete e riprova.', }, @@ -987,6 +989,11 @@ const translations = { 'Il file che hai caricato è vuoto o contiene dati non validi. Assicurati che il file sia formattato correttamente e contenga le informazioni necessarie prima di caricarlo di nuovo.', importSpreadsheet: 'Importa foglio di calcolo', downloadCSV: 'Scarica CSV', + importMemberConfirmation: () => ({ + one: `Conferma i dettagli di seguito per un nuovo membro del workspace che verrà aggiunto come parte di questo caricamento. I membri esistenti non riceveranno aggiornamenti di ruolo né messaggi di invito.`, + other: (count: number) => + `Conferma i dettagli di seguito per i ${count} nuovi membri del workspace che verranno aggiunti come parte di questo caricamento. I membri esistenti non riceveranno aggiornamenti di ruolo né messaggi di invito.`, + }), }, receipt: { upload: 'Carica ricevuta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index d5dad88e0f848..069646f4681c4 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -646,6 +646,8 @@ const translations = { getTheApp: 'アプリを入手', scanReceiptsOnTheGo: '携帯電話から領収書をスキャンする', headsUp: 'ご注意ください!', + submitTo: '送信先', + forwardTo: '転送先', merge: 'マージ', unstableInternetConnection: 'インターネット接続が不安定です。ネットワークを確認してもう一度お試しください。', }, @@ -989,6 +991,11 @@ const translations = { 'アップロードしたファイルは空であるか、無効なデータが含まれています。ファイルが正しくフォーマットされ、必要な情報が含まれていることを確認してから、再度アップロードしてください。', importSpreadsheet: 'スプレッドシートをインポート', downloadCSV: 'CSVをダウンロード', + importMemberConfirmation: () => ({ + one: `このアップロードで追加される新しいワークスペースメンバーの詳細を以下で確認してください。既存のメンバーにはロールの更新や招待メッセージは送信されません。`, + other: (count: number) => + `このアップロードで追加される${count}人の新しいワークスペースメンバーの詳細を以下で確認してください。既存のメンバーにはロールの更新や招待メッセージは送信されません。`, + }), }, receipt: { upload: '領収書をアップロード', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index dfa59eab5dd29..69252cc2a8500 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -645,6 +645,8 @@ const translations = { getTheApp: 'Download de app', scanReceiptsOnTheGo: 'Scan bonnetjes vanaf je telefoon', headsUp: 'Let op!', + submitTo: 'Sturen naar', + forwardTo: 'Doorsturen naar', merge: 'Samenvoegen', unstableInternetConnection: 'Onstabiele internetverbinding. Controleer je netwerk en probeer het opnieuw.', }, @@ -988,6 +990,11 @@ const translations = { 'Het bestand dat u heeft geüpload is ofwel leeg of bevat ongeldige gegevens. Zorg ervoor dat het bestand correct is opgemaakt en de benodigde informatie bevat voordat u het opnieuw uploadt.', importSpreadsheet: 'Spreadsheet importeren', downloadCSV: 'CSV downloaden', + importMemberConfirmation: () => ({ + one: `Bevestig hieronder de gegevens voor een nieuw werkruimte-lid dat via deze upload wordt toegevoegd. Bestaande leden ontvangen geen rolupdates of uitnodigingsberichten.`, + other: (count: number) => + `Bevestig hieronder de gegevens voor de ${count} nieuwe werkruimte-leden die via deze upload worden toegevoegd. Bestaande leden ontvangen geen rolupdates of uitnodigingsberichten.`, + }), }, receipt: { upload: 'Bonnetje uploaden', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 5cfabe7696c1a..a673e3fb270f6 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -646,6 +646,8 @@ const translations = { getTheApp: 'Pobierz aplikację', scanReceiptsOnTheGo: 'Skanuj paragony za pomocą telefonu', headsUp: 'Uwaga!', + submitTo: 'Wyślij do', + forwardTo: 'Przekaż do', merge: 'Scal', unstableInternetConnection: 'Niestabilne połączenie internetowe. Sprawdź swoją sieć i spróbuj ponownie.', }, @@ -986,6 +988,11 @@ const translations = { 'Plik, który przesłałeś, jest pusty lub zawiera nieprawidłowe dane. Upewnij się, że plik jest poprawnie sformatowany i zawiera niezbędne informacje przed ponownym przesłaniem.', importSpreadsheet: 'Importuj arkusz kalkulacyjny', downloadCSV: 'Pobierz CSV', + importMemberConfirmation: () => ({ + one: `Potwierdź poniższe szczegóły dotyczące nowego członka przestrzeni roboczej, który zostanie dodany w ramach tego przesyłania. Istniejący członkowie nie otrzymają aktualizacji ról ani wiadomości z zaproszeniem.`, + other: (count: number) => + `Potwierdź poniższe szczegóły dotyczące ${count} nowych członków przestrzeni roboczej, którzy zostaną dodani w ramach tego przesyłania. Istniejący członkowie nie otrzymają aktualizacji ról ani wiadomości z zaproszeniem.`, + }), }, receipt: { upload: 'Prześlij paragon', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 955b0f3ef0f27..99d8c1c1ff015 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -646,6 +646,8 @@ const translations = { getTheApp: 'Obtenha o aplicativo', scanReceiptsOnTheGo: 'Digitalize recibos com seu celular', headsUp: 'Atenção!', + submitTo: 'Enviar para', + forwardTo: 'Encaminhar para', merge: 'Mesclar', unstableInternetConnection: 'Conexão de internet instável. Verifique sua rede e tente novamente.', }, @@ -989,6 +991,11 @@ const translations = { 'O arquivo que você enviou está vazio ou contém dados inválidos. Por favor, certifique-se de que o arquivo está formatado corretamente e contém as informações necessárias antes de enviá-lo novamente.', importSpreadsheet: 'Importar planilha', downloadCSV: 'Baixar CSV', + importMemberConfirmation: () => ({ + one: `Confirme os detalhes abaixo para um novo membro do workspace que será adicionado como parte deste envio. Os membros existentes não receberão atualizações de função nem mensagens de convite.`, + other: (count: number) => + `Confirme os detalhes abaixo para os ${count} novos membros do workspace que serão adicionados como parte deste envio. Os membros existentes não receberão atualizações de função nem mensagens de convite.`, + }), }, receipt: { upload: 'Fazer upload de recibo', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 53d55e29afc89..c58f485b8cfb7 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -645,6 +645,8 @@ const translations = { getTheApp: '获取应用程序', scanReceiptsOnTheGo: '用手机扫描收据', headsUp: '\u6CE8\u610F\uFF01', + submitTo: '提交到', + forwardTo: '转发到', merge: '合并', unstableInternetConnection: '互联网连接不稳定。请检查你的网络,然后重试。', }, @@ -981,6 +983,10 @@ const translations = { invalidFileMessage: '您上传的文件要么是空的,要么包含无效数据。请确保文件格式正确并包含必要的信息,然后再重新上传。', importSpreadsheet: '导入电子表格', downloadCSV: '下载 CSV', + importMemberConfirmation: () => ({ + one: `请确认以下信息,以添加此次上传中的一位新工作区成员。现有成员不会收到角色更新或邀请消息。`, + other: (count: number) => `请确认以下信息,以添加此次上传中的 ${count} 位新工作区成员。现有成员不会收到角色更新或邀请消息。`, + }), }, receipt: { upload: '上传收据', diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index ff64eb62be38b..b9c3630e2352e 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -315,6 +315,7 @@ const SettingsModalStackNavigator = createModalStackNavigator require('../../../../pages/workspace/WorkspaceInvitePage').default, [SCREENS.WORKSPACE.MEMBERS_IMPORT]: () => require('../../../../pages/workspace/members/ImportMembersPage').default, [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: () => require('../../../../pages/workspace/members/ImportedMembersPage').default, + [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION]: () => require('../../../../pages/workspace/members/ImportedMembersConfirmationPage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsCreatePage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EDIT]: () => require('../../../../pages/workspace/workflows/approvals/WorkspaceWorkflowsApprovalsEditPage').default, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_EXPENSES_FROM]: () => diff --git a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts index b649b2025e8bc..1da1353614ae2 100755 --- a/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts +++ b/src/libs/Navigation/linkingConfig/RELATIONS/WORKSPACE_TO_RHP.ts @@ -22,6 +22,7 @@ const WORKSPACE_TO_RHP: Partial['config'] = { [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: { path: ROUTES.WORKSPACE_MEMBERS_IMPORTED.route, }, + [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION]: { + path: ROUTES.WORKSPACE_MEMBERS_IMPORTED_CONFIRMATION.route, + }, [SCREENS.WORKSPACE.WORKFLOWS_APPROVALS_NEW]: { path: ROUTES.WORKSPACE_WORKFLOWS_APPROVALS_NEW.route, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 4f50f90e534ef..dbd40fac1889d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -190,6 +190,9 @@ type SettingsNavigatorParamList = { [SCREENS.WORKSPACE.MEMBERS_IMPORTED]: { policyID: string; }; + [SCREENS.WORKSPACE.MEMBERS_IMPORTED_CONFIRMATION]: { + policyID: string; + }; [SCREENS.WORKSPACE.INVITE_MESSAGE]: { policyID: string; backTo?: Routes; diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 7f92542bf49ab..08380c6ae100e 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -284,6 +284,15 @@ function isPolicyMember(currentUserLogin: string | undefined, policyID: string | return !!currentUserLogin && !!policyID && !!allPolicies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`]?.employeeList?.[currentUserLogin]; } +function isPolicyMemberWithoutPendingDelete(currentUserLogin: string | undefined, policy: OnyxEntry): boolean { + if (!currentUserLogin || !policy?.id) { + return false; + } + + const policyEmployee = policy?.employeeList?.[currentUserLogin]; + return !!policyEmployee && policyEmployee.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; +} + function isPolicyPayer(policy: OnyxEntry, currentUserLogin: string | undefined): boolean { if (!policy) { return false; @@ -1664,6 +1673,7 @@ export { getPolicyRole, hasIndependentTags, getLengthOfTag, + isPolicyMemberWithoutPendingDelete, getPolicyEmployeeAccountIDs, }; diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 206830463cc52..f2808add75e9a 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -28,7 +28,17 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as FormActions from '@userActions/FormActions'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {InvitedEmailsToAccountIDs, PersonalDetailsList, Policy, PolicyEmployee, PolicyOwnershipChangeChecks, Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type { + ImportedSpreadsheetMemberData, + InvitedEmailsToAccountIDs, + PersonalDetailsList, + Policy, + PolicyEmployee, + PolicyOwnershipChangeChecks, + Report, + ReportAction, + ReportActions, +} from '@src/types/onyx'; import type {PendingAction} from '@src/types/onyx/OnyxCommon'; import type {JoinWorkspaceResolution} from '@src/types/onyx/OriginalMessage'; import type {ApprovalRule} from '@src/types/onyx/Policy'; @@ -1011,6 +1021,8 @@ function addMembersToWorkspace( type PolicyMember = { email: string; role: string; + submitsTo?: string; + forwardsTo?: string; }; function importPolicyMembers(policyID: string, members: PolicyMember[]) { @@ -1021,7 +1033,7 @@ function importPolicyMembers(policyID: string, members: PolicyMember[]) { (acc, curr) => { const employee = policy?.employeeList?.[curr.email]; if (employee) { - if (curr.role !== employee.role) { + if (curr.role !== employee.role || curr.submitsTo !== employee.submitsTo || curr.forwardsTo !== employee.forwardsTo) { acc.updated++; } } else { @@ -1035,7 +1047,7 @@ function importPolicyMembers(policyID: string, members: PolicyMember[]) { const parameters = { policyID, - employees: JSON.stringify(members.map((member) => ({email: member.email, role: member.role}))), + employees: JSON.stringify(members.map((member) => ({email: member.email, role: member.role, submitsTo: member.submitsTo, forwardsTo: member.forwardsTo}))), }; API.write(WRITE_COMMANDS.IMPORT_MEMBERS_SPREADSHEET, parameters, onyxData); @@ -1332,6 +1344,14 @@ function clearInviteDraft(policyID: string) { FormActions.clearDraftValues(ONYXKEYS.FORMS.WORKSPACE_INVITE_MESSAGE_FORM); } +function setImportedSpreadsheetMemberData(memberData: ImportedSpreadsheetMemberData[]) { + Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, memberData); +} + +function clearImportedSpreadsheetMemberData() { + Onyx.set(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, null); +} + export { removeMembers, buildUpdateWorkspaceMembersRoleOnyxData, @@ -1357,4 +1377,6 @@ export { openPolicyMemberProfilePage, setWorkspaceInviteRoleDraft, clearWorkspaceInviteRoleDraft, + setImportedSpreadsheetMemberData, + clearImportedSpreadsheetMemberData, }; diff --git a/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx new file mode 100644 index 0000000000000..e4fb3cc38e770 --- /dev/null +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -0,0 +1,218 @@ +import React, {useCallback, useEffect, useMemo, useState} from 'react'; +import {View} from 'react-native'; +import type {GestureResponderEvent} from 'react-native/Libraries/Types/CoreEventTypes'; +import type {ValueOf} from 'type-fest'; +import Button from '@components/Button'; +import ConfirmModal from '@components/ConfirmModal'; +import FixedFooter from '@components/FixedFooter'; +import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; +import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +import ReportActionAvatars from '@components/ReportActionAvatars'; +import ScreenWrapper from '@components/ScreenWrapper'; +import Text from '@components/Text'; +import useCloseImportPage from '@hooks/useCloseImportPage'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; +import useThemeStyles from '@hooks/useThemeStyles'; +import {openExternalLink} from '@libs/actions/Link'; +import {clearImportedSpreadsheetMemberData, importPolicyMembers} from '@libs/actions/Policy/Member'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {getAccountIDsByLogins} from '@libs/PersonalDetailsUtils'; +import {isPolicyMemberWithoutPendingDelete} from '@libs/PolicyUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import WorkspaceMemberDetailsRoleSelectionModal from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; +import type {ListItemType} from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; + +type ImportedMembersConfirmationPageProps = PlatformStackScreenProps; + +function ImportedMembersConfirmationPage({route}: ImportedMembersConfirmationPageProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + const [spreadsheet] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET, {canBeMissing: true}); + const [role, setRole] = useState>(CONST.POLICY.ROLE.USER); + const [isRoleSelectionModalVisible, setIsRoleSelectionModalVisible] = useState(false); + + const policyID = route.params.policyID; + const policy = usePolicy(policyID); + const [isImporting, setIsImporting] = useState(false); + const {isOffline} = useNetwork(); + + const personalDetails = usePersonalDetails(); + const {setIsClosing} = useCloseImportPage(); + + useEffect(() => { + return () => { + clearImportedSpreadsheetMemberData(); + }; + }, []); + + const [importedSpreadsheetMemberData] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, {canBeMissing: true}); + const newMembers = useMemo(() => { + return importedSpreadsheetMemberData?.filter((member) => !isPolicyMemberWithoutPendingDelete(member.email, policy) && !member.role) ?? []; + }, [importedSpreadsheetMemberData, policy]); + const invitedEmailsToAccountIDsDraft = useMemo(() => { + const memberEmails = newMembers.map((member) => member.email); + return memberEmails.reduce( + (acc, email) => { + acc[email] = getAccountIDsByLogins([email])?.at(0) ?? 0; + return acc; + }, + {} as Record, + ); + // getAccountIDsByLogins function uses the personalDetails data from the connection, so we need to re-run this logic when the personal detail is changed. + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [newMembers, personalDetails]); + + /** Opens privacy url as an external link */ + const openPrivacyURL = (event: GestureResponderEvent | KeyboardEvent | undefined) => { + event?.preventDefault(); + openExternalLink(CONST.OLD_DOT_PUBLIC_URLS.PRIVACY_URL); + }; + + const importMembers = useCallback(() => { + if (!newMembers) { + return; + } + setIsImporting(true); + const membersWithRole = (importedSpreadsheetMemberData ?? []).map((member) => ({...member, role: member.role || role})); + importPolicyMembers(policyID, membersWithRole); + }, [importedSpreadsheetMemberData, newMembers, policyID, role]); + + const closeImportPageAndModal = () => { + setIsClosing(true); + setIsImporting(false); + Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); + }; + + const onRoleChange = (item: ListItemType) => { + setRole(item.value); + setIsRoleSelectionModalVisible(false); + }; + + const roleItems: ListItemType[] = [ + { + value: CONST.POLICY.ROLE.ADMIN, + text: translate('common.admin'), + alternateText: translate('workspace.common.adminAlternateText'), + isSelected: role === CONST.POLICY.ROLE.ADMIN, + keyForList: CONST.POLICY.ROLE.ADMIN, + }, + { + value: CONST.POLICY.ROLE.AUDITOR, + text: translate('common.auditor'), + alternateText: translate('workspace.common.auditorAlternateText'), + isSelected: role === CONST.POLICY.ROLE.AUDITOR, + keyForList: CONST.POLICY.ROLE.AUDITOR, + }, + { + value: CONST.POLICY.ROLE.USER, + text: translate('common.member'), + alternateText: translate('workspace.common.memberAlternateText'), + isSelected: role === CONST.POLICY.ROLE.USER, + keyForList: CONST.POLICY.ROLE.USER, + }, + ]; + + if (!spreadsheet || !importedSpreadsheetMemberData) { + return ; + } + + return ( + + { + Navigation.goBack(); + }} + /> + + + + + + {translate('spreadsheet.importMemberConfirmation', {count: newMembers?.length ?? 0})} + + + + { + setIsRoleSelectionModalVisible(true); + }} + /> + + + + +