From 35e55e9207ae8d50ac811b46c5b8e3d60e9f5dd7 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 3 Jul 2025 14:48:56 +0700 Subject: [PATCH 01/29] fix: update CSV upload options for Members --- src/languages/en.ts | 2 ++ src/languages/es.ts | 2 ++ src/libs/actions/Policy/Member.ts | 6 ++++-- .../workspace/members/ImportedMembersPage.tsx | 16 ++++++++++++++++ 4 files changed, 24 insertions(+), 2 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b221c0f4cfd7d..cdc10009cd284 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -606,6 +606,8 @@ const translations = { getTheApp: 'Get the app', scanReceiptsOnTheGo: 'Scan receipts from your phone', headsUp: 'Heads up!', + submitTo: 'Submit to', + forwardTo: 'Forward to', }, supportalNoAccess: { title: 'Not so fast', diff --git a/src/languages/es.ts b/src/languages/es.ts index 341db9f025c3b..68b82ab915d0e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -597,6 +597,8 @@ const translations = { getTheApp: 'Descarga la app', scanReceiptsOnTheGo: 'Escanea recibos desde tu teléfono', headsUp: '¡Atención!', + submitTo: 'Enviar a', + forwardTo: 'Reenviar a', }, supportalNoAccess: { title: 'No tan rápido', diff --git a/src/libs/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index fc4c83cb92b5c..09ed034212a65 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -993,6 +993,8 @@ function addMembersToWorkspace(invitedEmailsToAccountIDs: InvitedEmailsToAccount type PolicyMember = { email: string; role: string; + submitsTo?: string; + forwardsTo?: string; }; function importPolicyMembers(policyID: string, members: PolicyMember[]) { @@ -1003,7 +1005,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 { @@ -1017,7 +1019,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); diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 790556779fbfb..4f45d8aad1368 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -37,6 +37,8 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { {text: translate('common.ignore'), value: CONST.CSV_IMPORT_COLUMNS.IGNORE}, {text: translate('common.email'), value: CONST.CSV_IMPORT_COLUMNS.EMAIL, isRequired: true}, {text: translate('common.role'), value: CONST.CSV_IMPORT_COLUMNS.ROLE}, + {text: translate('common.submitTo'), value: CONST.CSV_IMPORT_COLUMNS.SUBMIT_TO}, + {text: translate('common.forwardTo'), value: CONST.CSV_IMPORT_COLUMNS.APPROVE_TO}, ]; const requiredColumns = columnRoles.filter((role) => role.isRequired).map((role) => role); @@ -74,15 +76,29 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { const membersRolesColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.ROLE); const membersEmails = spreadsheet?.data[membersEmailsColumn].map((email) => email); const membersRoles = membersRolesColumn !== -1 ? spreadsheet?.data[membersRolesColumn].map((role) => role) : []; + const membersSubmitsToColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.SUBMIT_TO); + const membersForwardsToColumn = columns.findIndex((column) => column === CONST.CSV_IMPORT_COLUMNS.APPROVE_TO); + const membersSubmitsTo = membersSubmitsToColumn !== -1 ? spreadsheet?.data[membersSubmitsToColumn].map((submitsTo) => submitsTo) : []; + const membersForwardsTo = membersForwardsToColumn !== -1 ? spreadsheet?.data[membersForwardsToColumn].map((forwardsTo) => forwardsTo) : []; const members = membersEmails?.slice(containsHeader ? 1 : 0).map((email, index) => { let role: string = CONST.POLICY.ROLE.USER; if (membersRolesColumn !== -1 && membersRoles?.[containsHeader ? index + 1 : index]) { role = membersRoles?.[containsHeader ? index + 1 : index]; } + let submitsTo = ''; + if (membersSubmitsToColumn !== -1 && membersSubmitsTo?.[containsHeader ? index + 1 : index]) { + submitsTo = membersSubmitsTo?.[containsHeader ? index + 1 : index]; + } + let forwardsTo = ''; + if (membersForwardsToColumn !== -1 && membersForwardsTo?.[containsHeader ? index + 1 : index]) { + forwardsTo = membersForwardsTo?.[containsHeader ? index + 1 : index]; + } return { email, role, + submitsTo, + forwardsTo, }; }); From 09029b84db713aea894a5d1e5f1a60a13285f84d Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Thu, 3 Jul 2025 15:23:08 +0700 Subject: [PATCH 02/29] Add import button inworkflow --- src/languages/en.ts | 1 + src/languages/es.ts | 1 + .../workflows/WorkspaceWorkflowsPage.tsx | 17 ++++++++++++++++- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index cdc10009cd284..0f3d4bd3e7902 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1854,6 +1854,7 @@ const translations = { submissionFrequencyDateOfMonth: 'Date of month', addApprovalsTitle: 'Add approvals', addApprovalButton: 'Add approval workflow', + importApprovalWorkflow: 'Import approval workflow', addApprovalTip: 'This default workflow applies to all members, unless a more specific workflow exists.', approver: 'Approver', connectBankAccount: 'Connect bank account', diff --git a/src/languages/es.ts b/src/languages/es.ts index 68b82ab915d0e..04117d2381103 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1853,6 +1853,7 @@ const translations = { submissionFrequencyDateOfMonth: 'Fecha del mes', addApprovalsTitle: 'Aprobaciones', addApprovalButton: 'Añadir flujo de aprobación', + importApprovalWorkflow: 'Importar flujo de aprobación', addApprovalTip: 'Este flujo de trabajo por defecto se aplica a todos los miembros, a menos que exista un flujo de trabajo más específico.', approver: 'Aprobador', connectBankAccount: 'Conectar cuenta bancaria', diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 71c2412240ffe..944710c0887d3 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -6,7 +6,7 @@ import ApprovalWorkflowSection from '@components/ApprovalWorkflowSection'; import ConfirmModal from '@components/ConfirmModal'; import getBankIcon from '@components/Icon/BankIcons'; import type {BankName} from '@components/Icon/BankIconsUtils'; -import {Plus} from '@components/Icon/Expensicons'; +import {Plus, Table} from '@components/Icon/Expensicons'; import {Workflows} from '@components/Icon/Illustrations'; import {LockedAccountContext} from '@components/LockedAccountModalProvider'; import MenuItem from '@components/MenuItem'; @@ -220,6 +220,21 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { style={[styles.sectionMenuItemTopDescription, styles.mt6, styles.mbn3]} onPress={addApprovalAction} /> + { + if (isAccountLocked) { + showLockedAccountModal(); + return; + } + Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(route.params.policyID)); + }} + /> ), disabled: isSmartLimitEnabled, From 6de9c44e0359a194d2d74d1dad2aad3dd0525d9a Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 9 Jul 2025 02:58:06 +0700 Subject: [PATCH 03/29] remove import in workflow page --- src/languages/en.ts | 1 - src/languages/es.ts | 1 - .../workflows/WorkspaceWorkflowsPage.tsx | 15 --------------- 3 files changed, 17 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 2413385187632..a894a4f3dceef 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1866,7 +1866,6 @@ const translations = { submissionFrequencyDateOfMonth: 'Date of month', addApprovalsTitle: 'Add approvals', addApprovalButton: 'Add approval workflow', - importApprovalWorkflow: 'Import approval workflow', addApprovalTip: 'This default workflow applies to all members, unless a more specific workflow exists.', approver: 'Approver', connectBankAccount: 'Connect bank account', diff --git a/src/languages/es.ts b/src/languages/es.ts index 4d311f0fad116..2e59ba9223366 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1865,7 +1865,6 @@ const translations = { submissionFrequencyDateOfMonth: 'Fecha del mes', addApprovalsTitle: 'Aprobaciones', addApprovalButton: 'Añadir flujo de aprobación', - importApprovalWorkflow: 'Importar flujo de aprobación', addApprovalTip: 'Este flujo de trabajo por defecto se aplica a todos los miembros, a menos que exista un flujo de trabajo más específico.', approver: 'Aprobador', connectBankAccount: 'Conectar cuenta bancaria', diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 19463b0e957c7..6bd53b127ef7f 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -220,21 +220,6 @@ function WorkspaceWorkflowsPage({policy, route}: WorkspaceWorkflowsPageProps) { style={[styles.sectionMenuItemTopDescription, styles.mt6, styles.mbn3]} onPress={addApprovalAction} /> - { - if (isAccountLocked) { - showLockedAccountModal(); - return; - } - Navigation.navigate(ROUTES.WORKSPACE_MEMBERS_IMPORT.getRoute(route.params.policyID)); - }} - /> ), disabled: isSmartLimitEnabled, From 58a9d6645d72db6cce920d5e9a3999dc93260b30 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 9 Jul 2025 02:58:51 +0700 Subject: [PATCH 04/29] fix import --- src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx index 6bd53b127ef7f..44f5ebc74adf5 100644 --- a/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx +++ b/src/pages/workspace/workflows/WorkspaceWorkflowsPage.tsx @@ -5,7 +5,7 @@ import ApprovalWorkflowSection from '@components/ApprovalWorkflowSection'; import ConfirmModal from '@components/ConfirmModal'; import getBankIcon from '@components/Icon/BankIcons'; import type {BankName} from '@components/Icon/BankIconsUtils'; -import {Plus, Table} from '@components/Icon/Expensicons'; +import {Plus} from '@components/Icon/Expensicons'; import {Workflows} from '@components/Icon/Illustrations'; import {LockedAccountContext} from '@components/LockedAccountModalProvider'; import MenuItem from '@components/MenuItem'; From 744b9838e67ee0138149b08b9208344288c53bd8 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 9 Jul 2025 03:03:16 +0700 Subject: [PATCH 05/29] fix ts --- src/languages/de.ts | 2 ++ src/languages/fr.ts | 2 ++ src/languages/it.ts | 2 ++ src/languages/ja.ts | 2 ++ src/languages/nl.ts | 2 ++ src/languages/pl.ts | 2 ++ src/languages/pt-BR.ts | 2 ++ src/languages/zh-hans.ts | 2 ++ 8 files changed, 16 insertions(+) diff --git a/src/languages/de.ts b/src/languages/de.ts index 1a37f9b6e5d71..703f6f31952ff 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -618,6 +618,8 @@ const translations = { getTheApp: 'Hole dir die App', scanReceiptsOnTheGo: 'Scannen Sie Belege von Ihrem Telefon aus', headsUp: 'Achtung!', + submitTo: 'Einreichen an', + forwardTo: 'Weiterleiten an', }, supportalNoAccess: { title: 'Nicht so schnell', diff --git a/src/languages/fr.ts b/src/languages/fr.ts index c2994e5a9f9b3..ec623c05dc7a9 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -618,6 +618,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 à', }, supportalNoAccess: { title: 'Pas si vite', diff --git a/src/languages/it.ts b/src/languages/it.ts index 397f4833dfe11..01f5d1f9205db 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -618,6 +618,8 @@ const translations = { getTheApp: "Scarica l'app", scanReceiptsOnTheGo: 'Scansiona le ricevute dal tuo telefono', headsUp: 'Attenzione!', + submitTo: 'Invia a', + forwardTo: 'Inoltra a', }, supportalNoAccess: { title: 'Non così in fretta', diff --git a/src/languages/ja.ts b/src/languages/ja.ts index e248d565ecd2a..87e8bcf533f7d 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -618,6 +618,8 @@ const translations = { getTheApp: 'アプリを入手', scanReceiptsOnTheGo: '携帯電話から領収書をスキャンする', headsUp: 'ご注意ください!', + submitTo: '送信先', + forwardTo: '転送先', }, supportalNoAccess: { title: 'ちょっと待ってください', diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 36a6a1eaac9ea..113fc614678ef 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -617,6 +617,8 @@ const translations = { getTheApp: 'Download de app', scanReceiptsOnTheGo: 'Scan bonnetjes vanaf je telefoon', headsUp: 'Let op!', + submitTo: 'Sturen naar', + forwardTo: 'Doorsturen naar', }, supportalNoAccess: { title: 'Niet zo snel', diff --git a/src/languages/pl.ts b/src/languages/pl.ts index d8596ff3e1681..3c7421e3ba710 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -618,6 +618,8 @@ const translations = { getTheApp: 'Pobierz aplikację', scanReceiptsOnTheGo: 'Skanuj paragony za pomocą telefonu', headsUp: 'Uwaga!', + submitTo: 'Wyślij do', + forwardTo: 'Przekaż do', }, supportalNoAccess: { title: 'Nie tak szybko', diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index 18caa03c74b75..74fb6119dfe92 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -617,6 +617,8 @@ const translations = { getTheApp: 'Obtenha o aplicativo', scanReceiptsOnTheGo: 'Digitalize recibos com seu celular', headsUp: 'Atenção!', + submitTo: 'Enviar para', + forwardTo: 'Encaminhar para', }, supportalNoAccess: { title: 'Não tão rápido', diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index 1d3efbae9d03d..c84bbe4a06ff3 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -617,6 +617,8 @@ const translations = { getTheApp: '获取应用程序', scanReceiptsOnTheGo: '用手机扫描收据', headsUp: '\u6CE8\u610F\uFF01', + submitTo: '提交到', + forwardTo: '转发到', }, supportalNoAccess: { title: '慢一点', From 18d4c1fd6cd31143c80e10257a92248a939215d7 Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 29 Jul 2025 22:34:48 +0700 Subject: [PATCH 06/29] add submitsTo and forwardsTo to member --- .../workspace/members/ImportedMembersPage.tsx | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 5fdf10de7e249..4e62c1ea66c61 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -7,11 +7,13 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useCloseImportPage from '@hooks/useCloseImportPage'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; +import usePolicy from '@hooks/usePolicy'; import {importPolicyMembers} from '@libs/actions/Policy/Member'; import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; +import {isPolicyMember} from '@libs/PolicyUtils'; import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -102,9 +104,32 @@ function ImportedMembersPage({route}: ImportedMembersPageProps) { }; }); - if (members) { + const allMembers = [...(members ?? [])]; + + // add submitsTo and forwardsTo members if they are not in the workspace + members?.forEach((member) => { + if (member.submitsTo && !isPolicyMember(member.submitsTo, policyID) && !allMembers.some((m) => m.email === member.submitsTo)) { + allMembers.push({ + email: member.submitsTo, + role: CONST.POLICY.ROLE.USER, + submitsTo: '', + forwardsTo: '', + }); + } + + if (member.forwardsTo && !isPolicyMember(member.forwardsTo, policyID) && !allMembers.some((m) => m.email === member.forwardsTo)) { + allMembers.push({ + email: member.forwardsTo, + role: CONST.POLICY.ROLE.USER, + submitsTo: '', + forwardsTo: '', + }); + } + }); + + if (allMembers) { setIsImporting(true); - importPolicyMembers(policyID, members); + importPolicyMembers(policyID, allMembers); } }, [validate, spreadsheet, containsHeader, policyID]); From 75a0f4158173aa38ae08e544e4441df2786a887c Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Tue, 29 Jul 2025 22:47:51 +0700 Subject: [PATCH 07/29] fix lint --- src/pages/workspace/members/ImportedMembersPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/workspace/members/ImportedMembersPage.tsx b/src/pages/workspace/members/ImportedMembersPage.tsx index 4e62c1ea66c61..b510263bdc840 100644 --- a/src/pages/workspace/members/ImportedMembersPage.tsx +++ b/src/pages/workspace/members/ImportedMembersPage.tsx @@ -7,7 +7,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import useCloseImportPage from '@hooks/useCloseImportPage'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; -import usePolicy from '@hooks/usePolicy'; import {importPolicyMembers} from '@libs/actions/Policy/Member'; import {findDuplicate, generateColumnNames} from '@libs/importSpreadsheetUtils'; import Navigation from '@libs/Navigation/Navigation'; From 0c532697d32ebf8ef78e06cd0efb285e7d8255ff Mon Sep 17 00:00:00 2001 From: nkdengineer Date: Wed, 30 Jul 2025 14:59:09 +0700 Subject: [PATCH 08/29] add confirmation step --- src/ONYXKEYS.ts | 5 + src/ROUTES.ts | 4 + src/languages/en.ts | 3 + src/languages/es.ts | 3 + src/languages/params.ts | 6 + .../ModalStackNavigators/index.tsx | 1 + .../RELATIONS/WORKSPACE_TO_RHP.ts | 1 + src/libs/Navigation/linkingConfig/config.ts | 3 + src/libs/Navigation/types.ts | 3 + src/libs/actions/Policy/Member.ts | 22 +- .../ImportedMembersConfirmationPage.tsx | 252 ++++++++++++++++++ .../workspace/members/ImportedMembersPage.tsx | 34 ++- .../onyx/ImportedSpreadsheetMemberData.ts | 16 ++ src/types/onyx/index.ts | 2 + 14 files changed, 344 insertions(+), 11 deletions(-) create mode 100644 src/pages/workspace/members/ImportedMembersConfirmationPage.tsx create mode 100644 src/types/onyx/ImportedSpreadsheetMemberData.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2e3cd62f5699c..7a19a4232e8a3 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -465,9 +465,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', @@ -1180,6 +1184,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 b660e1f8496a5..4f713d33be785 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -1341,6 +1341,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/languages/en.ts b/src/languages/en.ts index 38796810e7b47..9240b626b4577 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -100,6 +100,7 @@ import type { ImportedTagsMessageParams, ImportedTypesParams, ImportFieldParams, + ImportMemberConfirmationParams, ImportMembersSuccessfulDescriptionParams, ImportPerDiemRatesSuccessfulDescriptionParams, ImportTagsSuccessfulDescriptionParams, @@ -960,6 +961,8 @@ 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: ({totalMembers, newMembers}: ImportMemberConfirmationParams) => + `${newMembers} of the ${totalMembers} people you’ve uploaded is not yet a member of this workspace. The options below apply only to newly added people. Existing members in your upload will not have their settings changed, nor receive an email.`, }, receipt: { upload: 'Upload receipt', diff --git a/src/languages/es.ts b/src/languages/es.ts index 5f8ee07388d94..6119527e9b05a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -99,6 +99,7 @@ import type { ImportedTagsMessageParams, ImportedTypesParams, ImportFieldParams, + ImportMemberConfirmationParams, ImportMembersSuccessfulDescriptionParams, ImportPerDiemRatesSuccessfulDescriptionParams, ImportTagsSuccessfulDescriptionParams, @@ -954,6 +955,8 @@ 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: ({totalMembers, newMembers}: ImportMemberConfirmationParams) => + `${newMembers} de las ${totalMembers} personas que has subido aún no es miembro de este espacio de trabajo. Las opciones a continuación solo se aplican a las personas recién añadidas. Los miembros existentes incluidos en tu carga no tendrán cambios en su configuración ni recibirán un correo electrónico.`, }, receipt: { upload: 'Subir recibo', diff --git a/src/languages/params.ts b/src/languages/params.ts index 09a4ecfa20081..85bf351b28ac7 100644 --- a/src/languages/params.ts +++ b/src/languages/params.ts @@ -818,7 +818,13 @@ type SettlementAccountInfoParams = { accountNumber: string; }; +type ImportMemberConfirmationParams = { + totalMembers: number; + newMembers: number; +}; + export type { + ImportMemberConfirmationParams, ContactMethodsRouteParams, ContactMethodParams, SplitExpenseEditTitleParams, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx index 0bdb78b9be9ac..9422d3059559f 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators/index.tsx @@ -311,6 +311,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 8deb8304eb7f4..ebd62826dd05d 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 86406e46dcc55..a3dbb6a62aac9 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -188,6 +188,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/actions/Policy/Member.ts b/src/libs/actions/Policy/Member.ts index 09ed034212a65..bcef94603ae20 100644 --- a/src/libs/actions/Policy/Member.ts +++ b/src/libs/actions/Policy/Member.ts @@ -27,7 +27,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'; @@ -1316,6 +1326,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, @@ -1341,4 +1359,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..b9d30ed76f004 --- /dev/null +++ b/src/pages/workspace/members/ImportedMembersConfirmationPage.tsx @@ -0,0 +1,252 @@ +import React, {useCallback, 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 MultipleAvatars from '@components/MultipleAvatars'; +import {usePersonalDetails} from '@components/OnyxListItemProvider'; +import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; +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 {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 {getAvatarsForAccountIDs} from '@libs/OptionsListUtils'; +import {getAccountIDsByLogins} from '@libs/PersonalDetailsUtils'; +import NotFoundPage from '@pages/ErrorPage/NotFoundPage'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; +import WorkspaceMemberDetailsRoleSelectionModal from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; +import type {ListItemType} from '@pages/workspace/WorkspaceMemberRoleSelectionModal'; +import { isPolicyMember } from '@libs/PolicyUtils'; + +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(); + + const [importedSpreadsheetMemberData] = useOnyx(ONYXKEYS.IMPORTED_SPREADSHEET_MEMBER_DATA, {canBeMissing: true}); + const newMembers = useMemo(() => { + return importedSpreadsheetMemberData?.filter((member) => !isPolicyMember(member.email, policyID)) ?? []; + }, [importedSpreadsheetMemberData, policyID]); + 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, + ); + // eslint-disable-next-line react-compiler/react-compiler + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [importedSpreadsheetMemberData, personalDetails]); + + const [allPersonalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {canBeMissing: false}); + + /** 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 newMembersWithRole = newMembers.map((member) => ({...member, role})); + importPolicyMembers(policyID, newMembersWithRole); + }, [newMembers, policyID, role]); + + const closeImportPageAndModal = () => { + setIsClosing(true); + setIsImporting(false); + Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(policyID)); + }; + + const roleItems: ListItemType[] = useMemo( + () => [ + { + 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, + }, + ], + [role, translate], + ); + + if (!spreadsheet || !importedSpreadsheetMemberData) { + return ; + } + + return ( + + { + Navigation.goBack(); + }} + // onBackButtonPress={() => Navigation.goBack(route.params.backTo)} + /> + + + + + + {translate('spreadsheet.importMemberConfirmation', {totalMembers: importedSpreadsheetMemberData?.length, newMembers: newMembers?.length})} + + + + { + setIsRoleSelectionModalVisible(true); + }} + /> + + {/* { + setWelcomeNote(text); + }} + ref={(element: AnimatedTextInputRef) => { + if (!element) { + return; + } + if (!inputRef.current) { + updateMultilineInputRange(element); + } + inputCallbackRef(element); + }} + shouldSaveDraft + /> */} + + + {/* */} + {/* */} + +