- {renderRecordsSearch()}
+
+ {renderSearch()}
{renderContent()}
diff --git a/react/features/salesforce/components/web/SearchResultsSection.tsx b/react/features/salesforce/components/web/SearchResultsSection.tsx
new file mode 100644
index 000000000000..1f84eb8ac018
--- /dev/null
+++ b/react/features/salesforce/components/web/SearchResultsSection.tsx
@@ -0,0 +1,167 @@
+import React from 'react';
+import { useTranslation } from 'react-i18next';
+import { makeStyles } from 'tss-react/mui';
+
+import {
+ IconRecordAccount,
+ IconRecordContact,
+ IconRecordLead,
+ IconRecordOpportunity
+} from '../../../base/icons/svg';
+import { BUTTON_TYPES } from '../../../base/ui/constants.web';
+import {
+ IAccountMatch,
+ IContactMatch,
+ ILeadMatch,
+ IOpportunityMatch,
+ ISearchResults,
+ SalesforceObjectType
+} from '../../types';
+
+import { RecordListItem } from './RecordListItem';
+
+interface IProps {
+ linkingId: string | null;
+ onLink: (type: SalesforceObjectType, data: IAccountMatch | ILeadMatch | IContactMatch | IOpportunityMatch) => void;
+ results: ISearchResults;
+}
+
+const useStyles = makeStyles()(theme => {
+ return {
+ section: {
+ marginBottom: '16px'
+ },
+ groupTitle: {
+ fontSize: '0.75rem',
+ fontWeight: 600,
+ color: theme.palette.text03,
+ textTransform: 'uppercase',
+ margin: '16px 0 8px 0'
+ },
+ list: {
+ listStyle: 'none',
+ margin: 0,
+ padding: 0
+ },
+ noResults: {
+ textAlign: 'center',
+ color: theme.palette.text03,
+ padding: '24px'
+ }
+ };
+});
+
+/**
+ * Component for displaying Salesforce search results with link buttons.
+ *
+ * @param {IProps} props - Component props.
+ * @returns {React.ReactElement}
+ */
+export const SearchResultsSection = ({ results, linkingId, onLink }: IProps) => {
+ const { t } = useTranslation();
+ const { classes } = useStyles();
+
+ const hasResults = results.accounts.length > 0
+ || results.leads.length > 0
+ || results.contacts.length > 0
+ || results.opportunities.length > 0;
+
+ if (!hasResults) {
+ return (
+
+ {t('dialog.searchResultsNotFound')}
+
+ );
+ }
+
+ return (
+
+ {results.accounts.length > 0 && (
+ <>
+
{t('record.type.account')}
+
+ {results.accounts.map(account => (
+ onLink('Account', account) } />
+ ))}
+
+ >
+ )}
+
+ {results.leads.length > 0 && (
+ <>
+
{t('record.type.lead')}
+
+ {results.leads.map(lead => (
+ onLink('Lead', lead) } />
+ ))}
+
+ >
+ )}
+
+ {results.contacts.length > 0 && (
+ <>
+
{t('record.type.contact')}
+
+ {results.contacts.map(contact => (
+ onLink('Contact', contact) } />
+ ))}
+
+ >
+ )}
+
+ {results.opportunities.length > 0 && (
+ <>
+
{t('record.type.opportunity')}
+
+ {results.opportunities.map(opp => (
+ onLink('Opportunity', opp) } />
+ ))}
+
+ >
+ )}
+
+ );
+};
+
+export default SearchResultsSection;
diff --git a/react/features/salesforce/functions.ts b/react/features/salesforce/functions.ts
index e117f674bb8a..322b110b0e95 100644
--- a/react/features/salesforce/functions.ts
+++ b/react/features/salesforce/functions.ts
@@ -1,7 +1,19 @@
import { IReduxState } from '../app/types';
-import { doGetJSON } from '../base/util/httpUtils';
import { isInBreakoutRoom } from '../breakout-rooms/functions';
+import {
+ IAccountMatch,
+ IConfirmAccountResult,
+ IConfirmDealResult,
+ IContactMatch,
+ ILeadMatch,
+ ILinkResult,
+ IOpportunityMatch,
+ ISalesforceData,
+ ISearchResults,
+ SalesforceObjectType
+} from './types';
+
/**
* Determines whether Salesforce is enabled for the current conference.
*
@@ -17,98 +29,188 @@ export const isSalesforceEnabled = (state: IReduxState) => {
};
/**
- * Fetches the Salesforce records that were most recently interacted with.
+ * Helper to make authenticated requests to the meet-metrics API.
*
- * @param {string} url - The endpoint for the session records.
- * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} url - The full URL to fetch.
+ * @param {string} jwt - The JWT token for authentication.
+ * @param {RequestInit} options - Additional fetch options.
* @returns {Promise
}
*/
-export async function getRecentSessionRecords(
- url: string,
- jwt: string
-) {
- return doGetJSON(`${url}/records/recents`, true, {
+async function fetchWithAuth(url: string, jwt: string, options: RequestInit = {}): Promise {
+ const res = await fetch(url, {
+ ...options,
headers: {
- 'Authorization': `Bearer ${jwt}`
+ 'Content-Type': 'application/json',
+ 'Authorization': `Bearer ${jwt}`,
+ ...options.headers
}
});
+
+ const json = await res.json();
+
+ return res.ok ? json : Promise.reject(json);
}
/**
- * Fetches the Salesforce records that match the search criteria.
+ * Fetches the Salesforce data for a session including current links and pending suggestions.
*
- * @param {string} url - The endpoint for the session records.
+ * @param {string} url - The base URL for the meet-metrics API.
* @param {string} jwt - The JWT needed for authentication.
- * @param {string} text - The search term for the session record to find.
- * @returns {Promise}
+ * @param {string} sessionId - The meeting session ID.
+ * @returns {Promise}
*/
-export async function searchSessionRecords(
+export async function getSessionSalesforceData(
url: string,
jwt: string,
- text: string
-) {
- return doGetJSON(`${url}/records?text=${text}`, true, {
- headers: {
- 'Authorization': `Bearer ${jwt}`
- }
+ sessionId: string
+): Promise {
+ const response = await fetchWithAuth(`${url}/api/sessions/crm`, jwt, {
+ method: 'POST',
+ body: JSON.stringify({ sessionIds: [ sessionId ] })
});
+
+ // Extract data for this specific session from the batch response
+ const sessionData = response?.crmDataBySession?.[sessionId]?.salesforce;
+
+ return sessionData || null;
}
/**
-* Fetches the Salesforce record details from the server.
-*
-* @param {string} url - The endpoint for the record details.
-* @param {string} jwt - The JWT needed for authentication.
-* @param {Object} item - The item for which details are being retrieved.
-* @returns {Promise}
-*/
-export async function getSessionRecordDetails(
+ * Searches Salesforce objects (Accounts, Leads, Contacts, Opportunities).
+ *
+ * @param {string} url - The base URL for the meet-metrics API.
+ * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} query - The search query (minimum 2 characters).
+ * @returns {Promise}
+ */
+export async function searchSalesforce(
url: string,
jwt: string,
- item: {
- id: string;
- type: string;
- } | null
-) {
- const fullUrl = `${url}/records/${item?.id}?type=${item?.type}`;
-
- return doGetJSON(fullUrl, true, {
- headers: {
- 'Authorization': `Bearer ${jwt}`
- }
+ query: string
+): Promise {
+ return fetchWithAuth(`${url}/api/salesforce/search?q=${encodeURIComponent(query)}`, jwt);
+}
+
+/**
+ * Links a session to a Salesforce object.
+ *
+ * @param {string} url - The base URL for the meet-metrics API.
+ * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} sessionId - The meeting session ID.
+ * @param {SalesforceObjectType} type - The type of Salesforce object.
+ * @param {Object} data - The match data for the object.
+ * @returns {Promise}
+ */
+export async function linkSession(
+ url: string,
+ jwt: string,
+ sessionId: string,
+ type: SalesforceObjectType,
+ data: IAccountMatch | ILeadMatch | IContactMatch | IOpportunityMatch
+): Promise {
+ return fetchWithAuth(`${url}/api/salesforce/sessions/${sessionId}/link`, jwt, {
+ method: 'POST',
+ body: JSON.stringify({ type, data })
});
}
/**
-* Executes the meeting linking.
-*
-* @param {string} url - The endpoint for meeting linking.
-* @param {string} jwt - The JWT needed for authentication.
-* @param {string} sessionId - The ID of the meeting session.
-* @param {Object} body - The body of the request.
-* @returns {Object}
-*/
-export async function executeLinkMeetingRequest(
+ * Confirms a pending account match and auto-links the best opportunity.
+ *
+ * @param {string} url - The base URL for the meet-metrics API.
+ * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} sessionId - The meeting session ID.
+ * @param {string} accountId - The Salesforce Account ID to confirm.
+ * @returns {Promise}
+ */
+export async function confirmPendingAccount(
url: string,
jwt: string,
- sessionId: String,
- body: {
- id?: string;
- notes: string;
- type?: string;
- }
-) {
- const fullUrl = `${url}/sessions/${sessionId}/records/${body.id}`;
- const res = await fetch(fullUrl, {
- method: 'PUT',
- headers: {
- 'Content-Type': 'application/json',
- 'Authorization': `Bearer ${jwt}`
- },
- body: JSON.stringify(body)
+ sessionId: string,
+ accountId: string
+): Promise {
+ return fetchWithAuth(`${url}/api/salesforce/sessions/${sessionId}/confirm-account`, jwt, {
+ method: 'POST',
+ body: JSON.stringify({ accountId })
});
+}
- const json = await res.json();
+/**
+ * Rejects all pending account matches for a session.
+ *
+ * @param {string} url - The base URL for the meet-metrics API.
+ * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} sessionId - The meeting session ID.
+ * @returns {Promise}
+ */
+export async function rejectPendingAccounts(
+ url: string,
+ jwt: string,
+ sessionId: string
+): Promise {
+ return fetchWithAuth(`${url}/api/salesforce/sessions/${sessionId}/pending-accounts`, jwt, {
+ method: 'DELETE'
+ });
+}
- return res.ok ? json : Promise.reject(json);
+/**
+ * Confirms a pending deal match.
+ *
+ * @param {string} url - The base URL for the meet-metrics API.
+ * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} sessionId - The meeting session ID.
+ * @param {string} opportunityId - The Salesforce Opportunity ID to confirm.
+ * @returns {Promise}
+ */
+export async function confirmPendingDeal(
+ url: string,
+ jwt: string,
+ sessionId: string,
+ opportunityId: string
+): Promise {
+ return fetchWithAuth(`${url}/api/salesforce/sessions/${sessionId}/confirm-deal`, jwt, {
+ method: 'POST',
+ body: JSON.stringify({ opportunityId })
+ });
+}
+
+/**
+ * Rejects all pending deal matches for a session.
+ *
+ * @param {string} url - The base URL for the meet-metrics API.
+ * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} sessionId - The meeting session ID.
+ * @returns {Promise}
+ */
+export async function rejectPendingDeals(
+ url: string,
+ jwt: string,
+ sessionId: string
+): Promise {
+ return fetchWithAuth(`${url}/api/salesforce/sessions/${sessionId}/pending-deals`, jwt, {
+ method: 'DELETE'
+ });
+}
+
+/**
+ * Unlinks a Salesforce object from a session.
+ *
+ * @param {string} url - The base URL for the meet-metrics API.
+ * @param {string} jwt - The JWT needed for authentication.
+ * @param {string} sessionId - The meeting session ID.
+ * @param {SalesforceObjectType} type - The type of Salesforce object to unlink.
+ * @param {string} id - The Salesforce object ID to unlink.
+ * @returns {Promise}
+ */
+export async function unlinkSession(
+ url: string,
+ jwt: string,
+ sessionId: string,
+ type: SalesforceObjectType,
+ id: string
+): Promise {
+ return fetchWithAuth(`${url}/api/salesforce/sessions/${sessionId}/link`, jwt, {
+ method: 'DELETE',
+ body: JSON.stringify({ type, id })
+ });
}
diff --git a/react/features/salesforce/types.ts b/react/features/salesforce/types.ts
new file mode 100644
index 000000000000..845319ceb30b
--- /dev/null
+++ b/react/features/salesforce/types.ts
@@ -0,0 +1,166 @@
+/**
+ * Pending account suggestion awaiting user confirmation.
+ */
+export interface IPendingAccount {
+ accountId: string;
+ accountName: string;
+ matchConfidence: number;
+ matchedEmailDomain?: string;
+}
+
+/**
+ * Pending deal/opportunity suggestion awaiting user confirmation.
+ */
+export interface IPendingDeal {
+ accountId: string;
+ accountName: string;
+ amount?: number;
+ closeDate?: string;
+ isClosed?: boolean;
+ matchConfidence: number;
+ opportunityId: string;
+ opportunityName: string;
+ opportunityStage: string;
+}
+
+/**
+ * Linked account information.
+ */
+export interface ILinkedAccount {
+ accountId: string;
+ accountName: string;
+}
+
+/**
+ * Linked lead information.
+ */
+export interface ILinkedLead {
+ leadCompany?: string;
+ leadId: string;
+ leadName: string;
+}
+
+/**
+ * Linked contact information.
+ */
+export interface ILinkedContact {
+ contactId: string;
+ contactName: string;
+}
+
+/**
+ * Linked deal/opportunity information.
+ */
+export interface ILinkedDeal {
+ amount?: number;
+ closeDate?: string;
+ isClosed?: boolean;
+ isWon?: boolean;
+ opportunityId: string;
+ opportunityName: string;
+ opportunityStage: string;
+ probability?: number;
+}
+
+/**
+ * Complete Salesforce data for a session including current links and pending suggestions.
+ */
+export interface ISalesforceData {
+ account?: ILinkedAccount;
+ contacts?: ILinkedContact[];
+ deal?: ILinkedDeal;
+ leads?: ILinkedLead[];
+ pendingAccounts?: IPendingAccount[];
+ pendingDeals?: IPendingDeal[];
+}
+
+/**
+ * Account match from search results.
+ */
+export interface IAccountMatch {
+ accountId: string;
+ accountName: string;
+ matchConfidence: number;
+ matchedEmailDomain?: string;
+}
+
+/**
+ * Lead match from search results.
+ */
+export interface ILeadMatch {
+ leadCompany?: string;
+ leadEmail: string;
+ leadId: string;
+ leadName: string;
+ matchConfidence: number;
+}
+
+/**
+ * Contact match from search results.
+ */
+export interface IContactMatch {
+ accountId?: string;
+ accountName?: string;
+ contactEmail: string;
+ contactId: string;
+ contactName: string;
+ matchConfidence: number;
+}
+
+/**
+ * Opportunity match from search results.
+ */
+export interface IOpportunityMatch {
+ accountId?: string;
+ accountName?: string;
+ amount?: number;
+ closeDate?: string;
+ isClosed?: boolean;
+ matchConfidence: number;
+ opportunityId: string;
+ opportunityName: string;
+ opportunityStage: string;
+}
+
+/**
+ * Unified search results from Salesforce.
+ */
+export interface ISearchResults {
+ accounts: IAccountMatch[];
+ contacts: IContactMatch[];
+ leads: ILeadMatch[];
+ opportunities: IOpportunityMatch[];
+}
+
+/**
+ * Salesforce object type for API calls.
+ */
+export type SalesforceObjectType = 'Account' | 'Lead' | 'Contact' | 'Opportunity';
+
+/**
+ * Link result from API.
+ */
+export interface ILinkResult {
+ error?: string;
+ success: boolean;
+}
+
+/**
+ * Confirm pending account result.
+ */
+export interface IConfirmAccountResult {
+ error?: string;
+ linkedOpportunity?: {
+ opportunityId: string;
+ opportunityName: string;
+ };
+ success: boolean;
+}
+
+/**
+ * Confirm pending deal result.
+ */
+export interface IConfirmDealResult {
+ error?: string;
+ success: boolean;
+}
diff --git a/react/features/salesforce/useSalesforceLinkDialog.ts b/react/features/salesforce/useSalesforceLinkDialog.ts
index d5c86d33c9d8..e35a3551cf92 100644
--- a/react/features/salesforce/useSalesforceLinkDialog.ts
+++ b/react/features/salesforce/useSalesforceLinkDialog.ts
@@ -1,6 +1,5 @@
-import { useCallback, useEffect, useState } from 'react';
+import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
-import { GestureResponderEvent } from 'react-native';
import { useDispatch, useSelector } from 'react-redux';
import { IReduxState } from '../app/types';
@@ -13,92 +12,300 @@ import {
} from '../notifications/constants';
import {
- executeLinkMeetingRequest,
- getRecentSessionRecords,
- getSessionRecordDetails,
- searchSessionRecords
+ confirmPendingAccount,
+ confirmPendingDeal,
+ getSessionSalesforceData,
+ linkSession,
+ rejectPendingAccounts,
+ rejectPendingDeals,
+ searchSalesforce,
+ unlinkSession
} from './functions';
+import {
+ IAccountMatch,
+ IContactMatch,
+ ILeadMatch,
+ IOpportunityMatch,
+ ISalesforceData,
+ ISearchResults,
+ SalesforceObjectType
+} from './types';
+
+/**
+ * Debounce helper.
+ *
+ * @param {Function} fn - Function to debounce.
+ * @param {number} delay - Delay in milliseconds.
+ * @returns {Function}
+ */
+function debounce any>(fn: T, delay: number): T {
+ let timeoutId: ReturnType;
-interface ISelectedRecord {
- id: string;
- name: string;
- onClick: (e?: React.MouseEvent | GestureResponderEvent) => void;
- type: string;
+ return ((...args: Parameters) => {
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(() => fn(...args), delay);
+ }) as T;
}
export const useSalesforceLinkDialog = () => {
const { t } = useTranslation();
const dispatch = useDispatch();
- const [ selectedRecord, setSelectedRecord ] = useState(null);
- const [ selectedRecordOwner, setSelectedRecordOwner ] = useState<{
- id: string; name: string; type: string; } | null>(null);
- const [ records, setRecords ] = useState([]);
- const [ isLoading, setLoading ] = useState(false);
- const [ searchTerm, setSearchTerm ] = useState(null);
- const [ notes, setNotes ] = useState('');
- const [ hasRecordsErrors, setRecordsErrors ] = useState(false);
- const [ hasDetailsErrors, setDetailsErrors ] = useState(false);
+
+ // Redux state
const conference = useSelector(getCurrentConference);
const sessionId = conference?.getMeetingUniqueId();
const { salesforceUrl = '' } = useSelector((state: IReduxState) => state['features/base/config']);
const { jwt = '' } = useSelector((state: IReduxState) => state['features/base/jwt']);
- const showSearchResults = searchTerm && searchTerm.length > 1;
- const showNoResults = showSearchResults && records.length === 0;
+
+ // Component state
+ const [ salesforceData, setSalesforceData ] = useState(null);
+ const [ searchResults, setSearchResults ] = useState(null);
+ const [ searchTerm, setSearchTerm ] = useState('');
+ const [ isLoading, setIsLoading ] = useState(true);
+ const [ isSearching, setIsSearching ] = useState(false);
+ const [ error, setError ] = useState(null);
+
+ // Loading states for actions
+ const [ confirmingAccountId, setConfirmingAccountId ] = useState(null);
+ const [ confirmingDealId, setConfirmingDealId ] = useState(null);
+ const [ rejectingAccounts, setRejectingAccounts ] = useState(false);
+ const [ rejectingDeals, setRejectingDeals ] = useState(false);
+ const [ linkingId, setLinkingId ] = useState(null);
+ const [ unlinkingId, setUnlinkingId ] = useState(null);
+
+ /**
+ * Refreshes the Salesforce data for the current session.
+ */
+ const refreshSalesforceData = useCallback(async () => {
+ if (!sessionId) {
+ return;
+ }
+
+ try {
+ const data = await getSessionSalesforceData(salesforceUrl, jwt, sessionId);
+
+ setSalesforceData(data);
+ setError(null);
+ } catch (err: any) {
+ console.error('Failed to fetch Salesforce data:', err);
+ setError(err?.error || t('dialog.salesforceDataError'));
+ }
+ }, [ salesforceUrl, jwt, sessionId, t ]);
+
+ /**
+ * Performs a search in Salesforce.
+ */
+ const performSearch = useCallback(async (query: string) => {
+ if (query.length < 2) {
+ setSearchResults(null);
+
+ return;
+ }
+
+ setIsSearching(true);
+
+ try {
+ const results = await searchSalesforce(salesforceUrl, jwt, query);
+
+ setSearchResults(results);
+ setError(null);
+ } catch (err: any) {
+ console.error('Salesforce search failed:', err);
+ setError(err?.error || t('dialog.searchResultsError'));
+ setSearchResults(null);
+ } finally {
+ setIsSearching(false);
+ }
+ }, [ salesforceUrl, jwt, t ]);
+
+ // Debounced search
+ const debouncedSearch = useMemo(
+ () => debounce(performSearch, 500),
+ [ performSearch ]
+ );
+
+ // Track if component is mounted
+ const isMounted = useRef(true);
useEffect(() => {
- const fetchRecords = async () => {
- setRecordsErrors(false);
- setLoading(true);
-
- try {
- const text = showSearchResults ? searchTerm : null;
- const result = text
- ? await searchSessionRecords(salesforceUrl, jwt, text)
- : await getRecentSessionRecords(salesforceUrl, jwt);
-
- setRecords(result);
- } catch (error) {
- setRecordsErrors(true);
- }
+ isMounted.current = true;
- setLoading(false);
+ return () => {
+ isMounted.current = false;
};
+ }, []);
- fetchRecords();
- }, [
- getRecentSessionRecords,
- jwt,
- salesforceUrl,
- searchSessionRecords,
- searchTerm
- ]);
-
+ // Fetch Salesforce data on mount
useEffect(() => {
- const fetchRecordDetails = async () => {
- setDetailsErrors(false);
- setSelectedRecordOwner(null);
- try {
- const result = await getSessionRecordDetails(salesforceUrl, jwt, selectedRecord);
-
- setSelectedRecordOwner({
- id: result.id,
- name: result.ownerName,
- type: 'OWNER'
- });
- } catch (error) {
- setDetailsErrors(true);
+ const fetchData = async () => {
+ setIsLoading(true);
+ await refreshSalesforceData();
+
+ if (isMounted.current) {
+ setIsLoading(false);
}
};
- selectedRecord && fetchRecordDetails();
- }, [
- jwt,
- getSessionRecordDetails,
- salesforceUrl,
- selectedRecord
- ]);
+ fetchData();
+ }, [ refreshSalesforceData ]);
+
+ // Trigger search when search term changes
+ useEffect(() => {
+ if (searchTerm.length >= 2) {
+ debouncedSearch(searchTerm);
+ } else {
+ setSearchResults(null);
+ }
+ }, [ searchTerm, debouncedSearch ]);
+
+ /**
+ * Handles confirming a pending account.
+ */
+ const handleConfirmAccount = useCallback(async (accountId: string) => {
+ if (!sessionId) {
+ return;
+ }
+
+ setConfirmingAccountId(accountId);
+
+ try {
+ const result = await confirmPendingAccount(salesforceUrl, jwt, sessionId, accountId);
+
+ if (result.success) {
+ dispatch(showNotification({
+ titleKey: 'notify.confirmAccountSuccess',
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.SUCCESS
+ }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
+
+ await refreshSalesforceData();
+ }
+ } catch (err: any) {
+ dispatch(showNotification({
+ titleKey: 'notify.confirmAccountError',
+ descriptionKey: err?.error,
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.ERROR
+ }, NOTIFICATION_TIMEOUT_TYPE.LONG));
+ } finally {
+ setConfirmingAccountId(null);
+ }
+ }, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
+
+ /**
+ * Handles rejecting all pending accounts.
+ */
+ const handleRejectAllAccounts = useCallback(async () => {
+ if (!sessionId) {
+ return;
+ }
+
+ setRejectingAccounts(true);
+
+ try {
+ await rejectPendingAccounts(salesforceUrl, jwt, sessionId);
+ dispatch(showNotification({
+ titleKey: 'notify.dismissSuccess',
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.SUCCESS
+ }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
+
+ await refreshSalesforceData();
+ } catch (err: any) {
+ dispatch(showNotification({
+ titleKey: 'notify.dismissError',
+ descriptionKey: err?.error,
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.ERROR
+ }, NOTIFICATION_TIMEOUT_TYPE.LONG));
+ } finally {
+ setRejectingAccounts(false);
+ }
+ }, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
+
+ /**
+ * Handles confirming a pending deal.
+ */
+ const handleConfirmDeal = useCallback(async (opportunityId: string) => {
+ if (!sessionId) {
+ return;
+ }
+
+ setConfirmingDealId(opportunityId);
+
+ try {
+ const result = await confirmPendingDeal(salesforceUrl, jwt, sessionId, opportunityId);
+
+ if (result.success) {
+ dispatch(showNotification({
+ titleKey: 'notify.confirmDealSuccess',
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.SUCCESS
+ }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
+
+ await refreshSalesforceData();
+ }
+ } catch (err: any) {
+ dispatch(showNotification({
+ titleKey: 'notify.confirmDealError',
+ descriptionKey: err?.error,
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.ERROR
+ }, NOTIFICATION_TIMEOUT_TYPE.LONG));
+ } finally {
+ setConfirmingDealId(null);
+ }
+ }, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
+
+ /**
+ * Handles rejecting all pending deals.
+ */
+ const handleRejectAllDeals = useCallback(async () => {
+ if (!sessionId) {
+ return;
+ }
+
+ setRejectingDeals(true);
+
+ try {
+ await rejectPendingDeals(salesforceUrl, jwt, sessionId);
+ dispatch(showNotification({
+ titleKey: 'notify.dismissSuccess',
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.SUCCESS
+ }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
+
+ await refreshSalesforceData();
+ } catch (err: any) {
+ dispatch(showNotification({
+ titleKey: 'notify.dismissError',
+ descriptionKey: err?.error,
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.ERROR
+ }, NOTIFICATION_TIMEOUT_TYPE.LONG));
+ } finally {
+ setRejectingDeals(false);
+ }
+ }, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
+
+ /**
+ * Handles linking a record from search results.
+ */
+ const handleLinkRecord = useCallback(async (
+ type: SalesforceObjectType,
+ data: IAccountMatch | ILeadMatch | IContactMatch | IOpportunityMatch
+ ) => {
+ if (!sessionId) {
+ return;
+ }
+
+ const id = type === 'Account' ? (data as IAccountMatch).accountId
+ : type === 'Lead' ? (data as ILeadMatch).leadId
+ : type === 'Contact' ? (data as IContactMatch).contactId
+ : (data as IOpportunityMatch).opportunityId;
+
+ setLinkingId(id);
- const linkMeeting = useCallback(async () => {
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceProgress',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
@@ -106,51 +313,127 @@ export const useSalesforceLinkDialog = () => {
}, NOTIFICATION_TIMEOUT_TYPE.STICKY));
try {
- await executeLinkMeetingRequest(salesforceUrl, jwt, sessionId, {
- id: selectedRecord?.id,
- type: selectedRecord?.type,
- notes
- });
+ await linkSession(salesforceUrl, jwt, sessionId, type, data);
+
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceSuccess',
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.SUCCESS
- }, NOTIFICATION_TIMEOUT_TYPE.LONG));
- } catch (error: any) {
+ }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
+
+ // Clear search and refresh data
+ setSearchTerm('');
+ setSearchResults(null);
+ await refreshSalesforceData();
+ } catch (err: any) {
dispatch(hideNotification(SALESFORCE_LINK_NOTIFICATION_ID));
dispatch(showNotification({
titleKey: 'notify.linkToSalesforceError',
- descriptionKey: error?.messageKey && t(error.messageKey),
+ descriptionKey: err?.error,
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.ERROR
+ }, NOTIFICATION_TIMEOUT_TYPE.LONG));
+ } finally {
+ setLinkingId(null);
+ }
+ }, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
+
+ /**
+ * Handles unlinking a record from the session.
+ */
+ const handleUnlinkRecord = useCallback(async (type: SalesforceObjectType, id: string) => {
+ if (!sessionId) {
+ return;
+ }
+
+ setUnlinkingId(id);
+
+ try {
+ await unlinkSession(salesforceUrl, jwt, sessionId, type, id);
+
+ dispatch(showNotification({
+ titleKey: 'notify.unlinkFromSalesforceSuccess',
+ uid: SALESFORCE_LINK_NOTIFICATION_ID,
+ appearance: NOTIFICATION_TYPE.SUCCESS
+ }, NOTIFICATION_TIMEOUT_TYPE.SHORT));
+
+ await refreshSalesforceData();
+ } catch (err: any) {
+ dispatch(showNotification({
+ titleKey: 'notify.unlinkFromSalesforceError',
+ descriptionKey: err?.error,
uid: SALESFORCE_LINK_NOTIFICATION_ID,
appearance: NOTIFICATION_TYPE.ERROR
}, NOTIFICATION_TIMEOUT_TYPE.LONG));
+ } finally {
+ setUnlinkingId(null);
}
+ }, [ salesforceUrl, jwt, sessionId, dispatch, refreshSalesforceData ]);
+
+ // Filter out already-linked items from search results
+ const filteredSearchResults = useMemo(() => {
+ if (!searchResults || !salesforceData) {
+ return searchResults;
+ }
+
+ return {
+ accounts: searchResults.accounts.filter(
+ account => salesforceData.account?.accountId !== account.accountId
+ ),
+ leads: searchResults.leads.filter(
+ lead => !salesforceData.leads?.some(l => l.leadId === lead.leadId)
+ ),
+ contacts: searchResults.contacts.filter(
+ contact => !salesforceData.contacts?.some(c => c.contactId === contact.contactId)
+ ),
+ opportunities: searchResults.opportunities.filter(
+ opp => salesforceData.deal?.opportunityId !== opp.opportunityId
+ )
+ };
+ }, [ searchResults, salesforceData ]);
- }, [
- executeLinkMeetingRequest,
- hideNotification,
- jwt,
- notes,
- salesforceUrl,
- selectedRecord,
- showNotification
- ]);
+ const hasSearchResults = filteredSearchResults
+ && (filteredSearchResults.accounts.length > 0
+ || filteredSearchResults.leads.length > 0
+ || filteredSearchResults.contacts.length > 0
+ || filteredSearchResults.opportunities.length > 0);
+
+ const hasPendingAccounts = (salesforceData?.pendingAccounts?.length ?? 0) > 0;
+ const hasPendingDeals = (salesforceData?.pendingDeals?.length ?? 0) > 0;
return {
- hasDetailsErrors,
- hasRecordsErrors,
- isLoading,
- linkMeeting,
- notes,
- records,
+ // Data
+ salesforceData,
+ searchResults: filteredSearchResults,
+ hasSearchResults,
+ hasPendingAccounts,
+ hasPendingDeals,
+
+ // State
searchTerm,
- selectedRecord,
- selectedRecordOwner,
- setNotes,
+ isLoading,
+ isSearching,
+ error,
+
+ // Action loading states
+ confirmingAccountId,
+ confirmingDealId,
+ rejectingAccounts,
+ rejectingDeals,
+ linkingId,
+ unlinkingId,
+
+ // Setters
setSearchTerm,
- setSelectedRecord,
- showNoResults,
- showSearchResults
+
+ // Actions
+ handleConfirmAccount,
+ handleRejectAllAccounts,
+ handleConfirmDeal,
+ handleRejectAllDeals,
+ handleLinkRecord,
+ handleUnlinkRecord,
+ refreshSalesforceData
};
};