diff --git a/src/CONST/index.ts b/src/CONST/index.ts index 02584e160216..abde1d6daf5d 100644 --- a/src/CONST/index.ts +++ b/src/CONST/index.ts @@ -9642,6 +9642,7 @@ const CONST = { HAS_MERGE_HR_SETUP_NEEDED: 'hasMergeHRSetupNeeded', HAS_HR_CONNECTION_ERROR: 'hasHRConnectionError', HAS_APPROVAL_WORKFLOW_NON_MEMBER_APPROVER: 'hasApprovalWorkflowNonMemberApprover', + HAS_AGENT_ERRORS: 'hasAgentErrors', }, DEBUG: { diff --git a/src/components/Navigation/DebugTabView.tsx b/src/components/Navigation/DebugTabView.tsx index e429b67bfef4..a1912b14459a 100644 --- a/src/components/Navigation/DebugTabView.tsx +++ b/src/components/Navigation/DebugTabView.tsx @@ -103,6 +103,8 @@ function getSettingsMessage(status: IndicatorStatus | undefined): TranslationPat return 'debug.indicatorStatus.completeHrSetup'; case CONST.INDICATOR_STATUS.HAS_HR_CONNECTION_ERROR: return 'debug.indicatorStatus.theresAProblemWithAnHRConnection'; + case CONST.INDICATOR_STATUS.HAS_AGENT_ERRORS: + return 'debug.indicatorStatus.theresAProblemWithYourAgent'; default: return undefined; } @@ -139,6 +141,8 @@ function getSettingsRoute(status: IndicatorStatus | undefined, reimbursementAcco case CONST.INDICATOR_STATUS.HAS_MERGE_HR_SETUP_NEEDED: case CONST.INDICATOR_STATUS.HAS_HR_CONNECTION_ERROR: return ROUTES.WORKSPACE_HR.getRoute(indicatorPolicyID); + case CONST.INDICATOR_STATUS.HAS_AGENT_ERRORS: + return ROUTES.SETTINGS_AGENTS; default: return undefined; } diff --git a/src/hooks/useAccountIndicatorChecks.ts b/src/hooks/useAccountIndicatorChecks.ts index 82a139d19fc8..36f789155f3f 100644 --- a/src/hooks/useAccountIndicatorChecks.ts +++ b/src/hooks/useAccountIndicatorChecks.ts @@ -1,4 +1,5 @@ import {hasPaymentMethodError} from '@libs/actions/PaymentMethods'; +import {hasAgentErrorsSelector} from '@libs/AgentUtils'; import {hasPartiallySetupBankAccount, hasPersonalBankAccountMissingInfo} from '@libs/BankAccountUtils'; import {hasCardPendingDigitalWalletApproval, hasPendingExpensifyCardAction} from '@libs/CardUtils'; import {hasSubscriptionGreenDotInfo, hasSubscriptionRedDotError} from '@libs/SubscriptionUtils'; @@ -39,6 +40,7 @@ function useAccountIndicatorChecks(): AccountIndicatorChecksResult { const [reimbursementAccount] = useOnyx(ONYXKEYS.REIMBURSEMENT_ACCOUNT); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const [hasDeviceManagementErrorValue] = useOnyx(ONYXKEYS.LOGINS, {selector: hasDeviceManagementError}); + const [hasAgentError] = useOnyx(ONYXKEYS.COLLECTION.AGENT, {selector: hasAgentErrorsSelector}); const { companyCards: {shouldShowRBR: hasCompanyCardFeedErrors}, @@ -68,6 +70,7 @@ function useAccountIndicatorChecks(): AccountIndicatorChecksResult { [CONST.INDICATOR_STATUS.HAS_EMPLOYEE_CARD_FEED_ERRORS]: !isPolicyAdmin ? hasCompanyCardFeedErrors : false, [CONST.INDICATOR_STATUS.HAS_LOCKED_BANK_ACCOUNT]: Object.values(bankAccountList ?? {}).some((bankAccount) => bankAccount?.accountData?.state === CONST.BANK_ACCOUNT.STATE.LOCKED), [CONST.INDICATOR_STATUS.HAS_DEVICE_MANAGEMENT_ERROR]: hasDeviceManagementErrorValue, + [CONST.INDICATOR_STATUS.HAS_AGENT_ERRORS]: !!hasAgentError, }; const infoChecks: Partial> = { diff --git a/src/hooks/useAgents.ts b/src/hooks/useAgentsTable.ts similarity index 92% rename from src/hooks/useAgents.ts rename to src/hooks/useAgentsTable.ts index 305c08ee2a72..cd280a944cf1 100644 --- a/src/hooks/useAgents.ts +++ b/src/hooks/useAgentsTable.ts @@ -3,9 +3,9 @@ import {usePersonalDetails} from '@components/OnyxListItemProvider'; import type {TableHandle} from '@components/Table'; import type {AgentRowData, AgentsTableColumnKey} from '@components/Tables/AgentsTable'; -import {clearAgentDeleteError, clearAgentError, clearAgentUpdateError, deleteAgent, openAgentsPage} from '@libs/actions/Agent'; +import {clearAgentDeleteError, clearAgentError, clearAgentUpdateError, deleteAgent} from '@libs/actions/Agent'; import {getRuleBotEnforcedPolicy} from '@libs/AgentRulesUtils'; -import {getLatestError} from '@libs/ErrorUtils'; +import {getAgentErrors} from '@libs/AgentUtils'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -43,11 +43,7 @@ function handleErrorClose(pendingAction: PendingAction | null | undefined, accou /** * Hook that derives the current user's manageable agent rows from Onyx and exposes selection and bulk-delete helpers for the Agents table. */ -function useAgents() { - useEffect(() => { - openAgentsPage(); - }, []); - +function useAgentsTable() { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const [allAgents] = useOnyx(ONYXKEYS.COLLECTION.AGENT); @@ -72,8 +68,7 @@ function useAgents() { } const pendingAction = agent.pendingAction; const isPendingDeletion = pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - - if (!isOffline && isPendingDeletion) { + if (isPendingDeletion && !isOffline) { return []; } @@ -92,13 +87,7 @@ function useAgents() { ownerDisplayName = domainNameSelector(domain) ?? (ownerDetails?.login ? Str.extractEmailDomain(ownerDetails.login) : undefined); } - const mergedErrors = { - ...getLatestError(agent.errors ?? undefined), - ...getLatestError(agent.nameErrors ?? undefined), - ...getLatestError(agent.promptErrors ?? undefined), - ...getLatestError(agent.avatarErrors ?? undefined), - }; - const rowErrors = getLatestError(mergedErrors); + const agentErrors = getAgentErrors(agent); return [ { @@ -111,7 +100,7 @@ function useAgents() { ownerType: agent.ownerType, ownerPolicyID, pendingAction, - errors: Object.keys(rowErrors).length > 0 ? rowErrors : undefined, + errors: Object.keys(agentErrors ?? {}).length > 0 ? agentErrors : undefined, disabled: isPendingDeletion, canManage: true, action: () => Navigation.navigate(createDynamicRoute(DYNAMIC_ROUTES.AGENT_EDIT.getRoute(accountID))), @@ -169,6 +158,7 @@ function useAgents() { } clearSelectedAgents(); }; + const askForConfirmationToDelete = async () => { const ruleBotEnforcedPolicy = selectedAgentKeys.map((accountIDString) => getRuleBotEnforcedPolicy(Number(accountIDString), allPolicies)).find(Boolean); if (ruleBotEnforcedPolicy) { @@ -194,4 +184,4 @@ function useAgents() { return {agents, selectedAgentKeys, setSelectedAgents, clearSelectedAgents, askForConfirmationToDelete, tableRef}; } -export default useAgents; +export default useAgentsTable; diff --git a/src/hooks/useWorkspaceAgents.ts b/src/hooks/useWorkspaceAgents.ts index a62326007db6..6bd7e069fcad 100644 --- a/src/hooks/useWorkspaceAgents.ts +++ b/src/hooks/useWorkspaceAgents.ts @@ -1,71 +1,59 @@ import CONST from '@src/CONST'; -import type {Policy} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Agent, Policy} from '@src/types/onyx'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import useAgents from './useAgents'; import useNetwork from './useNetwork'; +import useOnyx from './useOnyx'; import useWorkspaceAccountID from './useWorkspaceAccountID'; import useWorkspaceMembers from './useWorkspaceMembers'; /** - * Hook that returns the agent rows for a workspace: agents the user can manage joined with their workspace role, plus read-only rows for custom-agent members the user cannot manage. + * Hook that derives the policy's agents, including both agents owned by the policy and member agents. */ function useWorkspaceAgents(policy: OnyxEntry) { const {isOffline} = useNetwork(); + const [allAgents] = useOnyx(ONYXKEYS.COLLECTION.AGENT); const policyID = policy?.id; const workspaceAccountID = useWorkspaceAccountID(policyID); const workspaceMembers = useWorkspaceMembers(policy); - const policyRoles = Object.values(CONST.POLICY.ROLE); - const {agents, ...rest} = useAgents(); - const workspaceAgents = agents.flatMap((agent) => { - const agentMembership = workspaceMembers.find((member) => member.accountID === agent.accountID); - const isAgentOwnedByWorkspace = agent.ownerAccountID === workspaceAccountID; - const isAgentMemberOfWorkspace = !!agentMembership; - if (!isAgentOwnedByWorkspace && !isAgentMemberOfWorkspace) { - return []; - } - // Attach the workspace role to the agent row. The agent object is local to this flatMap and not shared - // eslint-disable-next-line no-param-reassign - agent.role = policyRoles.find((policyRole) => policyRole === agentMembership?.policyEmployee.role); - return [agent]; - }); + const manageableWorkspaceAgents: OnyxCollection = Object.fromEntries( + Object.entries(allAgents ?? {}).filter(([key, agent]) => { + if (!agent) { + return false; + } + const isPendingDeletion = agent.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; + if (isPendingDeletion && !isOffline) { + return false; + } + const accountID = Number(key.slice(ONYXKEYS.COLLECTION.AGENT.length)); + const isAgentOwnedByWorkspace = agent.ownerAccountID === workspaceAccountID; + const isAgentMemberOfWorkspace = !!workspaceMembers.find((member) => member.accountID === accountID); + if (!isAgentOwnedByWorkspace && !isAgentMemberOfWorkspace) { + return false; + } + return true; + }), + ); + const manageableWorkspaceAgentsIDs = new Set(Object.keys(manageableWorkspaceAgents).map((key) => Number(key.slice(ONYXKEYS.COLLECTION.AGENT.length)))); - // The useAgents hook only includes agents that the user can manage - // We need to include other agents in the workspace too - const manageableAgentAccountIDs = new Set(workspaceAgents.map(({accountID}) => accountID)); - for (const workspaceMember of workspaceMembers) { - const {policyEmployee, accountID, details} = workspaceMember; + const nonManageableWorkspaceAgentsIDs: number[] = []; + for (const {accountID, details} of workspaceMembers) { if (!details.isCustomAgent) { continue; } - if (manageableAgentAccountIDs.has(accountID)) { - continue; - } - const pendingAction = policyEmployee.pendingAction; - const isPendingDeletion = pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; - if (!isOffline && isPendingDeletion) { + if (manageableWorkspaceAgentsIDs.has(accountID)) { continue; } - workspaceAgents.push({ - keyForList: String(accountID), - accountID, - displayName: details.displayName ?? details.login ?? '', - login: details.login ?? '', - role: policyRoles.find((policyRole) => policyRole === policyEmployee.role), - pendingAction, - disabled: true, - isSelectionDisabled: true, - canManage: false, - action: () => {}, - onChatPress: () => {}, - onCopilotPress: () => {}, - dismissError: () => {}, - }); + nonManageableWorkspaceAgentsIDs.push(accountID); } - return {agents: workspaceAgents, ...rest}; + return { + manageableWorkspaceAgents, + nonManageableWorkspaceAgentsIDs, + }; } export default useWorkspaceAgents; diff --git a/src/hooks/useWorkspaceAgentsTable.ts b/src/hooks/useWorkspaceAgentsTable.ts new file mode 100644 index 000000000000..1a3937d6c5f1 --- /dev/null +++ b/src/hooks/useWorkspaceAgentsTable.ts @@ -0,0 +1,73 @@ +import type {AgentRowData} from '@components/Tables/AgentsTable'; + +import Log from '@libs/Log'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy} from '@src/types/onyx'; + +import type {OnyxEntry} from 'react-native-onyx'; + +import useAgentsTable from './useAgentsTable'; +import useWorkspaceAgents from './useWorkspaceAgents'; +import useWorkspaceMembers from './useWorkspaceMembers'; + +/** + * Hook that returns the agent rows for a workspace: agents the user can manage joined with their workspace role, plus read-only rows for custom-agent members the user cannot manage. + */ +function useWorkspaceAgentsTable(policy: OnyxEntry) { + const policyID = policy?.id; + const policyRoles = Object.values(CONST.POLICY.ROLE); + const workspaceMembers = useWorkspaceMembers(policy); + + const {agents, ...rest} = useAgentsTable(); + const {manageableWorkspaceAgents, nonManageableWorkspaceAgentsIDs} = useWorkspaceAgents(policy); + + const workspaceAgents: AgentRowData[] = []; + for (const key of Object.keys(manageableWorkspaceAgents)) { + const accountID = Number(key.slice(ONYXKEYS.COLLECTION.AGENT.length)); + const agent = agents.find((agentItem) => agentItem.accountID === accountID); + + if (!agent) { + Log.warn('Manageable agent is not found in agents list', {accountID, policyID}); + continue; + } + + // Attach role if agent is a member of the policy + const agentMembership = workspaceMembers.find((member) => member.accountID === accountID); + if (agentMembership) { + const role = agentMembership.policyEmployee.role; + agent.role = policyRoles.find((policyRole) => policyRole === role); + } + + workspaceAgents.push(agent); + } + for (const accountID of nonManageableWorkspaceAgentsIDs) { + const agentMembership = workspaceMembers.find((member) => member.accountID === accountID); + + if (!agentMembership) { + Log.warn('Non-manageable agent is not found in members list', {accountID, policyID}); + continue; + } + + workspaceAgents.push({ + keyForList: String(accountID), + accountID, + displayName: agentMembership.details.displayName ?? agentMembership.details.login ?? '', + login: agentMembership.details.login ?? '', + role: policyRoles.find((policyRole) => policyRole === agentMembership.policyEmployee.role), + pendingAction: agentMembership.policyEmployee.pendingAction, + disabled: true, + isSelectionDisabled: true, + canManage: false, + action: () => {}, + onChatPress: () => {}, + onCopilotPress: () => {}, + dismissError: () => {}, + }); + } + + return {agents: workspaceAgents, ...rest}; +} + +export default useWorkspaceAgentsTable; diff --git a/src/hooks/useWorkspaceMembers.ts b/src/hooks/useWorkspaceMembers.ts index e82e4951cbca..fa8c557aca55 100644 --- a/src/hooks/useWorkspaceMembers.ts +++ b/src/hooks/useWorkspaceMembers.ts @@ -1,52 +1,31 @@ -import {openWorkspaceMembersPage} from '@libs/actions/Policy/Member'; -import {arePaymentsEnabled, shouldFilterExpensifyTeam, isDeletedPolicyEmployee, isExpensifyTeam, isPolicyAdmin, canMemberRead, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; +import {shouldFilterExpensifyTeam, isDeletedPolicyEmployee, isExpensifyTeam, getMemberAccountIDsForWorkspace} from '@libs/PolicyUtils'; import {generateAccountID} from '@libs/UserUtils'; -import CONST from '@src/CONST'; import type {PersonalDetails, Policy, PolicyEmployee} from '@src/types/onyx'; import {isEmptyValueObject} from '@src/types/utils/EmptyObject'; import type {OnyxEntry} from 'react-native-onyx'; -import {useEffectEvent, useEffect} from 'react'; - import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; import useLocalize from './useLocalize'; import useNetwork from './useNetwork'; import {usePersonalDetailsByLogins} from './usePersonalDetailByLogin'; -import {useAllPersonalDetails} from './usePersonalDetails'; +import {usePersonalDetailsByIDs} from './usePersonalDetails'; /** * Hook that returns a workspace's members joined with personal details, filling in fallback identities when details haven't loaded and filtering out Expensify team members on Expensify-owned policies. */ function useWorkspaceMembers(policy: OnyxEntry) { - const policyID = policy?.id; const policyOwner = policy?.owner; - const {formatPhoneNumber} = useLocalize(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserLogin = currentUserPersonalDetails.login; const employeePersonalDetails = usePersonalDetailsByLogins(Object.keys(policy?.employeeList ?? {})); const policyMemberEmailsToAccountIDs = getMemberAccountIDsForWorkspace(policy?.employeeList, employeePersonalDetails, true); + const policyMemberAccountIDs = Object.values(policyMemberEmailsToAccountIDs); const shouldFilter = shouldFilterExpensifyTeam(policyOwner, currentUserLogin); - const [personalDetails] = useAllPersonalDetails(); - - const getWorkspaceMembers = () => { - if (!policyID) { - return; - } - if (!canMemberRead(policy, currentUserPersonalDetails.login ?? '', CONST.POLICY.POLICY_FEATURE.MEMBERS)) { - return; - } - const clientMemberEmails = Object.keys(getMemberAccountIDsForWorkspace(policy?.employeeList, employeePersonalDetails)); - openWorkspaceMembersPage(policyID, clientMemberEmails); - }; - const {isOffline} = useNetwork({onReconnect: getWorkspaceMembers}); - const getWorkspaceMembersEvent = useEffectEvent(() => getWorkspaceMembers()); - const shouldShowMemberBankAccounts = isPolicyAdmin(policy) && arePaymentsEnabled(policy); - - // The bank accounts are only returned while the workspace shows them, so load the members again when that changes. - useEffect(() => getWorkspaceMembersEvent(), [shouldShowMemberBankAccounts]); + const [personalDetails] = usePersonalDetailsByIDs(policyMemberAccountIDs); + const {isOffline} = useNetwork(); const result: Array<{email: string; policyEmployee: PolicyEmployee; accountID: number; details: PersonalDetails}> = []; for (const [email, policyEmployee] of Object.entries(policy?.employeeList ?? {})) { diff --git a/src/languages/de.ts b/src/languages/de.ts index 6f11c7487d2a..50b52a7f0065 100644 --- a/src/languages/de.ts +++ b/src/languages/de.ts @@ -11939,6 +11939,7 @@ Fügen Sie weitere Ausgabelimits hinzu, um den Cashflow Ihres Unternehmens zu sc aBankAccountIsLocked: 'Ein Bankkonto ist gesperrt', completeHrSetup: 'HR-Einrichtung abschließen', theresAProblemWithAnHRConnection: 'Es gibt ein Problem mit einer HR-Verbindung', + theresAProblemWithYourAgent: 'Es gibt ein Problem mit Ihrem/Ihren Agent(en)', }, }, emptySearchView: { diff --git a/src/languages/el.ts b/src/languages/el.ts index b127085d047f..d971e3d7f6b7 100644 --- a/src/languages/el.ts +++ b/src/languages/el.ts @@ -12155,6 +12155,7 @@ ${reportName}`, aBankAccountIsLocked: 'Ένας τραπεζικός λογαριασμός είναι κλειδωμένος', completeHrSetup: 'Ολοκληρώστε τη ρύθμιση HR', theresAProblemWithAnHRConnection: 'Υπάρχει πρόβλημα με μια σύνδεση HR', + theresAProblemWithYourAgent: 'Υπάρχει πρόβλημα με τον/τους εκπρόσωπό σας', }, }, emptySearchView: { diff --git a/src/languages/en.ts b/src/languages/en.ts index 6a3d50b964fc..68dbcfcf5e91 100644 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -12206,6 +12206,7 @@ const translations = { aBankAccountIsLocked: 'A bank account is locked', completeHrSetup: 'Complete HR setup', theresAProblemWithAnHRConnection: "There's a problem with an HR connection", + theresAProblemWithYourAgent: "There's a problem with your agent(s)", }, }, emptySearchView: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 3e7c4bbabfab..ac2b51f02a8f 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -12122,6 +12122,7 @@ ${reportName}`, aBankAccountIsLocked: 'Una cuenta bancaria está bloqueada', completeHrSetup: 'Completa la configuración de RR. HH.', theresAProblemWithAnHRConnection: 'Hay un problema con una conexión de RR. HH.', + theresAProblemWithYourAgent: 'Hay un problema con tu(s) agente(s)', }, }, emptySearchView: { diff --git a/src/languages/fr.ts b/src/languages/fr.ts index 16b4140858b7..ddf946bf0993 100644 --- a/src/languages/fr.ts +++ b/src/languages/fr.ts @@ -11965,6 +11965,7 @@ Ajoutez davantage de règles de dépenses pour protéger la trésorerie de l’e aBankAccountIsLocked: 'Un compte bancaire est verrouillé', completeHrSetup: 'Terminer la configuration RH', theresAProblemWithAnHRConnection: 'Un problème est survenu avec une connexion RH', + theresAProblemWithYourAgent: 'Il y a un problème avec votre/vos agent(s)', }, }, emptySearchView: { diff --git a/src/languages/it.ts b/src/languages/it.ts index 2620ffeacbe9..483c54e659da 100644 --- a/src/languages/it.ts +++ b/src/languages/it.ts @@ -11891,6 +11891,7 @@ Aggiungi altre regole di spesa per proteggere il flusso di cassa aziendale.`, aBankAccountIsLocked: 'Un conto bancario è bloccato', completeHrSetup: 'Completa la configurazione HR', theresAProblemWithAnHRConnection: 'C’è un problema con una connessione HR', + theresAProblemWithYourAgent: "C'è un problema con il tuo agente/i tuoi agenti", }, }, emptySearchView: { diff --git a/src/languages/ja.ts b/src/languages/ja.ts index 4445b9e7d196..29d279a7f9ce 100644 --- a/src/languages/ja.ts +++ b/src/languages/ja.ts @@ -11713,6 +11713,7 @@ ${reportName}`, aBankAccountIsLocked: '銀行口座がロックされています', completeHrSetup: '人事設定を完了する', theresAProblemWithAnHRConnection: '人事連携に問題が発生しています', + theresAProblemWithYourAgent: 'エージェントに問題があります', }, }, emptySearchView: { diff --git a/src/languages/nl.ts b/src/languages/nl.ts index 78e4091355d2..03187d0f7333 100644 --- a/src/languages/nl.ts +++ b/src/languages/nl.ts @@ -11855,6 +11855,7 @@ er bestedingsregels toe om de kasstroom van het bedrijf te beschermen.`, aBankAccountIsLocked: 'Een bankrekening is geblokkeerd', completeHrSetup: 'HR-configuratie voltooien', theresAProblemWithAnHRConnection: 'Er is een probleem met een HR-verbinding', + theresAProblemWithYourAgent: 'Er is een probleem met je agent(en)', }, }, emptySearchView: { diff --git a/src/languages/pl.ts b/src/languages/pl.ts index 6577dad67a95..6753f10bbd05 100644 --- a/src/languages/pl.ts +++ b/src/languages/pl.ts @@ -11884,6 +11884,7 @@ Dodaj więcej zasad wydatków, żeby chronić płynność finansową firmy.`, aBankAccountIsLocked: 'Konto bankowe jest zablokowane', completeHrSetup: 'Dokończ konfigurację HR', theresAProblemWithAnHRConnection: 'Wystąpił problem z połączeniem HR', + theresAProblemWithYourAgent: 'Wystąpił problem z twoim(i) agentem(ami)', }, }, emptySearchView: { diff --git a/src/languages/pt-BR.ts b/src/languages/pt-BR.ts index dc089ccf0c9b..b6d0576fbb2c 100644 --- a/src/languages/pt-BR.ts +++ b/src/languages/pt-BR.ts @@ -11843,6 +11843,7 @@ Adicione mais regras de gasto para proteger o fluxo de caixa da empresa.`, aBankAccountIsLocked: 'Uma conta bancária está bloqueada', completeHrSetup: 'Concluir configuração de RH', theresAProblemWithAnHRConnection: 'Há um problema com uma conexão de RH', + theresAProblemWithYourAgent: 'Há um problema com seu(s) agente(s)', }, }, emptySearchView: { diff --git a/src/languages/zh-hans.ts b/src/languages/zh-hans.ts index d084f57aadb3..edd57f3d721a 100644 --- a/src/languages/zh-hans.ts +++ b/src/languages/zh-hans.ts @@ -11423,6 +11423,7 @@ ${reportName}`, aBankAccountIsLocked: '银行账户已锁定', completeHrSetup: '完成人力资源设置', theresAProblemWithAnHRConnection: 'HR 连接出现问题', + theresAProblemWithYourAgent: '您的代理存在问题', }, }, emptySearchView: { diff --git a/src/libs/AgentUtils.ts b/src/libs/AgentUtils.ts new file mode 100644 index 000000000000..9d145b70ae57 --- /dev/null +++ b/src/libs/AgentUtils.ts @@ -0,0 +1,35 @@ +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Agent} from '@src/types/onyx'; +import type {Errors} from '@src/types/onyx/OnyxCommon'; + +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; + +import {getLatestError} from './ErrorUtils'; + +function getAgentErrors(agent: OnyxEntry): Errors | undefined { + if (!agent) { + return undefined; + } + const mergedErrors = { + ...getLatestError(agent.errors ?? undefined), + ...getLatestError(agent.nameErrors ?? undefined), + ...getLatestError(agent.promptErrors ?? undefined), + ...getLatestError(agent.avatarErrors ?? undefined), + }; + return getLatestError(mergedErrors); +} + +function doesAgentHaveErrors(agent: OnyxEntry): boolean { + const agentErrors = getAgentErrors(agent); + return Object.keys(agentErrors ?? {}).length > 0; +} + +function hasAgentErrorsSelector(agents: OnyxCollection): boolean { + return Object.values(agents ?? {}).some(doesAgentHaveErrors); +} + +function agentAccountIDsSelector(agents: OnyxCollection): number[] { + return Object.keys(agents ?? {}).map((key) => Number(key.slice(ONYXKEYS.COLLECTION.AGENT.length))); +} + +export {getAgentErrors, hasAgentErrorsSelector, agentAccountIDsSelector}; diff --git a/src/libs/actions/Agent.ts b/src/libs/actions/Agent.ts index 8b53c3784ae0..9811e8114694 100644 --- a/src/libs/actions/Agent.ts +++ b/src/libs/actions/Agent.ts @@ -384,6 +384,9 @@ function deleteAgent(accountID: number, agentLogin?: string, allPolicies?: OnyxC key: `${ONYXKEYS.COLLECTION.AGENT}${agentAccountID}`, value: {pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE}, }, + + // Set accountID to 0 so the agent is not included in personal details list (e.g. so you can't invite an agent that you are deleting) + buildPersonalDetailsUpdate({[agentAccountID]: {accountID: CONST.DEFAULT_NUMBER_ID}}), ]; const successData: AnyOnyxUpdate[] = [ @@ -404,6 +407,7 @@ function deleteAgent(accountID: number, agentLogin?: string, allPolicies?: OnyxC errors: getMicroSecondOnyxErrorWithTranslationKey('common.genericErrorMessage'), }, }, + buildPersonalDetailsUpdate({[agentAccountID]: {accountID: agentAccountID}}), ]; // Mark the agent's row pending-delete on every policy it belongs to so workflow cards render diff --git a/src/pages/settings/Agents/AgentsPage.tsx b/src/pages/settings/Agents/AgentsPage.tsx index 5d5302c1cc91..ee79b65c81a6 100644 --- a/src/pages/settings/Agents/AgentsPage.tsx +++ b/src/pages/settings/Agents/AgentsPage.tsx @@ -7,7 +7,7 @@ import RenderHTML from '@components/RenderHTML'; import ScreenWrapper from '@components/ScreenWrapper'; import AgentsTable from '@components/Tables/AgentsTable'; -import useAgents from '@hooks/useAgents'; +import useAgentsTable from '@hooks/useAgentsTable'; import useDocumentTitle from '@hooks/useDocumentTitle'; import useLayoutSpacing from '@hooks/useLayoutSpacing'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; @@ -17,6 +17,7 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useShouldDisplayButtonsInSeparateLine from '@hooks/useShouldDisplayButtonsInSeparateLine'; import useThemeStyles from '@hooks/useThemeStyles'; +import {openAgentsPage} from '@libs/actions/Agent'; import {turnOffMobileSelectionMode} from '@libs/actions/MobileSelectionMode'; import createDynamicRoute from '@libs/Navigation/helpers/dynamicRoutesUtils/createDynamicRoute'; import Navigation from '@libs/Navigation/Navigation'; @@ -25,6 +26,7 @@ import CONST from '@src/CONST'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; +import {useEffect} from 'react'; import {View} from 'react-native'; function AgentsPage() { @@ -36,12 +38,13 @@ function AgentsPage() { const shouldDisplayButtonsInSeparateLine = useShouldDisplayButtonsInSeparateLine(); const selectionModeHeader = isMobileSelectionModeEnabled && shouldUseNarrowLayout; const icons = useMemoizedLazyExpensifyIcons(['Plus', 'Trashcan']); - const {agents, selectedAgentKeys, setSelectedAgents, clearSelectedAgents, askForConfirmationToDelete, tableRef} = useAgents(); + const {agents, selectedAgentKeys, setSelectedAgents, clearSelectedAgents, askForConfirmationToDelete, tableRef} = useAgentsTable(); const hasAgents = agents.length > 0; const canSelectMultiple = shouldUseNarrowLayout ? isMobileSelectionModeEnabled : true; const shouldShowBulkActionsButton = shouldUseNarrowLayout ? canSelectMultiple : selectedAgentKeys.length > 0; useDocumentTitle(translate('agentsPage.title')); + useEffect(() => openAgentsPage(), []); const newAgentButton = hasAgents ? (